Release: Enforce Code Freeze #131
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 'Release: Enforce Code Freeze' | |
| on: | |
| schedule: | |
| - cron: '0 18 * * *' # Every day at 18h UTC | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| concurrency: | |
| group: release-code-freeze | |
| env: | |
| GIT_COMMITTER_NAME: 'WooCommerce Bot' | |
| GIT_COMMITTER_EMAIL: 'no-reply@woocommerce.com' | |
| GIT_AUTHOR_NAME: 'WooCommerce Bot' | |
| GIT_AUTHOR_EMAIL: 'no-reply@woocommerce.com' | |
| jobs: | |
| check-feature-freeze-event: | |
| name: Check for Feature Freeze events today | |
| runs-on: ${{ ( github.repository == 'woocommerce/woocommerce' && 'blacksmith-2vcpu-ubuntu-2404' ) || 'ubuntu-latest' }} | |
| outputs: | |
| should-run: ${{ steps.check-feature-freeze-event.outputs.should-run }} | |
| steps: | |
| - name: Install node-ical | |
| run: npm install node-ical | |
| - name: Check for Feature Freeze events today | |
| id: check-feature-freeze-event | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1 | |
| with: | |
| script: | | |
| const shouldSkipCalendarCheck = '${{ github.event_name }}' !== 'schedule'; | |
| if (shouldSkipCalendarCheck) { | |
| console.log('Skip calendar event check, since the workflow was triggered manually.'); | |
| core.setOutput('should-run', 'true'); | |
| return; | |
| } | |
| const ical = require('node-ical'); | |
| console.log('Checking for Feature Freeze events today...'); | |
| try { | |
| const today = new Date(); | |
| const events = await ical.async.fromURL('https://calendar.google.com/calendar/ical/${{ secrets.RELEASE_CALENDAR_ID }}/public/basic.ics'); | |
| const dayEvents = Object.values(events) | |
| .filter(event => event.type === 'VEVENT') | |
| .filter(event => { | |
| if (!event.start) return false; | |
| return new Date(event.start).toDateString() === new Date(today).toDateString(); | |
| }); | |
| const ffEvent = dayEvents.find(event => { | |
| if (!event.summary) return false; | |
| const pattern = /^WooCommerce \d+\.\d+ Feature Freeze$/i; | |
| return pattern.test(event.summary); | |
| }); | |
| if (ffEvent) { | |
| console.log(`Found Feature Freeze event: ${ffEvent.summary}`); | |
| core.setOutput('should-run', 'true'); | |
| } else { | |
| console.log('No Feature Freeze events found for today. Code freeze will not proceed.'); | |
| core.setOutput('should-run', 'false'); | |
| } | |
| } catch (error) { | |
| core.setFailed(`Failed to fetch calendar events: ${error.message}`); | |
| } | |
| prepare-for-code-freeze: | |
| name: Calculate versions and confirm repo is in a good state | |
| needs: check-feature-freeze-event | |
| if: ${{ needs.check-feature-freeze-event.outputs.should-run == 'true' }} | |
| runs-on: ${{ ( github.repository == 'woocommerce/woocommerce' && 'blacksmith-2vcpu-ubuntu-2404' ) || 'ubuntu-latest' }} | |
| outputs: | |
| nextReleaseBranch: ${{ steps.calculate-versions.outputs.nextReleaseBranch }} | |
| nextReleaseVersion: ${{ steps.calculate-versions.outputs.nextReleaseVersion }} | |
| steps: | |
| - name: Verify no PRs open by the github-actions bot are open | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1 | |
| with: | |
| script: | | |
| const automatedPRs = await github.rest.search.issuesAndPullRequests({ | |
| q: `repo:${context.repo.owner}/${context.repo.repo} is:pr is:open author:app/github-actions`, | |
| }); | |
| if (automatedPRs.data.items.length > 0) { | |
| core.setFailed('There are PRs by the github-actions bot that are still open. Please merge or close before proceeding.'); | |
| process.exit(1); | |
| } | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: trunk | |
| sparse-checkout: | | |
| /plugins/woocommerce/woocommerce.php | |
| sparse-checkout-cone-mode: false | |
| - name: Fetch version from trunk | |
| id: fetch-trunk-version | |
| run: | | |
| header_version=$( cat plugins/woocommerce/woocommerce.php | grep -oP '(?<=Version: )(.+)' | head -n1 ) | |
| echo "Trunk version is '$header_version'." | |
| echo "trunk-version=$header_version" >> $GITHUB_OUTPUT | |
| - name: Compute next release and dev cycle versions | |
| id: calculate-versions | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1 | |
| with: | |
| script: | | |
| const trunkVersion = `${{ steps.fetch-trunk-version.outputs.trunk-version }}`; | |
| const m = trunkVersion.match( /^(\d+\.\d+)\.\d+(-dev)?$/ ); | |
| if ( ! m ) { | |
| core.setFailed( `Trunk version number is incorrect: ${ trunkVersion }` ); | |
| return; | |
| } | |
| const trunkMainVersion = m[1]; | |
| const nextReleaseVersion = trunkVersion.replace( '-dev', '' ); | |
| const nextReleaseBranch = `release/${ nextReleaseVersion.slice( 0, -2 ) }`; | |
| const nextTrunkVersion = `${ ( Number( trunkMainVersion ) + 0.1 ).toFixed( 1 ) }.0-dev`; | |
| core.setOutput( 'nextReleaseVersion', nextReleaseVersion ); | |
| core.setOutput( 'nextReleaseBranch', nextReleaseBranch ); | |
| core.setOutput( 'nextTrunkVersion', nextTrunkVersion ); | |
| run-code-freeze: | |
| name: Perform code freeze | |
| runs-on: ${{ ( github.repository == 'woocommerce/woocommerce' && 'blacksmith-2vcpu-ubuntu-2404' ) || 'ubuntu-latest' }} | |
| needs: prepare-for-code-freeze | |
| steps: | |
| - name: Checkout trunk | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: trunk | |
| - name: Push frozen branch to the repo | |
| run: | | |
| # Last opportunity to bail if branch already exists. | |
| if [[ -n $(git ls-remote --heads origin ${{ needs.prepare-for-code-freeze.outputs.nextReleaseBranch }}) ]]; then | |
| echo "::error::Release branch '${{ needs.prepare-for-code-freeze.outputs.nextReleaseBranch }}' already exists." | |
| exit 1 | |
| fi | |
| git checkout trunk | |
| git checkout -b ${{ needs.prepare-for-code-freeze.outputs.nextReleaseBranch }} | |
| git push origin ${{ needs.prepare-for-code-freeze.outputs.nextReleaseBranch }} | |
| bump-version-in-trunk: | |
| name: Bump version in trunk for next development cycle | |
| uses: ./.github/workflows/release-bump-version.yml | |
| needs: [run-code-freeze] | |
| secrets: inherit | |
| with: | |
| branch: trunk | |
| bump-type: dev | |
| build-dev-release: | |
| name: 'Build WooCommerce -dev release' | |
| uses: ./.github/workflows/release-build-zip-file.yml | |
| needs: [prepare-for-code-freeze, run-code-freeze] | |
| with: | |
| skip_verify: true | |
| create_github_release: true | |
| branch: ${{ needs.prepare-for-code-freeze.outputs.nextReleaseBranch }} | |
| publish-dev-release: | |
| name: 'Publish -dev release' | |
| runs-on: ${{ ( github.repository == 'woocommerce/woocommerce' && 'blacksmith-2vcpu-ubuntu-2404' ) || 'ubuntu-latest' }} | |
| needs: [prepare-for-code-freeze, build-dev-release] | |
| outputs: | |
| release-zip: ${{ steps.publish-release.outputs.release-zip }} | |
| steps: | |
| - id: publish-release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| gh release edit '${{ needs.prepare-for-code-freeze.outputs.nextReleaseVersion }}-dev' \ | |
| --prerelease \ | |
| --latest=false \ | |
| --draft=false | |
| echo "release-zip=https://github.com/${{ github.repository }}/releases/download/${{ needs.prepare-for-code-freeze.outputs.nextReleaseVersion }}-dev/woocommerce.zip" >> $GITHUB_OUTPUT | |
| notify-slack: | |
| name: Notify Slack | |
| runs-on: ${{ ( github.repository == 'woocommerce/woocommerce' && 'blacksmith-2vcpu-ubuntu-2404' ) || 'ubuntu-latest' }} | |
| needs: [publish-dev-release, prepare-for-code-freeze] | |
| steps: | |
| - name: Notify Slack on success | |
| uses: archive/github-actions-slack@f530f3aa696b2eef0e5aba82450e387bd7723903 #v2.0.0 | |
| with: | |
| slack-bot-user-oauth-access-token: ${{ secrets.CODE_FREEZE_BOT_TOKEN }} | |
| slack-channel: ${{ secrets.WOO_RELEASE_SLACK_CHANNEL }} | |
| slack-text: | | |
| :ice_cube: Code Freeze completed for `${{ needs.prepare-for-code-freeze.outputs.nextReleaseBranch }}` :checking: | |
| If you need a fix or change to be included in this release, please request a backport following the <https://developer.woocommerce.com/docs/contribution/releases/backporting|Backporting Guide>. | |
| <${{ needs.publish-dev-release.outputs.release-zip }}|Download zip> | |
| slack-optional-unfurl_links: false | |
| slack-optional-unfurl_media: false | |
| trigger-webhook: | |
| name: Trigger Release Webhook | |
| runs-on: ${{ ( github.repository == 'woocommerce/woocommerce' && 'blacksmith-2vcpu-ubuntu-2404' ) || 'ubuntu-latest' }} | |
| needs: [publish-dev-release, prepare-for-code-freeze] | |
| steps: | |
| - name: Trigger Code Freeze Webhook | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1 | |
| with: | |
| script: | | |
| const crypto = require('crypto'); | |
| const payload = { | |
| action: 'feature-freeze', | |
| version: '${{ needs.prepare-for-code-freeze.outputs.nextReleaseVersion }}', | |
| build_zip: '${{ needs.publish-dev-release.outputs.release-zip }}', | |
| post_status: process.env.ENVIRONMENT === 'production' ? 'publish' : 'draft' | |
| }; | |
| console.log('Webhook payload:'); | |
| console.log(` Version: ${payload.version}`); | |
| console.log(` Build ZIP: ${payload.build_zip}`); | |
| console.log(` Post Status: ${payload.post_status}`); | |
| const requestBody = JSON.stringify({ | |
| payload | |
| }); | |
| const hmac = crypto.createHmac('sha256', process.env.WPCOM_WEBHOOK_SECRET); | |
| hmac.update(requestBody); | |
| const signature = hmac.digest('hex'); | |
| try { | |
| const response = await fetch(process.env.WPCOM_RELEASE_WEBHOOK_URL, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-Hub-Signature-256': `sha256=${signature}` | |
| }, | |
| body: requestBody | |
| }); | |
| if (!response.ok) { | |
| if (response.status === 409) { | |
| console.log('Webhook request failed: Duplicate post detected (409)'); | |
| } else if (response.status === 403) { | |
| console.log('Webhook request failed: Invalid secret (403)'); | |
| } | |
| core.setFailed(`Webhook request failed with status ${response.status}`); | |
| } else { | |
| console.log('Webhook triggered successfully'); | |
| } | |
| } catch (error) { | |
| core.setFailed(`Webhook request failed: ${error.message}`); | |
| } | |
| env: | |
| WPCOM_WEBHOOK_SECRET: ${{ secrets.WPCOM_WEBHOOK_SECRET }} | |
| WPCOM_RELEASE_WEBHOOK_URL: ${{ secrets.WPCOM_RELEASE_WEBHOOK_URL }} | |
| ENVIRONMENT: ${{ secrets.ENVIRONMENT }} | |