Update code-freeze tool to work with accelerated

This commit is contained in:
Jonathan Sadowski 2023-11-13 15:24:21 -06:00
parent 703936e307
commit 4d6d57e63c
19 changed files with 1563 additions and 949 deletions

View File

@ -1,7 +1,7 @@
name: 'Release: Code freeze'
on:
schedule:
- cron: '0 23 * * 1' # Run at 2300 UTC on Mondays.
- cron: '0 0 * * 4' # Run at start of day UTC on Thursdays.
workflow_dispatch:
inputs:
timeOverride:
@ -31,10 +31,15 @@ jobs:
issues: write
pull-requests: write
outputs:
freeze: ${{ steps.check-freeze.outputs.freeze }}
nextReleaseBranch: ${{ steps.branch.outputs.nextReleaseBranch }}
nextReleaseVersion: ${{ steps.milestone.outputs.nextReleaseVersion }}
nextDevelopmentVersion: ${{ steps.milestone.outputs.nextDevelopmentVersion }}
isTodayAcceleratedFreeze: ${{ steps.get-versions.outputs.isTodayAcceleratedFreeze }}
isTodayMonthlyFreeze: ${{ steps.get-versions.outputs.isTodayMonthlyFreeze }}
acceleratedVersion: ${{ steps.get-versions.outputs.acceleratedVersion }}
monthlyVersion: ${{ steps.get-versions.outputs.monthlyVersion }}
monthlyVersionXY: ${{ steps.get-versions.outputs.monthlyVersionXY }}
releasesFrozenToday: ${{ steps.get-versions.outputs.releasesFrozenToday }}
acceleratedBranch: ${{ steps.get-versions.outputs.acceleratedBranch }}
monthlyBranch: ${{ steps.get-versions.outputs.monthlyBranch }}
monthlyMilestone: ${{ steps.get-versions.outputs.monthlyMilestone }}
steps:
- name: Checkout code
uses: actions/checkout@v3
@ -60,43 +65,74 @@ jobs:
pnpm build
working-directory: tools/monorepo-utils
- name: 'Check whether today is the code freeze day'
id: check-freeze
run: pnpm utils code-freeze verify-day -o $TIME_OVERRIDE
- name: 'Get the versions for the accelerated and monthly releases'
id: get-versions
run: pnpm utils code-freeze get-version -o $TIME_OVERRIDE
- name: Create next milestone
- name: Create next monthly milestone
id: milestone
if: steps.check-freeze.outputs.freeze == 'true'
if: steps.get-versions.outputs.isTodayMonthlyFreeze == 'yes'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm run utils code-freeze milestone -o ${{ github.repository_owner }}
run: pnpm run utils code-freeze milestone -o ${{ github.repository_owner }} -m ${{ steps.get-versions.outputs.monthlyMilestone }}
- name: Create next release branch
- name: Create next monthly release branch
id: branch
if: steps.check-freeze.outputs.freeze == 'true'
if: steps.get-versions.outputs.isTodayMonthlyFreeze == 'yes'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm run utils code-freeze branch -o ${{ github.repository_owner }}
run: pnpm run utils code-freeze branch -o ${{ github.repository_owner }} -b ${{ steps.get-versions.outputs.monthlyBranch }}
- name: Create next accelerated release branch
id: branch-accel
if: steps.get-versions.outputs.isTodayAcceleratedFreeze == 'yes'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm run utils code-freeze branch -o ${{ github.repository_owner }} -b ${{ steps.get-versions.outputs.acceleratedBranch }}
- name: Bump versions for Beta.1 monthly release
id: version-bump
if: steps.get-versions.outputs.isTodayMonthlyFreeze == 'yes'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm run utils code-freeze version-bump -o ${{ github.repository_owner }} -b ${{ steps.get-versions.outputs.monthlyBranch }} -c ${{ steps.get-versions.outputs.monthlyVersion }}-beta.1
- name: Bump versions for accelerated release
id: version-bump-accel
if: steps.get-versions.outputs.isTodayAcceleratedFreeze == 'yes'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm run utils code-freeze version-bump -o ${{ github.repository_owner }} -b ${{ steps.get-versions.outputs.acceleratedBranch }} -c ${{ steps.get-versions.outputs.acceleratedVersion }} -af
- name: Prep accelerated release
id: accel-release-prep
if: steps.get-versions.outputs.isTodayAcceleratedFreeze == 'yes'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm utils code-freeze accelerated-prep -o ${{ github.repository_owner }} -b ${{ steps.get-versions.outputs.acceleratedBranch }} -c ${{ steps.get-versions.outputs.acceleratedVersion }} ${{ steps.get-versions.outputs.acceleratedReleaseDate }}
- name: Prepare trunk for next development cycle
id: prep-trunk
if: steps.check-freeze.outputs.freeze == 'true'
if: steps.get-versions.outputs.isTodayMonthlyFreeze == 'yes'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm run utils code-freeze version-bump ${{ steps.milestone.outputs.nextDevelopmentVersion }}.0-dev -o ${{ github.repository_owner }}
run: pnpm run utils code-freeze version-bump ${{ steps.get-versions.outputs.monthlyMilestone }}-dev -o ${{ github.repository_owner }}
- name: Generate changelog changes
id: changelog
if: steps.check-freeze.outputs.freeze == 'true'
if: steps.get-versions.outputs.isTodayMonthlyFreeze == 'yes'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm run utils code-freeze changelog -o ${{ github.repository_owner }} -v ${{ steps.milestone.outputs.nextReleaseVersion }}
run: pnpm run utils code-freeze changelog -c -o ${{ github.repository_owner }} -v ${{ steps.get-versions.outputs.monthlyVersionXY }}
notify-slack:
name: 'Sends code freeze notification to Slack'
runs-on: ubuntu-20.04
needs: code-freeze-prep
if: ${{ needs.code-freeze-prep.outputs.freeze == 'true' && inputs.skipSlackPing != true }}
if: ${{ inputs.skipSlackPing != true && ( needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' || needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' ) }}
outputs:
ts: ${{ steps.notify.outputs.ts }}
steps:
- name: Checkout code
uses: actions/checkout@v3
@ -126,6 +162,210 @@ jobs:
id: notify
run: |
pnpm utils slack "${{ secrets.CODE_FREEZE_BOT_TOKEN }}" "
:warning-8c: ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} Code Freeze :ice_cube:
The automation to cut the release branch for ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} has run. Any PRs that were not already merged will be a part of ${{ needs.code-freeze-prep.outputs.nextDevelopmentVersion }} by default. If you have something that needs to make ${{ needs.code-freeze-prep.outputs.nextReleaseVersion }} that hasn't yet been merged, please see the <${{ secrets.FG_LINK }}/code-freeze-for-woocommerce-core-release/|fieldguide page for the code freeze>.
:warning-8c: ${{ join( fromJSON( needs.code-freeze-prep.outputs.releasesFrozenToday ), ' and ' ) }} Code Freeze :ice_cube:
The freeze automation for ${{ join( fromJSON( needs.code-freeze-prep.outputs.releasesFrozenToday ), ' and ' ) }} has finished. ${{ ( needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' && 'If you need to request a code freeze exception, see the <' ) || '' }}${{ ( needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' && secrets.FG_LINK ) || '' }}${{ ( needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' && '/code-freeze-for-woocommerce-core-release/|fieldguide page for the code freeze>.' ) || '' }}
The build for ${{ join( fromJSON( needs.code-freeze-prep.outputs.releasesFrozenToday ), ' and ' ) }} will appear in this thread shortly... :thread:
" "${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}"
build-monthly:
name: Build beta zip file
runs-on: ubuntu-20.04
needs: code-freeze-prep
if: ${{ needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' }}
permissions:
contents: read
steps:
- uses: actions/checkout@v3
with:
ref: ${{ needs.code-freeze-prep.outputs.monthlyBranch }}
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
build: false
- name: Build zip
working-directory: plugins/woocommerce
run: bash bin/build-zip.sh
- name: Upload the zip file as an artifact
uses: actions/upload-artifact@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: double-zipped-woocommerce.${{ needs.code-freeze-prep.outputs.monthlyVersion }}-beta.1
path: plugins/woocommerce/woocommerce.zip
retention-days: 2
build-a:
name: Build accelerated zip file
runs-on: ubuntu-20.04
needs: code-freeze-prep
if: ${{ needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' }}
permissions:
contents: read
steps:
- uses: actions/checkout@v3
with:
ref: ${{ needs.code-freeze-prep.outputs.acceleratedBranch }}
- name: Setup WooCommerce Monorepo
uses: ./.github/actions/setup-woocommerce-monorepo
with:
build: false
- name: Build zip
working-directory: plugins/woocommerce
run: bash bin/build-zip.sh
- name: Upload the zip file as an artifact
uses: actions/upload-artifact@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: double-zipped-woocommerce.${{ needs.code-freeze-prep.outputs.acceleratedVersion }}
path: plugins/woocommerce/woocommerce.zip
retention-days: 2
slack-upload-monthly:
name: Upload Beta to Slack
runs-on: ubuntu-20.04
needs: [ code-freeze-prep, notify-slack, build-monthly ]
if: ${{ needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' && inputs.skipSlackPing != true }}
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup PNPM
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
with:
version: '8.6.7'
- name: Setup Node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
with:
node-version-file: .nvmrc
cache: pnpm
registry-url: 'https://registry.npmjs.org'
- name: Install prerequisites
run: |
pnpm install --filter monorepo-utils --ignore-scripts
# ignore scripts speeds up setup signficantly, but we still need to build monorepo utils
pnpm build
working-directory: tools/monorepo-utils
- id: download
uses: actions/download-artifact@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: double-zipped-woocommerce.${{ needs.code-freeze-prep.outputs.monthlyVersion }}-beta.1
path: download
- run: ls -lah ${{steps.download.outputs.download-path}}
- name: Send release zip to Slack
id: send-file-slack
run : |
pnpm utils slack file "${{ secrets.CODE_FREEZE_BOT_TOKEN }}" "Here's the generated release build for ${{ needs.code-freeze-prep.outputs.monthlyVersion }}-beta.1" "${{ steps.download.outputs.download-path }}/woocommerce.zip" "${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}" --reply-ts ${{ needs.notify-slack.outputs.ts }} --filename "woocommerce.${{ needs.code-freeze-prep.outputs.monthlyVersion }}-beta.1.zip"
slack-upload-accelerated:
name: Upload Accelerated to Slack
runs-on: ubuntu-20.04
needs: [ code-freeze-prep, notify-slack, build-a ]
if: ${{ needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' && inputs.skipSlackPing != true }}
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup PNPM
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
with:
version: '8.6.7'
- name: Setup Node
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
with:
node-version-file: .nvmrc
cache: pnpm
registry-url: 'https://registry.npmjs.org'
- name: Install prerequisites
run: |
pnpm install --filter monorepo-utils --ignore-scripts
# ignore scripts speeds up setup signficantly, but we still need to build monorepo utils
pnpm build
working-directory: tools/monorepo-utils
- id: download
uses: actions/download-artifact@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: double-zipped-woocommerce.${{ needs.code-freeze-prep.outputs.acceleratedVersion }}
path: download
- run: ls -lah ${{steps.download.outputs.download-path}}
- name: Send release zip to Slack
id: send-file-slack
run : |
pnpm utils slack file "${{ secrets.CODE_FREEZE_BOT_TOKEN }}" "Here's the generated release build for ${{ needs.code-freeze-prep.outputs.acceleratedVersion }}" "${{ steps.download.outputs.download-path }}/woocommerce.zip" "${{ inputs.slackChannelOverride || secrets.WOO_RELEASE_SLACK_CHANNEL }}" --reply-ts ${{ needs.notify-slack.outputs.ts }} --filename "woocommerce.${{ needs.code-freeze-prep.outputs.acceleratedVersion }}.zip"
github-upload-monthly:
name: Create single-zipped GitHub asset (Monthly)
runs-on: ubuntu-20.04
needs: [ code-freeze-prep, build-monthly ]
if: ${{ needs.code-freeze-prep.outputs.isTodayMonthlyFreeze == 'yes' }}
steps:
- id: download
uses: actions/download-artifact@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: double-zipped-woocommerce.${{ needs.code-freeze-prep.outputs.monthlyVersion }}-beta.1
path: download
- name: Unzip the file (prevents double zip problem)
run: unzip ${{ steps.download.outputs.download-path }}/woocommerce.zip -d zipfile
- name: Upload the zip file as an artifact
uses: actions/upload-artifact@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: woocommerce.${{ needs.code-freeze-prep.outputs.monthlyVersion }}-beta.1
path: zipfile
retention-days: 10
github-upload-accelerated:
name: Create single-zipped GitHub asset (Accelerated)
runs-on: ubuntu-20.04
needs: [ code-freeze-prep, build-a ]
if: ${{ needs.code-freeze-prep.outputs.isTodayAcceleratedFreeze == 'yes' }}
steps:
- id: download
uses: actions/download-artifact@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: double-zipped-woocommerce.${{ needs.code-freeze-prep.outputs.acceleratedVersion }}
path: download
- name: Unzip the file (prevents double zip problem)
run: unzip ${{ steps.download.outputs.download-path }}/woocommerce.zip -d zipfile
- name: Upload the zip file as an artifact
uses: actions/upload-artifact@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: woocommerce.${{ needs.code-freeze-prep.outputs.acceleratedVersion }}
path: zipfile
retention-days: 10

File diff suppressed because it is too large Load Diff

View File

@ -13,6 +13,7 @@
"@octokit/graphql": "4.8.0",
"@octokit/graphql-schema": "^14.1.0",
"@octokit/types": "^9.2.0",
"@slack/web-api": "^6.9.0",
"@types/cli-table": "^0.3.1",
"@types/uuid": "^9.0.1",
"chalk": "^4.1.2",
@ -24,13 +25,13 @@
"graphql": "^16.6.0",
"gray-matter": "^4.0.3",
"js-yaml": "^4.1.0",
"luxon": "^3.4.4",
"octokit": "^2.0.14",
"ora": "^5.4.1",
"promptly": "^3.2.0",
"semver": "^7.3.2",
"simple-git": "^3.10.0",
"uuid": "^9.0.0",
"@slack/web-api": "^6.9.0"
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/jest": "^27.4.1",

View File

@ -0,0 +1,159 @@
/**
* External dependencies
*/
import { Command } from '@commander-js/extra-typings';
import simpleGit from 'simple-git';
/**
* Internal dependencies
*/
import { Logger } from '../../../core/logger';
import {
sparseCheckoutRepoShallow,
checkoutRemoteBranch,
} from '../../../core/git';
import { createPullRequest } from '../../../core/github/repo';
import { getEnvVar } from '../../../core/environment';
import { Options } from './types';
import { addHeader, createChangelog } from './lib/prep';
export const acceleratedPrepCommand = new Command( 'accelerated-prep' )
.description( 'Prep for an accelerated release' )
.argument( '<version>', 'Version to bump to use for changelog' )
.argument( '<date>', 'Release date to use in changelog' )
.option(
'-o --owner <owner>',
'Repository owner. Default: woocommerce',
'woocommerce'
)
.option(
'-n --name <name>',
'Repository name. Default: woocommerce',
'woocommerce'
)
.option(
'-b --base <base>',
'Base branch to create the PR against. Default: trunk',
'trunk'
)
.option(
'-d --dry-run',
'Prepare the version bump and log a diff. Do not create a PR or push to branch',
false
)
.option(
'-c --commit-direct-to-base',
'Commit directly to the base branch. Do not create a PR just push directly to base branch',
false
)
.action( async ( version, date, options: Options ) => {
const { owner, name, base, dryRun, commitDirectToBase } = options;
Logger.startTask(
`Making a temporary clone of '${ owner }/${ name }'`
);
const source = `github.com/${ owner }/${ name }`;
const token = getEnvVar( 'GITHUB_TOKEN', true );
const remote = `https://${ owner }:${ token }@${ source }`;
const tmpRepoPath = await sparseCheckoutRepoShallow(
remote,
'woocommerce',
[
'plugins/woocommerce/includes/class-woocommerce.php',
// All that's needed is the line above, but including these here for completeness.
'plugins/woocommerce/composer.json',
'plugins/woocommerce/package.json',
'plugins/woocommerce/readme.txt',
'plugins/woocommerce/woocommerce.php',
]
);
Logger.endTask();
Logger.notice(
`Temporary clone of '${ owner }/${ name }' created at ${ tmpRepoPath }`
);
const git = simpleGit( {
baseDir: tmpRepoPath,
config: [ 'core.hooksPath=/dev/null' ],
} );
const branch = `prep/${ base }-accelerated`;
try {
if ( commitDirectToBase ) {
if ( base === 'trunk' ) {
Logger.error(
`The --commit-direct-to-base option cannot be used with the trunk branch as a base. A pull request must be created instead.`
);
}
Logger.notice( `Checking out ${ base }` );
await checkoutRemoteBranch( tmpRepoPath, base );
} else {
const exists = await git.raw( 'ls-remote', 'origin', branch );
if ( ! dryRun && exists.trim().length > 0 ) {
Logger.error(
`Branch ${ branch } already exists. Run \`git push <remote> --delete ${ branch }\` and rerun this command.`
);
}
if ( base !== 'trunk' ) {
// if the base is not trunk, we need to checkout the base branch first before creating a new branch.
Logger.notice( `Checking out ${ base }` );
await checkoutRemoteBranch( tmpRepoPath, base );
}
Logger.notice( `Creating new branch ${ branch }` );
await git.checkoutBranch( branch, base );
}
const workingBranch = commitDirectToBase ? base : branch;
Logger.notice(
`Adding Woo header to main plugin file and creating changelog.txt on ${ workingBranch } branch`
);
addHeader( tmpRepoPath );
createChangelog( tmpRepoPath, version, date );
if ( dryRun ) {
const diff = await git.diffSummary();
Logger.notice(
`The prep has been completed in the following files:`
);
Logger.warn( diff.files.map( ( f ) => f.file ).join( '\n' ) );
Logger.notice(
'Dry run complete. No pull was request created nor was a commit made.'
);
return;
}
Logger.notice( 'Adding and committing changes' );
await git.add( '.' );
await git.commit(
`Add Woo header to main plugin file and create changelog in ${ base }`
);
Logger.notice( `Pushing ${ workingBranch } branch to Github` );
await git.push( 'origin', workingBranch );
if ( ! commitDirectToBase ) {
Logger.startTask( 'Creating a pull request' );
const pullRequest = await createPullRequest( {
owner,
name,
title: `Add Woo header to main plugin file and create changelog in ${ base }`,
body: `This PR adds the Woo header to the main plugin file and creates a changelog.txt file in ${ base }.`,
head: branch,
base,
} );
Logger.notice(
`Pull request created: ${ pullRequest.html_url }`
);
Logger.endTask();
}
} catch ( error ) {
Logger.error( error );
}
} );

View File

@ -0,0 +1,57 @@
/**
* External dependencies
*/
import { readFile, writeFile } from 'fs/promises';
import { join } from 'path';
/**
* Internal dependencies
*/
import { Logger } from '../../../../core/logger';
/**
* Add Woo header to main plugin file.
*
* @param tmpRepoPath cloned repo path
*/
export const addHeader = async ( tmpRepoPath: string ): Promise< void > => {
const filePath = join( tmpRepoPath, 'plugins/woocommerce/woocommerce.php' );
try {
const pluginFileContents = await readFile( filePath, 'utf8' );
const updatedPluginFileContents = pluginFileContents.replace(
' * @package WooCommerce\n */',
' *\n * Woo: 18734002369816:624a1b9ba2fe66bb06d84bcdd401c6a6\n *\n * @package WooCommerce\n */'
);
await writeFile( filePath, updatedPluginFileContents );
} catch ( e ) {
Logger.error( e );
}
};
/**
* Create changelog file.
*
* @param tmpRepoPath cloned repo path
* @param version version for the changelog file
* @param date date of the release (Y-m-d)
*/
export const createChangelog = async (
tmpRepoPath: string,
version: string,
date: string
): Promise< void > => {
const filePath = join( tmpRepoPath, 'plugins/woocommerce/changelog.txt' );
try {
const changelogContents = `*** WooCommerce ***
${ date } - Version ${ version }
* Update - Deploy of WOoCommerce ${ version }
`;
await writeFile( filePath, changelogContents );
} catch ( e ) {
Logger.error( e );
}
};

View File

@ -0,0 +1,7 @@
export type Options = {
owner?: string;
name?: string;
base?: string;
dryRun?: boolean;
commitDirectToBase?: boolean;
};

View File

@ -28,6 +28,11 @@ export const changelogCommand = new Command( 'changelog' )
'-d --dev-repo-path <devRepoPath>',
'Path to existing repo. Use this option to avoid cloning a fresh repo for development purposes. Note that using this option assumes dependencies are already installed.'
)
.option(
'-c --commit-direct-to-base',
'Commit directly to the base branch. Do not create a PR just push directly to base branch',
false
)
.option(
'-o, --override <override>',
"Time Override: The time to use in checking whether the action should run (default: 'now').",
@ -39,10 +44,15 @@ export const changelogCommand = new Command( 'changelog' )
Logger.startTask(
`Making a temporary clone of '${ owner }/${ name }'`
);
const cloneOptions = {
owner: owner ? owner : 'woocommerce',
name: name ? name : 'woocommerce',
};
// Use a supplied path, otherwise do a full clone of the repo, including history so that changelogs can be created with links to PRs.
const tmpRepoPath = devRepoPath
? devRepoPath
: await cloneAuthenticatedRepo( options, false );
: await cloneAuthenticatedRepo( cloneOptions, false );
Logger.endTask();

View File

@ -13,7 +13,10 @@ import { Logger } from '../../../../core/logger';
import { checkoutRemoteBranch } from '../../../../core/git';
import { createPullRequest } from '../../../../core/github/repo';
import { Options } from '../types';
import { getToday } from '../../verify-day/utils';
import {
getToday,
DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE,
} from '../../get-version/lib';
/**
* Perform changelog adjustments after Jetpack Changelogger has run.
@ -27,9 +30,10 @@ const updateReleaseChangelogs = async (
) => {
const today = getToday( override );
// The release date is 22 days after the code freeze.
const releaseTime = new Date( today.getTime() + 22 * 24 * 60 * 60 * 1000 );
const releaseDate = releaseTime.toISOString().split( 'T' )[ 0 ];
const releaseTime = today.plus( {
days: DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE,
} );
const releaseDate = releaseTime.toISODate();
const readmeFile = path.join(
tmpRepoPath,
@ -79,7 +83,7 @@ export const updateReleaseBranchChangelogs = async (
tmpRepoPath: string,
releaseBranch: string
): Promise< { deletionCommitHash: string; prNumber: number } > => {
const { owner, name, version } = options;
const { owner, name, version, commitDirectToBase } = options;
try {
// Do a full checkout so that we can find the correct PR numbers for changelog entries.
await checkoutRemoteBranch( tmpRepoPath, releaseBranch, false );
@ -100,10 +104,12 @@ export const updateReleaseBranchChangelogs = async (
const branch = `update/${ version }-changelog`;
try {
if ( ! commitDirectToBase ) {
await git.checkout( {
'-b': null,
[ branch ]: null,
} );
}
Logger.notice( `Running the changelog script in ${ tmpRepoPath }` );
execSync(
@ -131,9 +137,18 @@ export const updateReleaseBranchChangelogs = async (
await git.commit(
`Update the readme files for the ${ version } release`
);
await git.push( 'origin', branch );
await git.push( 'origin', commitDirectToBase ? releaseBranch : branch );
await git.checkout( '.' );
if ( commitDirectToBase ) {
Logger.notice(
`Changelog update was committed directly to ${ releaseBranch }`
);
return {
deletionCommitHash: deletionCommitHash.trim(),
prNumber: -1,
};
}
Logger.notice( `Creating PR for ${ branch }` );
const pullRequest = await createPullRequest( {
owner,
@ -194,7 +209,9 @@ export const updateTrunkChangelog = async (
owner,
name,
title: `Release: Remove ${ version } change files`,
body: `This pull request was automatically generated during the code freeze to remove the changefiles from ${ version } that are compiled into the \`${ releaseBranch }\` branch via #${ prNumber }`,
body: `This pull request was automatically generated during the code freeze to remove the changefiles from ${ version } that are compiled into the \`${ releaseBranch }\` ${
prNumber > 0 ? `branch via #${ prNumber }` : ''
}`,
head: branch,
base: 'trunk',
} );

View File

@ -1,7 +1,8 @@
export type Options = {
owner: string;
name: string;
owner?: string;
name?: string;
version: string;
devRepoPath?: string;
override: string;
commitDirectToBase?: boolean;
override?: string;
};

View File

@ -0,0 +1,168 @@
/**
* External dependencies
*/
import { Command } from '@commander-js/extra-typings';
import { DateTime } from 'luxon';
import { setOutput } from '@actions/core';
import chalk from 'chalk';
/**
* Internal dependencies
*/
import { Logger } from '../../../core/logger';
import { isGithubCI } from '../../../core/environment';
import {
getToday,
getMonthlyCycle,
getAcceleratedCycle,
getVersionsBetween,
} from './lib/index';
const getRange = ( override, between ) => {
if ( isGithubCI() ) {
Logger.error(
'-b, --between option is not compatible with GitHub CI Output.'
);
process.exit( 1 );
}
const today = getToday( override );
const end = getToday( between );
const versions = getVersionsBetween( today, end );
Logger.notice(
chalk.greenBright.bold(
`Releases Between ${ today.toFormat( 'DDDD' ) } and ${ end.toFormat(
'DDDD'
) }\n`
)
);
Logger.table(
[ 'Version', 'Development Begins', 'Freeze', 'Release' ],
versions.map( ( v ) =>
Object.values( v ).map( ( d: DateTime | string ) =>
typeof d.toFormat === 'function'
? d.toFormat( 'EEE, MMM dd, yyyy' )
: d
)
)
);
process.exit( 0 );
};
export const getVersionCommand = new Command( 'get-version' )
.description( 'Get the release calendar for a given date' )
.option(
'-o, --override <override>',
"Time Override: The time to use in checking whether the action should run (default: 'now').",
'now'
)
.option(
'-b, --between <between>',
'When provided, instead of showing a single day, will show a releases in the range of <override> to <end>.'
)
.action( ( { override, between } ) => {
if ( between ) {
return getRange( override, between );
}
const today = getToday( override );
const acceleratedRelease = getAcceleratedCycle( today, false );
const acceleratedDevelopment = getAcceleratedCycle( today );
const monthlyRelease = getMonthlyCycle( today, false );
const monthlyDevelopment = getMonthlyCycle( today );
// Generate human-friendly output.
Logger.notice(
chalk.greenBright.bold(
`Release Calendar for ${ today.toFormat( 'DDDD' ) }\n`
)
);
const table = [];
// We're not in a release cycle on Wednesday.
if ( today.get( 'weekday' ) !== 3 ) {
table.push( [
`${ chalk.red( 'Accelerated Release Cycle' ) }`,
acceleratedRelease.version,
acceleratedRelease.begin.toFormat( 'EEE, MMM dd, yyyy' ),
acceleratedRelease.freeze.toFormat( 'EEE, MMM dd, yyyy' ),
acceleratedRelease.release.toFormat( 'EEE, MMM dd, yyyy' ),
] );
}
table.push( [
`${ chalk.red( 'Accelerated Development Cycle' ) }`,
acceleratedDevelopment.version,
acceleratedDevelopment.begin.toFormat( 'EEE, MMM dd, yyyy' ),
acceleratedDevelopment.freeze.toFormat( 'EEE, MMM dd, yyyy' ),
acceleratedDevelopment.release.toFormat( 'EEE, MMM dd, yyyy' ),
] );
// We're only in a release cycle if it is after the freeze day.
if ( today > monthlyRelease.freeze ) {
table.push( [
`${ chalk.red( 'Monthly Release Cycle' ) }`,
monthlyRelease.version,
monthlyRelease.begin.toFormat( 'EEE, MMM dd, yyyy' ),
monthlyRelease.freeze.toFormat( 'EEE, MMM dd, yyyy' ),
monthlyRelease.release.toFormat( 'EEE, MMM dd, yyyy' ),
] );
}
table.push( [
`${ chalk.red( 'Monthly Development Cycle' ) }`,
monthlyDevelopment.version,
monthlyDevelopment.begin.toFormat( 'EEE, MMM dd, yyyy' ),
monthlyDevelopment.freeze.toFormat( 'EEE, MMM dd, yyyy' ),
monthlyDevelopment.release.toFormat( 'EEE, MMM dd, yyyy' ),
] );
Logger.table(
[ '', 'Version', 'Development Begins', 'Freeze', 'Release' ],
table
);
if ( isGithubCI() ) {
// For the machines.
const isTodayAcceleratedFreeze = today.get( 'weekday' ) === 4;
const isTodayMonthlyFreeze = +today === +monthlyDevelopment.begin;
const monthlyVersionXY = monthlyRelease.version.substr(
0,
monthlyRelease.version.lastIndexOf( '.' )
);
setOutput(
'isTodayAcceleratedFreeze',
isTodayAcceleratedFreeze ? 'yes' : 'no'
);
setOutput(
'isTodayMonthlyFreeze',
isTodayMonthlyFreeze ? 'yes' : 'no'
);
setOutput( 'acceleratedVersion', acceleratedRelease.version );
setOutput( 'monthlyVersion', monthlyRelease.version );
setOutput( 'monthlyVersionXY', monthlyVersionXY );
setOutput(
'releasesFrozenToday',
JSON.stringify(
Object.values( {
...( isTodayMonthlyFreeze && {
monthlyVersion: `${ monthlyRelease.version } (Monthly)`,
} ),
...( isTodayAcceleratedFreeze && {
aVersion: `${ acceleratedRelease.version } (AF)`,
} ),
} )
)
);
setOutput(
'acceleratedBranch',
`release/${ acceleratedRelease.version }`
);
setOutput( 'monthlyBranch', `release/${ monthlyVersionXY }` );
setOutput( 'monthlyMilestone', monthlyDevelopment.version );
setOutput(
'acceleratedReleaseDate',
acceleratedDevelopment.release.toISODate()
);
}
process.exit( 0 );
} );

View File

@ -0,0 +1,150 @@
/**
* External dependencies
*/
import { DateTime } from 'luxon';
export const DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE = 19;
/**
* Get a DateTime object of now or the override time when specified. DateTime is normalized to start of day.
*
* @param {string} now The time to use in checking if today is the day of the code freeze. Default to now. Supports ISO formatted dates or 'now'.
*
* @return {DateTime} The DateTime object of now or the override time when specified.
*/
export const getToday = ( now = 'now' ): DateTime => {
const today =
now === 'now'
? DateTime.now().setZone( 'utc' )
: DateTime.fromISO( now, { zone: 'utc' } );
if ( isNaN( today.toMillis() ) ) {
throw new Error(
'Invalid date: Check the override parameter (-o, --override) is a correct ISO formatted string or "now"'
);
}
return today.set( { hour: 0, minute: 0, second: 0, millisecond: 0 } );
};
/**
* Get the second Tuesday of the month, given a DateTime.
*
* @param {DateTime} when A DateTime object.
*
* @return {DateTime} The second Tuesday of the month contained in the input.
*/
export const getSecondTuesday = ( when: DateTime ): DateTime => {
const year = when.get( 'year' );
const month = when.get( 'month' );
const firstDayOfMonth = DateTime.utc( year, month, 1 );
const dayOfWeek = firstDayOfMonth.get( 'weekday' );
const secondTuesday = dayOfWeek <= 2 ? 10 - dayOfWeek : 17 - dayOfWeek;
return DateTime.utc( year, month, secondTuesday );
};
export const getMonthlyCycle = ( when: DateTime, development = true ) => {
// July 12, 2023 is the start-point for 8.0.0, all versions follow that starting point.
const startTime = DateTime.fromObject(
{
year: 2023,
month: 7,
day: 12,
hour: 0,
minute: 0,
},
{ zone: 'UTC' }
);
const currentMonthRelease = getSecondTuesday( when );
const nextMonthRelease = getSecondTuesday(
currentMonthRelease.plus( { months: 1 } )
);
const release =
when <= currentMonthRelease ? currentMonthRelease : nextMonthRelease;
const previousRelease = getSecondTuesday(
release.minus( { days: DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE + 2 } )
);
const nextRelease = getSecondTuesday( release.plus( { months: 1 } ) );
const freeze = release.minus( {
days: DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE + 1,
} );
const monthNumber =
( previousRelease.get( 'year' ) - startTime.get( 'year' ) ) * 12 +
previousRelease.get( 'month' ) -
startTime.get( 'month' );
const version = ( ( 80 + monthNumber ) / 10 ).toFixed( 1 ) + '.0';
if ( development ) {
if ( when > freeze ) {
return getMonthlyCycle( nextRelease, false );
}
}
const begin = previousRelease.minus( {
days: DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE,
} );
return {
version,
begin,
freeze,
release,
};
};
/**
* Get version and all dates / related to an accelerated cycle.
*
* @param {DateTime} when A DateTime object.
* @param {boolean} development When true, the active development cycle will be returned, otherwise the active release cycle.
* @return {Object} An object containing version and dates for a release.
*/
export const getAcceleratedCycle = ( when: DateTime, development = true ) => {
if ( ! development ) {
when = when.minus( { week: 1 } );
}
const dayOfWeek = when.get( 'weekday' );
const daysTilWednesday = dayOfWeek < 4 ? 3 - dayOfWeek : 10 - dayOfWeek;
const freeze = when.plus( { days: daysTilWednesday } );
const lastAccelerated = freeze.minus( { days: 1 } );
const release = freeze.plus( { days: 6 } );
const begin = freeze.minus( { days: 6 } );
const currentMonthRelease = getSecondTuesday( lastAccelerated );
const nextMonthRelease = getSecondTuesday(
currentMonthRelease.plus( { months: 1 } )
);
const monthlyRelease =
freeze <= currentMonthRelease ? currentMonthRelease : nextMonthRelease;
const monthlyCycle = getMonthlyCycle( monthlyRelease, false );
const previousMonthlyRelease = getSecondTuesday(
monthlyRelease.minus( { days: 28 } )
);
const aVersion =
10 *
( lastAccelerated.diff( previousMonthlyRelease, 'weeks' ).toObject()
.weeks +
1 );
const version = `${ monthlyCycle.version }.${ aVersion }`;
return {
version,
begin,
freeze,
release,
};
};
export const getVersionsBetween = ( start: DateTime, end: DateTime ) => {
if ( start > end ) {
return getVersionsBetween( end, start );
}
const versions = {};
for ( let i = start; i < end; i = i.plus( { days: 28 } ) ) {
const monthly = getMonthlyCycle( i, false );
versions[ monthly.version ] = monthly;
}
for ( let i = start; i < end; i = i.plus( { days: 7 } ) ) {
const accelerated = getAcceleratedCycle( i, false );
versions[ accelerated.version ] = accelerated;
}
return Object.values( versions );
};

View File

@ -6,18 +6,20 @@ import { Command } from '@commander-js/extra-typings';
/**
* Internal dependencies
*/
import { verifyDayCommand } from './verify-day';
import { getVersionCommand } from './get-version';
import { milestoneCommand } from './milestone';
import { branchCommand } from './branch';
import { versionBumpCommand } from './version-bump';
import { changelogCommand } from './changelog';
import { acceleratedPrepCommand } from './accelerated-prep';
const program = new Command( 'code-freeze' )
.description( 'Code freeze utilities' )
.addCommand( verifyDayCommand )
.addCommand( getVersionCommand )
.addCommand( milestoneCommand )
.addCommand( branchCommand )
.addCommand( versionBumpCommand )
.addCommand( changelogCommand );
.addCommand( changelogCommand )
.addCommand( acceleratedPrepCommand );
export default program;

View File

@ -9,10 +9,8 @@ import ora from 'ora';
*/
import { getLatestGithubReleaseVersion } from '../../../core/github/repo';
import { octokitWithAuth } from '../../../core/github/api';
import { setGithubMilestoneOutputs } from './utils';
import { WPIncrement } from '../../../core/version';
import { Logger } from '../../../core/logger';
import { isGithubCI } from '../../../core/environment';
export const milestoneCommand = new Command( 'milestone' )
.description( 'Create a milestone' )
@ -33,14 +31,6 @@ export const milestoneCommand = new Command( 'milestone' )
)
.action( async ( options ) => {
const { owner, name, dryRun, milestone } = options;
const isGithub = isGithubCI();
if ( milestone && isGithub ) {
Logger.error(
"You can't manually supply a milestone using Github mode. Please use the CLI locally to add a milestone."
);
process.exit( 1 );
}
let nextMilestone;
let nextReleaseVersion;
@ -105,12 +95,6 @@ export const milestoneCommand = new Command( 'milestone' )
Logger.notice(
`Milestone ${ nextMilestone } already exists in ${ owner }/${ name }`
);
if ( isGithub ) {
setGithubMilestoneOutputs(
nextReleaseVersion,
nextMilestone
);
}
process.exit( 0 );
} else {
milestoneSpinner.fail();
@ -123,9 +107,7 @@ export const milestoneCommand = new Command( 'milestone' )
}
milestoneSpinner.succeed();
if ( isGithub ) {
setGithubMilestoneOutputs( nextReleaseVersion, nextMilestone );
}
Logger.notice(
`Successfully created milestone ${ nextMilestone } in ${ owner }/${ name }`
);

View File

@ -1,23 +0,0 @@
/**
* External dependencies
*/
import { setOutput } from '@actions/core';
/**
* Internal dependencies
*/
import { getMajorMinor } from '../../../core/version';
/**
* Set Github outputs.
*
* @param {string} nextReleaseVersion Next release version
* @param {string} nextMilestone Next milestone
*/
export const setGithubMilestoneOutputs = (
nextReleaseVersion: string,
nextMilestone: string
): void => {
setOutput( 'nextReleaseVersion', getMajorMinor( nextReleaseVersion ) );
setOutput( 'nextDevelopmentVersion', getMajorMinor( nextMilestone ) );
};

View File

@ -1,48 +0,0 @@
/**
* External dependencies
*/
import { Command } from '@commander-js/extra-typings';
import { setOutput } from '@actions/core';
/**
* Internal dependencies
*/
import {
isTodayCodeFreezeDay,
DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE,
getToday,
getFutureDate,
} from './utils';
import { Logger } from '../../../core/logger';
import { isGithubCI } from '../../../core/environment';
export const verifyDayCommand = new Command( 'verify-day' )
.description( 'Verify if today is the code freeze day' )
.option(
'-o, --override <override>',
"Time Override: The time to use in checking whether the action should run (default: 'now').",
'now'
)
.action( ( { override } ) => {
const today = getToday( override );
const futureDate = getFutureDate( today );
Logger.warn( "Today's timestamp UTC is: " + today.toUTCString() );
Logger.warn(
`Checking to see if ${ DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE } days from today is the second Tuesday of the month.`
);
const isCodeFreezeDay = isTodayCodeFreezeDay( override );
Logger.notice(
`${ futureDate.toUTCString() } ${
isCodeFreezeDay ? 'is' : 'is not'
} release day.`
);
Logger.notice(
`Today is ${ isCodeFreezeDay ? 'indeed' : 'not' } code freeze day.`
);
if ( isGithubCI() ) {
setOutput( 'freeze', isCodeFreezeDay.toString() );
}
process.exit( 0 );
} );

View File

@ -1,36 +0,0 @@
/**
* Internal dependencies
*/
import { isTodayCodeFreezeDay } from '../index';
describe( 'isTodayCodeFreezeDay', () => {
it( 'should return false when given a day not 22 days before release', () => {
const JUNE_5_2023 = '2023-06-05T00:00:00.000Z';
const JUNE_12_2023 = '2023-06-12T00:00:00.000Z';
const JUNE_26_2023 = '2023-06-26T00:00:00.000Z';
const AUG_10_2023 = '2023-08-10T00:00:00.000Z';
const AUG_17_2023 = '2023-08-17T00:00:00.000Z';
const AUG_24_2023 = '2023-08-24T00:00:00.000Z';
expect( isTodayCodeFreezeDay( JUNE_5_2023 ) ).toBeFalsy();
expect( isTodayCodeFreezeDay( JUNE_12_2023 ) ).toBeFalsy();
expect( isTodayCodeFreezeDay( JUNE_26_2023 ) ).toBeFalsy();
expect( isTodayCodeFreezeDay( AUG_10_2023 ) ).toBeFalsy();
expect( isTodayCodeFreezeDay( AUG_17_2023 ) ).toBeFalsy();
expect( isTodayCodeFreezeDay( AUG_24_2023 ) ).toBeFalsy();
} );
it( 'should return true when given a day 22 days before release', () => {
const JUNE_19_2023 = '2023-06-19T00:00:00.000Z';
const JULY_17_2023 = '2023-07-17T00:00:00.000Z';
const AUGUST_21_2023 = '2023-08-21T00:00:00.000Z';
expect( isTodayCodeFreezeDay( JUNE_19_2023 ) ).toBeTruthy();
expect( isTodayCodeFreezeDay( JULY_17_2023 ) ).toBeTruthy();
expect( isTodayCodeFreezeDay( AUGUST_21_2023 ) ).toBeTruthy();
} );
it( 'should error out when passed an invalid date', () => {
expect( () => isTodayCodeFreezeDay( 'invalid date' ) ).toThrow();
} );
} );

View File

@ -1,46 +0,0 @@
const MILLIS_IN_A_DAY = 24 * 60 * 60 * 1000;
export const DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE = 22;
/**
* Get a Date object of now or the override time when specified.
*
* @param {string} now The time to use in checking if today is the day of the code freeze. Default to now.
* @return {Date} The Date object of now or the override time when specified.
*/
export const getToday = ( now = 'now' ): Date => {
const today = now === 'now' ? new Date() : new Date( now );
if ( isNaN( today.getTime() ) ) {
throw new Error(
'Invalid date: Check the override parameter (-o, --override) is a correct Date string'
);
}
return today;
};
/**
* Get a future date from today to see if its the release day.
*
* @param {string} today The time to use in checking if today is the day of the code freeze. Default to now.
* @return {Date} The Date object of the future date.
*/
export const getFutureDate = ( today: Date ) => {
return new Date(
today.getTime() + DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE * MILLIS_IN_A_DAY
);
};
/**
* Determines if today is the day of the code freeze.
*
* @param {string} now The time to use in checking if today is the day of the code freeze. Default to now.
* @return {boolean} true if today is the day of the code freeze.
*/
export const isTodayCodeFreezeDay = ( now: string ) => {
const today = getToday( now );
const futureDate = getFutureDate( today );
const month = futureDate.getUTCMonth();
const year = futureDate.getUTCFullYear();
const firstDayOfMonth = new Date( Date.UTC( year, month, 1 ) );
const dayOfWeek = firstDayOfMonth.getUTCDay();
const secondTuesday = dayOfWeek <= 2 ? 10 - dayOfWeek : 17 - dayOfWeek;
return futureDate.getUTCDate() === secondTuesday;
};

View File

@ -4,10 +4,6 @@
import { Command } from '@commander-js/extra-typings';
import { ErrorCode, WebClient } from '@slack/web-api';
import { basename } from 'path';
/**
* External dependencies
*/
import { existsSync } from 'fs';
/**
@ -31,7 +27,22 @@ export const slackFileCommand = new Command( 'file' )
'--dont-fail',
'Do not fail the command if a message fails to send to any channel.'
)
.action( async ( token, text, filePath, channels, { dontFail } ) => {
.option(
'--reply-ts <replyTs>',
'Reply to the message with the corresponding ts'
)
.option(
'--filename <filename>',
'If provided, the filename that will be used for the file on Slack.'
)
.action(
async (
token,
text,
filePath,
channels,
{ dontFail, replyTs, filename }
) => {
Logger.startTask(
`Attempting to send message to Slack for channels: ${ channels.join(
','
@ -51,13 +62,16 @@ export const slackFileCommand = new Command( 'file' )
for ( const channel of channels ) {
try {
await client.files.uploadV2( {
const requestOptions = {
file: filePath,
filename: basename( filePath ),
filename: filename ? filename : basename( filePath ),
channel_id: channel,
initial_comment: text.replace( /\\n/g, '\n' ),
request_file_info: false,
} );
thread_ts: replyTs ? replyTs : null,
};
await client.files.uploadV2( requestOptions );
Logger.notice(
`Successfully uploaded ${ filePath } to channel: ${ channel }`
@ -80,4 +94,5 @@ export const slackFileCommand = new Command( 'file' )
}
Logger.endTask();
} );
}
);

View File

@ -2,16 +2,20 @@
* External dependencies
*/
import { Command } from '@commander-js/extra-typings';
import { setOutput } from '@actions/core';
/**
* Internal dependencies
*/
import { Logger } from '../../../core/logger';
import { requestAsync } from '../../../core/util';
import { isGithubCI } from '../../../core/environment';
type SlackResponse = {
ok: boolean;
error?: string;
channel?: string;
ts?: string;
};
export const slackMessageCommand = new Command( 'message' )
@ -72,6 +76,9 @@ export const slackMessageCommand = new Command( 'message' )
Logger.notice(
`Slack message sent successfully to channel: ${ channel }`
);
if ( isGithubCI() ) {
setOutput( 'ts', response.ts );
}
}
} catch ( e: unknown ) {
Logger.error( e, shouldFail );