Skip to content

Release: Enforce Code Freeze #131

Release: Enforce Code Freeze

Release: Enforce Code Freeze #131

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 }}