Code Freeze CLI: Add branch command (#37914)

This commit is contained in:
Paul Sealock 2023-04-26 10:53:19 +12:00 committed by GitHub
parent 6f7eeeaf49
commit 34bd5e1bf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 274 additions and 13 deletions

View File

@ -3296,6 +3296,9 @@ importers:
ora:
specifier: ^5.4.1
version: 5.4.1
promptly:
specifier: ^3.2.0
version: 3.2.0
semver:
specifier: ^7.3.2
version: 7.3.5
@ -8733,9 +8736,9 @@ packages:
'@babel/core': 7.21.3
'@babel/helper-annotate-as-pure': 7.16.7
'@babel/helper-module-imports': 7.16.7
'@babel/helper-plugin-utils': 7.18.9
'@babel/helper-plugin-utils': 7.20.2
'@babel/plugin-syntax-jsx': 7.16.7(@babel/core@7.21.3)
'@babel/types': 7.17.0
'@babel/types': 7.21.3
dev: true
/@babel/plugin-transform-react-jsx@7.19.0(@babel/core@7.12.9):
@ -10178,7 +10181,7 @@ packages:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.17.8
'@babel/helper-plugin-utils': 7.20.2
'@babel/helper-plugin-utils': 7.19.0
'@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.17.8)
'@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.17.8)
'@babel/types': 7.21.3
@ -10191,7 +10194,7 @@ packages:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.3
'@babel/helper-plugin-utils': 7.20.2
'@babel/helper-plugin-utils': 7.19.0
'@babel/plugin-proposal-unicode-property-regex': 7.18.6(@babel/core@7.21.3)
'@babel/plugin-transform-dotall-regex': 7.18.6(@babel/core@7.21.3)
'@babel/types': 7.21.3
@ -20863,8 +20866,8 @@ packages:
peerDependencies:
postcss: ^8.1.0
dependencies:
browserslist: 4.20.2
caniuse-lite: 1.0.30001352
browserslist: 4.21.4
caniuse-lite: 1.0.30001418
fraction.js: 4.2.0
normalize-range: 0.1.2
picocolors: 1.0.0
@ -22406,6 +22409,7 @@ packages:
escalade: 3.1.1
node-releases: 2.0.6
picocolors: 1.0.0
dev: true
/browserslist@4.20.4:
resolution: {integrity: sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==}
@ -27384,12 +27388,12 @@ packages:
vue-template-compiler:
optional: true
dependencies:
'@babel/code-frame': 7.18.6
'@babel/code-frame': 7.16.7
'@types/json-schema': 7.0.9
chalk: 4.1.2
chokidar: 3.5.3
cosmiconfig: 6.0.0
deepmerge: 4.3.0
deepmerge: 4.2.2
eslint: 8.32.0
fs-extra: 9.1.0
glob: 7.2.3

View File

@ -18,6 +18,7 @@
"graphql": "^16.6.0",
"octokit": "^2.0.14",
"ora": "^5.4.1",
"promptly": "^3.2.0",
"semver": "^7.3.2"
},
"devDependencies": {

View File

@ -0,0 +1,152 @@
/**
* External dependencies
*/
import { Command } from '@commander-js/extra-typings';
import { parse } from 'semver';
import { confirm } from 'promptly';
import chalk from 'chalk';
import ora from 'ora';
import { setOutput } from '@actions/core';
/**
* Internal dependencies
*/
import {
getLatestReleaseVersion,
doesGithubBranchExist,
getRefFromGithubBranch,
createGithubBranch,
deleteGithubBranch,
} from '../../../github/repo';
import { WPIncrement } from '../milestone/utils';
import { Options } from './types';
const getNextReleaseBranch = async ( options: Options ) => {
const latestReleaseVersion = await getLatestReleaseVersion( options );
const nextReleaseVersion = WPIncrement( latestReleaseVersion );
const parsedNextReleaseVersion = parse( nextReleaseVersion );
const nextReleaseMajorMinor = `${ parsedNextReleaseVersion.major }.${ parsedNextReleaseVersion.minor }`;
return `release/${ nextReleaseMajorMinor }`;
};
export const branchCommand = new Command( 'branch' )
.description( 'Create a new release branch' )
.option(
'-g --github',
'CLI command is used in the Github Actions context.'
)
.option( '-d --dryRun', 'Prepare the branch but do not create it.' )
.option(
'-o --owner <owner>',
'Repository owner. Default: woocommerce',
'woocommerce'
)
.option(
'-n --name <name>',
'Repository name. Default: woocommerce',
'woocommerce'
)
.option(
'-b --branch <branch>',
'Release branch to create. The branch will be determined from Github if none is supplied'
)
.option(
'-s --source <source>',
'Branch to create the release branch from. Default: trunk',
'trunk'
)
.action( async ( options: Options ) => {
const { github, source, branch, owner, name, dryRun } = options;
let nextReleaseBranch;
if ( ! branch ) {
const versionSpinner = ora(
chalk.yellow(
'No branch supplied, going off the latest release version'
)
).start();
nextReleaseBranch = await getNextReleaseBranch( options );
console.log(
chalk.yellow(
`The next release branch is ${ nextReleaseBranch }`
)
);
versionSpinner.succeed();
} else {
nextReleaseBranch = branch;
}
const branchSpinner = ora(
chalk.yellow(
`Check to see if branch ${ nextReleaseBranch } exists on ${ owner }/${ name }`
)
).start();
const branchExists = await doesGithubBranchExist(
options,
nextReleaseBranch
);
branchSpinner.succeed();
if ( branchExists ) {
if ( github ) {
console.log(
chalk.red(
`Release branch ${ nextReleaseBranch } already exists`
)
);
// When in Github Actions, we don't want to prompt the user for input.
process.exit( 0 );
}
const deleteExistingReleaseBranch = await confirm(
chalk.yellow(
`Release branch ${ nextReleaseBranch } already exists on ${ owner }/${ name }, do you want to delete it and create a new one from ${ source }? [y/n]`
)
);
if ( deleteExistingReleaseBranch ) {
const deleteBranchSpinner = ora(
chalk.yellow(
`Delete branch ${ nextReleaseBranch } on ${ owner }/${ name } and create new one from ${ source }`
)
).start();
await deleteGithubBranch( options, nextReleaseBranch );
deleteBranchSpinner.succeed();
} else {
console.log(
chalk.green(
`Branch ${ nextReleaseBranch } already exist on ${ owner }/${ name }, no action taken.`
)
);
process.exit( 0 );
}
}
const createBranchSpinner = ora(
chalk.yellow( `Create branch ${ nextReleaseBranch }` )
).start();
if ( dryRun ) {
createBranchSpinner.succeed();
console.log(
chalk.green(
`DRY RUN: Skipping actual creation of branch ${ nextReleaseBranch } on ${ owner }/${ name }`
)
);
process.exit( 0 );
}
const ref = await getRefFromGithubBranch( options, source );
await createGithubBranch( options, nextReleaseBranch, ref );
createBranchSpinner.succeed();
if ( github ) {
setOutput( 'nextReleaseBranch', nextReleaseBranch );
}
console.log(
chalk.green(
`Branch ${ nextReleaseBranch } successfully created on ${ owner }/${ name }`
)
);
} );

View File

@ -0,0 +1,8 @@
export type Options = {
github?: boolean;
dryRun?: boolean;
owner?: string;
name?: string;
branch?: string;
source?: string;
};

View File

@ -7,11 +7,13 @@ import { Command } from '@commander-js/extra-typings';
* Internal dependencies
*/
import { verifyDayCommand } from './verify-day';
import { milesStoneCommand } from './milestone';
import { milestoneCommand } from './milestone';
import { branchCommand } from './branch';
const program = new Command( 'code-freeze' )
.description( 'Code freeze utilities' )
.addCommand( verifyDayCommand )
.addCommand( milesStoneCommand );
.addCommand( milestoneCommand )
.addCommand( branchCommand );
export default program;

View File

@ -10,10 +10,10 @@ import ora from 'ora';
*/
import { getLatestReleaseVersion } from '../../../github/repo';
import { octokitWithAuth } from '../../../github/api';
import { WPIncrement, setGithubMilestoneOutputs } from './utils';
import { setGithubMilestoneOutputs, WPIncrement } from './utils';
import { Options } from './types';
export const milesStoneCommand = new Command( 'milestone' )
export const milestoneCommand = new Command( 'milestone' )
.description( 'Create a milestone' )
.option(
'-g --github',

View File

@ -6,7 +6,7 @@ import { Repository } from '@octokit/graphql-schema';
/**
* Internal dependencies
*/
import { graphqlWithAuth } from './api';
import { graphqlWithAuth, octokitWithAuth } from './api';
export const getLatestReleaseVersion = async ( options: {
owner?: string;
@ -34,3 +34,97 @@ export const getLatestReleaseVersion = async ( options: {
( tagName ) => tagName.isLatest
).tagName;
};
export const doesGithubBranchExist = async (
options: {
owner?: string;
name?: string;
},
nextReleaseBranch: string
): Promise< boolean > => {
const { owner, name } = options;
try {
const branchOnGithub = await octokitWithAuth.request(
'GET /repos/{owner}/{repo}/branches/{branch}',
{
owner,
repo: name,
branch: nextReleaseBranch,
}
);
return branchOnGithub.data.name === nextReleaseBranch;
} catch ( e ) {
if (
e.status === 404 &&
e.response.data.message === 'Branch not found'
) {
return false;
}
throw new Error( e );
}
};
export const getRefFromGithubBranch = async (
options: {
owner?: string;
name?: string;
},
source: string
): Promise< string > => {
const { owner, name } = options;
const { repository } = await graphqlWithAuth< {
repository: Repository;
} >( `
{
repository(owner:"${ owner }", name:"${ name }") {
ref(qualifiedName: "refs/heads/${ source }") {
target {
... on Commit {
history(first: 1) {
edges{ node{ oid } }
}
}
}
}
}
}
` );
// @ts-ignore: The graphql query is typed, but the response is not.
return repository.ref.target.history.edges.shift().node.oid;
};
export const createGithubBranch = async (
options: {
owner?: string;
name?: string;
},
branch: string,
ref: string
): Promise< void > => {
const { owner, name } = options;
await octokitWithAuth.request( 'POST /repos/{owner}/{repo}/git/refs', {
owner,
repo: name,
ref: `refs/heads/${ branch }`,
sha: ref,
} );
};
export const deleteGithubBranch = async (
options: {
owner?: string;
name?: string;
},
branch: string
): Promise< void > => {
const { owner, name } = options;
await octokitWithAuth.request(
'DELETE /repos/{owner}/{repo}/git/refs/heads/{ref}',
{
owner,
repo: name,
ref: branch,
}
);
};