From ed18a24d6477ddd3de7a41d6a91d31fb7ac22451 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 4 Oct 2023 14:45:08 +0200 Subject: [PATCH] move ensure-next-ahead from a bash script to a tested node script --- .github/workflows/publish.yml | 19 +--- scripts/package.json | 1 + .../__tests__/ensure-next-ahead.test.ts | 85 +++++++++++++++ scripts/release/ensure-next-ahead.ts | 102 ++++++++++++++++++ 4 files changed, 189 insertions(+), 18 deletions(-) create mode 100644 scripts/release/__tests__/ensure-next-ahead.test.ts create mode 100644 scripts/release/ensure-next-ahead.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index acb787424846..be37b132b1fc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -165,31 +165,14 @@ jobs: git merge ${{ github.ref_name }} git push origin ${{ steps.target.outputs.target }} - # This step ensures that next is always one minor ahead of main - # this is needed when releasing a stable from next - # next will be at eg. 7.4.0-alpha.4, and main will be at 7.3.0 - # then we release 7.4.0 by merging next to latest-release to main - # we then ensure here that next is bumped to 7.5.0 - without releasing it - # if this is a patch release bumping main to 7.3.1, next will not be touched because it's already ahead - name: Ensure `next` is a minor version ahead of `main` if: steps.target.outputs.target == 'main' run: | git checkout next git pull - CODE_PKG_JSON=$(cat ../code/package.json) - VERSION_ON_NEXT=$(echo $CODE_PKG_JSON | jq --raw-output '.version') - VERSION_ON_MAIN="${{ steps.version.outputs.current-version }}" + yarn release:ensure-next-ahead --main-version "${{ steps.version.outputs.current-version }}" - # skip if next is already ahead of main - if NEXT_IS_AHEAD=$(npx semver --include-prerelease --range ">=$VERSION_ON_MAIN" "$VERSION_ON_NEXT" 2>/dev/null); then - return - fi - - # temporarily set the version on next to be the same as main... - echo "$CODE_PKG_JSON" | jq --arg version "$VERSION_ON_MAIN" '.version = $version' > ../code/package.json - # ... then bump it by one minor - yarn release:version --release-type minor git add .. git commit -m "Bump next to be one minor ahead of main [skip ci]" git push origin next diff --git a/scripts/package.json b/scripts/package.json index 906bb2b95926..4cd557a0736e 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -13,6 +13,7 @@ "lint:js:cmd": "cross-env NODE_ENV=production eslint --cache --cache-location=../.cache/eslint --ext .js,.jsx,.json,.html,.ts,.tsx,.mjs --report-unused-disable-directives", "lint:package": "sort-package-json", "migrate-docs": "node --require esbuild-register ./ts-to-ts49.ts", + "release:ensure-next-ahead": "ts-node --swc ./release/ensure-next-ahead.ts", "release:generate-pr-description": "ts-node --swc ./release/generate-pr-description.ts", "release:get-changelog-from-file": "ts-node --swc ./release/get-changelog-from-file.ts", "release:get-current-version": "ts-node --swc ./release/get-current-version.ts", diff --git a/scripts/release/__tests__/ensure-next-ahead.test.ts b/scripts/release/__tests__/ensure-next-ahead.test.ts new file mode 100644 index 000000000000..0b192d39b106 --- /dev/null +++ b/scripts/release/__tests__/ensure-next-ahead.test.ts @@ -0,0 +1,85 @@ +/* eslint-disable global-require */ +/* eslint-disable no-underscore-dangle */ +import path from 'path'; +import { run as ensureNextAhead } from '../ensure-next-ahead'; +import * as gitClient_ from '../utils/git-client'; +import * as bumpVersion_ from '../version'; + +jest.mock('../utils/git-client', () => jest.requireActual('jest-mock-extended').mockDeep()); +const gitClient = jest.mocked(gitClient_); + +// eslint-disable-next-line jest/no-mocks-import +jest.mock('fs-extra', () => require('../../../code/__mocks__/fs-extra')); +const fsExtra = require('fs-extra'); + +jest.mock('../version', () => jest.requireActual('jest-mock-extended').mockDeep()); +const bumpVersion = jest.mocked(bumpVersion_); + +jest.spyOn(console, 'log').mockImplementation(() => {}); +jest.spyOn(console, 'warn').mockImplementation(() => {}); +jest.spyOn(console, 'error').mockImplementation(() => {}); + +const CODE_PACKAGE_JSON_PATH = path.join(__dirname, '..', '..', '..', 'code', 'package.json'); + +describe('Ensure next ahead', () => { + beforeEach(() => { + jest.clearAllMocks(); + gitClient.git.status.mockResolvedValue({ current: 'next' } as any); + fsExtra.__setMockFiles({ + [CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: '2.0.0' }), + }); + }); + + it('should throw when main-version is missing', async () => { + await expect(ensureNextAhead({})).rejects.toThrowErrorMatchingInlineSnapshot(` + "[ + { + "code": "invalid_type", + "expected": "string", + "received": "undefined", + "path": [ + "mainVersion" + ], + "message": "Required" + } + ]" + `); + }); + + it('should throw when main-version is not a semver string', async () => { + await expect(ensureNextAhead({ mainVersion: '200' })).rejects + .toThrowErrorMatchingInlineSnapshot(` + "[ + { + "code": "custom", + "message": "main-version must be a valid semver version string like '7.4.2'.", + "path": [] + } + ]" + `); + }); + + it('should not bump version when next is already ahead of main', async () => { + await expect(ensureNextAhead({ mainVersion: '1.0.0' })).resolves.toBeUndefined(); + expect(bumpVersion.run).not.toHaveBeenCalled(); + }); + + it('should bump version to 3.1.0-alpha.0 when main is 3.0.0 and next is 2.0.0', async () => { + await expect(ensureNextAhead({ mainVersion: '3.0.0' })).resolves.toBeUndefined(); + expect(bumpVersion.run).toHaveBeenCalledWith({ exact: '3.1.0-alpha.0' }); + }); + + it('should bump version to 2.1.0-alpha.0 when main and next are both 2.0.0', async () => { + await expect(ensureNextAhead({ mainVersion: '2.0.0' })).resolves.toBeUndefined(); + expect(bumpVersion.run).toHaveBeenCalledWith({ exact: '2.1.0-alpha.0' }); + }); + + it('should bump version to 2.1.0-alpha.0 when main is 2.0.0 and and next is 2.0.0-rc.10', async () => { + fsExtra.__setMockFiles({ + [CODE_PACKAGE_JSON_PATH]: JSON.stringify({ version: '2.0.0-rc.10' }), + }); + + await expect(ensureNextAhead({ mainVersion: '2.0.0' })).resolves.toBeUndefined(); + expect(bumpVersion.run).toHaveBeenCalledWith({ exact: '2.1.0-alpha.0' }); + }); +}); diff --git a/scripts/release/ensure-next-ahead.ts b/scripts/release/ensure-next-ahead.ts new file mode 100644 index 000000000000..280f5fd00fa9 --- /dev/null +++ b/scripts/release/ensure-next-ahead.ts @@ -0,0 +1,102 @@ +/** + * This script ensures that next is always one minor ahead of main. + * This is needed when releasing a stable from next. + * Next will be at eg. 7.4.0-alpha.4, and main will be at 7.3.0. + * Then we release 7.4.0 by merging next to latest-release to main. + * We then ensure here that next is bumped to 7.5.0-alpha.0 - without releasing it. + * If this is a patch release bumping main to 7.3.1, next will not be touched because it's already ahead. + */ + +/* eslint-disable no-console */ +import chalk from 'chalk'; +import path from 'path'; +import program from 'commander'; +import semver from 'semver'; +import { z } from 'zod'; +import { readJson } from 'fs-extra'; +import dedent from 'ts-dedent'; +import { run as bumpVersion } from './version'; +import { git } from './utils/git-client'; + +program + .name('ensure-next-ahead') + .description('ensure the "next" branch is always a minor version ahead of "main"') + .requiredOption('-M, --main-version ', 'The version currently on the "main" branch'); + +const optionsSchema = z + .object({ + mainVersion: z.string(), + }) + .refine((schema) => semver.valid(schema.mainVersion), { + message: "main-version must be a valid semver version string like '7.4.2'.", + }); + +type Options = { + mainVersion: string; +}; + +const CODE_DIR_PATH = path.join(__dirname, '..', '..', 'code'); +const CODE_PACKAGE_JSON_PATH = path.join(CODE_DIR_PATH, 'package.json'); + +const validateOptions = (options: { [key: string]: any }): options is Options => { + optionsSchema.parse(options); + return true; +}; + +const getCurrentVersion = async () => { + const { version } = await readJson(CODE_PACKAGE_JSON_PATH); + console.log(`📐 Current version of Storybook is ${chalk.green(version)}`); + return version; +}; + +export const run = async (options: unknown) => { + if (!validateOptions(options)) { + return; + } + const { mainVersion } = options; + + const { current: currentGitBranch } = await git.status(); + + if (currentGitBranch !== 'next') { + console.warn( + `🚧 The current branch is not "next" but "${currentGitBranch}", this only really makes sense to run on the "next" branch.` + ); + } + + // Get the current version from code/package.json + const currentNextVersion = await getCurrentVersion(); + if (semver.gt(currentNextVersion, mainVersion)) { + console.log( + `✅ The version on next (${chalk.green( + currentNextVersion + )}) is already ahead of the version on main (${chalk.green(mainVersion)}), no action needed.` + ); + return; + } + + const nextNextVersion = `${semver.inc(mainVersion, 'minor')}-alpha.0`; + + console.log( + `🤜 The version on next (${chalk.green( + currentNextVersion + )}) is behind the version on main (${chalk.green(mainVersion)}), bumping to ${chalk.blue( + nextNextVersion + )}...` + ); + + await bumpVersion({ exact: nextNextVersion }); + + console.log( + `✅ bumped all versions to ${chalk.green( + nextNextVersion + )}, remember to commit and push to next.` + ); +}; + +if (require.main === module) { + const parsed = program.parse(); + run(parsed.opts()).catch((err) => { + console.error(err); + process.exit(1); + }); +}