Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add automation script #51

Merged
merged 1 commit into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ To create a new release:

- Create a release branch named `release/vx.y.z`.

- Submit a release PR targeting the `master` branch:
- Submit a release PR:
- Bump the version in `package.json.js`.
- Run `npm run install-project` to update the `package.json` and `package-lock.json` files.
- The commit message should be of the form "Release vx.y.z"
- Create a new PR targeting the `master` branch.
- The commit message should be of the form "Release vx.y.z".
- The title of the release PR should be of the form "Release vx.y.z"

- Once the PR is merged, create a new release:
- Once the PR is merged, create a new GitHub release:
- Go the GitHub repo, and navigate to ["Releases"](https://github.com/fortanix/baklava/releases).
- Click ["Draft a new release"](https://github.com/fortanix/baklava/releases/new).
- Under "Choose a new tag", create a new tag of the form `vx.y.z`.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"repl": "tsx",
"plop": "NODE_OPTIONS=\"--import tsx\" plop",
"import": "tsx scripts/import.ts",
"automate": "tsx scripts/automate.ts",
"serve:dev": "vite --config=./vite.config.ts serve",
"build": "vite --config=./vite.config.ts --emptyOutDir build",
"storybook:serve": "storybook dev -p 6006",
Expand Down
1 change: 1 addition & 0 deletions package.json.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const packageConfig = {
'repl': 'tsx',
'plop': 'NODE_OPTIONS="--import tsx" plop',
'import': 'tsx scripts/import.ts',
'automate': 'tsx scripts/automate.ts',

// Library
//'lib:build': '',
Expand Down
167 changes: 167 additions & 0 deletions scripts/automate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@

import { dedent } from 'ts-dedent';
import { parseArgs } from 'node:util';
import { fileURLToPath } from 'node:url';
import * as fs from 'node:fs/promises';
import { exec } from 'node:child_process';
import { AsyncLocalStorage } from 'node:async_hooks';


//
// Setup
//

type Logger = Pick<Console, 'info' | 'error' | 'log'>;
type Services = { logger: Logger };
const servicesStorage = new AsyncLocalStorage<Services>();
const getServices = () => {
const services = servicesStorage.getStore();
if (typeof services === 'undefined') { throw new Error(`Missing services`); }
return services;
};

type ScriptArgs = {
values: {
help?: undefined | boolean,
silent?: undefined | boolean,
},
positionals: Array<string>,
};


//
// Common
//

const getCurrentGitBranch = () => new Promise<string>((resolve, reject) => {
return exec('git rev-parse --abbrev-ref HEAD', (err, stdout, stderr) => {
if (err) {
reject(`Failed to determine current git branch: ${err}`);
} else if (typeof stdout === 'string') {
resolve(stdout.trim());
}
});
});

const readPackageJson = async () => {
const packageJsonPath = fileURLToPath(new URL('../package.json', import.meta.url));
const packageJsonContent = (await fs.readFile(packageJsonPath)).toString();
return JSON.parse(packageJsonContent); // May throw
};


//
// Commands
//

// Utility automation to prepopulate a URL with the parameters for a new release on GitHub.
export const runGithubCreateReleasePullRequest = async (args: ScriptArgs) => {
const { logger } = getServices();

const branchName = await getCurrentGitBranch();

const packageJson = await readPackageJson();
const version = packageJson.version;

// https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/using-query-parameters-to-create-a-pull-request
const prUrl = new URL(`https://github.com/fortanix/baklava/compare/master...${branchName}`);
prUrl.search = new URLSearchParams({
quick_pull: '1',
title: `Release v${version}`,
labels: `new-release`,
body: `Update the version to v${version}.`,
}).toString();

logger.log('\n');
logger.log(`Open the following URL in your browser to create the pull request:`);
logger.log(prUrl.toString());
logger.log('\n');
};

// Utility automation to prepopulate a URL with the parameters for a new release on GitHub.
export const runGithubCreateRelease = async (args: ScriptArgs) => {
const { logger } = getServices();

const packageJson = await readPackageJson();
const version = packageJson.version;
const isPrerelease = ['alpha', 'beta', 'rc', 'pre'].some(qualifier => version.includes(`-${qualifier}`));

// https://docs.github.com/en/repositories/releasing-projects-on-github/automation-for-release-forms-with-query-parameters
const releaseUrl = new URL('https://github.com/fortanix/baklava/releases/new');
releaseUrl.search = new URLSearchParams({
tag: `v${version}`,
prerelease: isPrerelease ? '1' : '0',
title: `Release v${version}`,
}).toString();

logger.log('\n');
logger.log(`Open the following URL in your browser, and add the release notes:`);
logger.log(releaseUrl.toString());
logger.log('\n');
};


//
// Run
//

const printUsage = () => {
const { logger } = getServices();

logger.info(dedent`
Usage: automate.ts <cmd> <...args>

Commands:
- github:create-release
`);
};

// Run the script with the given CLI arguments
export const run = async (argsRaw: Array<string>): Promise<void> => {
// Ref: https://exploringjs.com/nodejs-shell-scripting/ch_node-util-parseargs.html
const args = parseArgs({
args: argsRaw,
allowPositionals: true,
options: {
help: { type: 'boolean', short: 'h' },
silent: { type: 'boolean' },
},
});

// Services
const logger: Logger = {
info: args.values.silent ? () => {} : console.info,
error: console.error,
log: console.log,
};

await servicesStorage.run({ logger }, async () => {
const command: null | string = args.positionals[0] ?? null;
if (command === null || args.values.help) {
printUsage();
return;
}

const argsForCommand: ScriptArgs = { ...args, positionals: args.positionals.slice(1) };
switch (command) {
case 'github:create-release-pr': await runGithubCreateReleasePullRequest(argsForCommand); break;
case 'github:create-release': await runGithubCreateRelease(argsForCommand); break;
default:
logger.error(`Unknown command '${command}'\n`);
printUsage();
break;
}
});
};

// Detect if this module is being run directly from the command line
const [_argExec, argScript, ...args] = process.argv; // First two arguments should be the executable + script
if (argScript && await fs.realpath(argScript) === fileURLToPath(import.meta.url)) {
try {
await run(args);
process.exit(0);
} catch (error: unknown) {
console.error(error);
process.exit(1);
}
}
Loading