From d422085cb989d511523bcdb6baa1ce6cb6891451 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 22 Jul 2022 17:26:36 -0600 Subject: [PATCH 1/6] Reformat root version to include the release no. This commit changes how the root version is bumped per release so that instead of looking like `..
`, it looks like `
..0`. That is, the root version consists of the date that the release was created + a release number, which is incremented every time a new release is issued. --- package.json | 1 + src/functional.test.ts | 20 +++--- src/initial-parameters.test.ts | 80 ++++++++++++++++++++++-- src/initial-parameters.ts | 5 +- src/monorepo-workflow-operations.test.ts | 67 +++++++++++++------- src/monorepo-workflow-operations.ts | 8 +-- src/project.test.ts | 69 +++++++++++++++----- src/project.ts | 76 ++++++++++++++++++++++ src/release-plan.test.ts | 50 +++++++++++---- src/release-plan.ts | 26 ++++---- src/repo.test.ts | 12 ++-- src/repo.ts | 32 +++++++--- tests/unit/helpers.ts | 4 ++ yarn.lock | 8 +++ 14 files changed, 361 insertions(+), 97 deletions(-) diff --git a/package.json b/package.json index 0618cf19..ead8e5c6 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dependencies": { "@metamask/action-utils": "^0.0.2", "@metamask/utils": "^2.1.0", + "date-fns": "^2.29.1", "debug": "^4.3.4", "execa": "^5.0.0", "glob": "^8.0.3", diff --git a/src/functional.test.ts b/src/functional.test.ts index 9c9dc4b9..9a4f97ce 100644 --- a/src/functional.test.ts +++ b/src/functional.test.ts @@ -9,7 +9,7 @@ describe('create-release-branch (functional)', () => { packages: { $root$: { name: '@scope/monorepo', - version: '2022.1.1', + version: '20220101.1.0', directoryPath: '.', }, a: { @@ -41,7 +41,7 @@ describe('create-release-branch (functional)', () => { workspaces: { '.': ['packages/*'], }, - today: new Date('2022-06-24'), + today: new Date(2022, 5, 24), }, async (environment) => { await environment.updateJsonFile('package.json', { @@ -88,7 +88,7 @@ describe('create-release-branch (functional)', () => { expect(await environment.readJsonFile('package.json')).toStrictEqual({ name: '@scope/monorepo', - version: '2022.6.24', + version: '20220624.2.0', private: true, workspaces: ['packages/*'], scripts: { foo: 'bar' }, @@ -138,7 +138,7 @@ describe('create-release-branch (functional)', () => { packages: { $root$: { name: '@scope/monorepo', - version: '2022.1.1', + version: '20220101.1.0', directoryPath: '.', }, a: { @@ -238,7 +238,7 @@ describe('create-release-branch (functional)', () => { packages: { $root$: { name: '@scope/monorepo', - version: '2022.1.1', + version: '20220101.1.0', directoryPath: '.', }, a: { @@ -250,7 +250,7 @@ describe('create-release-branch (functional)', () => { workspaces: { '.': ['packages/*'], }, - today: new Date('2022-06-24'), + today: new Date(2022, 5, 24), }, async (environment) => { await environment.runTool({ @@ -262,9 +262,9 @@ describe('create-release-branch (functional)', () => { }); // Tests four things: - // * The latest commit should be called "Release YYYY-MM-DD" + // * The latest commit should be called "Release YYYY-MM-DD (RN)" // * The latest commit should be the current commit (HEAD) - // * The latest branch should be called "release/YYYY-MM-DD" + // * The latest branch should be called "release/YYYY-MM-DD/N" // * The latest branch should point to the latest commit const [latestCommitSubject, latestCommitId, latestCommitRevsMarker] = ( @@ -284,9 +284,9 @@ describe('create-release-branch (functional)', () => { '--max-count=1', ]) ).stdout; - expect(latestCommitSubject).toStrictEqual('Release 2022-06-24'); + expect(latestCommitSubject).toStrictEqual('Release 2022-06-24 (R2)'); expect(latestCommitRevs).toContain('HEAD'); - expect(latestCommitRevs).toContain('release/2022-06-24'); + expect(latestCommitRevs).toContain('release/2022-06-24/2'); expect(latestBranchCommitId).toStrictEqual(latestCommitId); }, ); diff --git a/src/initial-parameters.test.ts b/src/initial-parameters.test.ts index 894b7ed1..f5b425ea 100644 --- a/src/initial-parameters.test.ts +++ b/src/initial-parameters.test.ts @@ -46,7 +46,7 @@ describe('initial-parameters', () => { project, tempDirectoryPath: '/path/to/temp', reset: true, - today: new Date('2022-06-22'), + today: new Date(2022, 5, 22), }); }); @@ -125,9 +125,81 @@ describe('initial-parameters', () => { ); }); - it('uses the current date if the TODAY environment variable was not provided', async () => { + it('returns initial parameters including reset: false, derived from a command-line argument of "--reset true"', async () => { const project = buildMockProject(); - const today = new Date('2022-01-01'); + when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments')) + .calledWith(['arg1', 'arg2']) + .mockResolvedValue({ + projectDirectory: '/path/to/project', + tempDirectory: '/path/to/temp', + reset: true, + }); + jest + .spyOn(envModule, 'getEnvironmentVariables') + .mockReturnValue({ TODAY: undefined, EDITOR: undefined }); + when(jest.spyOn(projectModule, 'readProject')) + .calledWith('/path/to/project') + .mockResolvedValue(project); + + const config = await determineInitialParameters( + ['arg1', 'arg2'], + '/path/to/somewhere', + ); + + expect(config.reset).toBe(true); + }); + + it('returns initial parameters including reset: false, derived from a command-line argument of "--reset false"', async () => { + const project = buildMockProject(); + when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments')) + .calledWith(['arg1', 'arg2']) + .mockResolvedValue({ + projectDirectory: '/path/to/project', + tempDirectory: '/path/to/temp', + reset: false, + }); + jest + .spyOn(envModule, 'getEnvironmentVariables') + .mockReturnValue({ TODAY: undefined, EDITOR: undefined }); + when(jest.spyOn(projectModule, 'readProject')) + .calledWith('/path/to/project') + .mockResolvedValue(project); + + const config = await determineInitialParameters( + ['arg1', 'arg2'], + '/path/to/somewhere', + ); + + expect(config.reset).toBe(false); + }); + + it("returns initial parameters including today's date, derived from the TODAY environment variable", async () => { + const project = buildMockProject(); + when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments')) + .calledWith(['arg1', 'arg2']) + .mockResolvedValue({ + projectDirectory: '/path/to/project', + tempDirectory: '/path/to/temp', + reset: true, + }); + jest + .spyOn(envModule, 'getEnvironmentVariables') + .mockReturnValue({ TODAY: '2022-01-01', EDITOR: undefined }); + when(jest.spyOn(projectModule, 'readProject')) + .calledWith('/path/to/project') + .mockResolvedValue(project); + + const config = await determineInitialParameters( + ['arg1', 'arg2'], + '/path/to/somewhere', + ); + + expect(config.today).toStrictEqual(new Date(2022, 0, 1)); + }); + + it('uses the current date if TODAY is undefined', async () => { + const project = buildMockProject(); + const today = new Date(2022, 0, 1); when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments')) .calledWith(['arg1', 'arg2']) .mockResolvedValue({ @@ -153,7 +225,7 @@ describe('initial-parameters', () => { it('uses the current date if TODAY is not a parsable date', async () => { const project = buildMockProject(); - const today = new Date('2022-01-01'); + const today = new Date(2022, 0, 1); when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments')) .calledWith(['arg1', 'arg2']) .mockResolvedValue({ diff --git a/src/initial-parameters.ts b/src/initial-parameters.ts index c4b81a77..252a8390 100644 --- a/src/initial-parameters.ts +++ b/src/initial-parameters.ts @@ -1,7 +1,8 @@ import os from 'os'; import path from 'path'; -import { getEnvironmentVariables } from './env'; +import { parseISO as parseDateAsISO } from 'date-fns'; import { readCommandLineArguments } from './command-line-arguments'; +import { getEnvironmentVariables } from './env'; import { readProject, Project } from './project'; interface InitialParameters { @@ -37,7 +38,7 @@ export async function determineInitialParameters( ) : path.resolve(cwd, inputs.tempDirectory); const parsedTodayTimestamp = - TODAY === undefined ? NaN : new Date(TODAY).getTime(); + TODAY === undefined ? NaN : parseDateAsISO(TODAY).getTime(); const today = isNaN(parsedTodayTimestamp) ? new Date() : new Date(parsedTodayTimestamp); diff --git a/src/monorepo-workflow-operations.test.ts b/src/monorepo-workflow-operations.test.ts index 5e9caaa8..1d19f866 100644 --- a/src/monorepo-workflow-operations.test.ts +++ b/src/monorepo-workflow-operations.test.ts @@ -89,21 +89,19 @@ function buildMockReleaseSpecification({ * * @param overrides - The properties you want to override in the mock release * plan. - * @param overrides.releaseName - The name of the new release. For a polyrepo or - * a monorepo with fixed versions, this will be a version string with the shape - * `..`; for a monorepo with independent versions, this - * will be a version string with the shape `..-`. + * @param overrides.releaseDate - The date of the release. + * @param overrides.releaseNumber - The number of the release. * @param overrides.packages - Information about all of the packages in the * project. For a polyrepo, this consists of the self-same package; for a * monorepo it consists of the root package and any workspace packages. * @returns The mock release specification. */ function buildMockReleasePlan({ - releaseName = 'release-name', + releaseDate = new Date(), + releaseNumber = 1, packages = [], }: Partial = {}): ReleasePlan { - return { releaseName, packages }; + return { releaseDate, releaseNumber, packages }; } /** @@ -178,8 +176,9 @@ async function setupFollowMonorepoWorkflow({ const releaseSpecification = buildMockReleaseSpecification({ path: releaseSpecificationPath, }); - const releaseName = 'some-release-name'; - const releasePlan = buildMockReleasePlan({ releaseName }); + const releaseDate = new Date(2022, 0, 1); + const releaseNumber = 12345; + const releasePlan = buildMockReleasePlan({ releaseDate, releaseNumber }); const projectDirectoryPath = '/path/to/project'; const project = buildMockProject({ directoryPath: projectDirectoryPath }); const today = new Date(); @@ -221,12 +220,21 @@ async function setupFollowMonorepoWorkflow({ } if (errorUponExecutingReleasePlan) { - executeReleasePlanSpy.mockRejectedValue(errorUponExecutingReleasePlan); + when(executeReleasePlanSpy) + .calledWith(project, releasePlan, stderr) + .mockRejectedValue(errorUponExecutingReleasePlan); } else { - executeReleasePlanSpy.mockResolvedValue(); + when(executeReleasePlanSpy) + .calledWith(project, releasePlan, stderr) + .mockResolvedValue(undefined); } - captureChangesInReleaseBranchSpy.mockResolvedValue(); + when(captureChangesInReleaseBranchSpy) + .calledWith(projectDirectoryPath, { + releaseDate, + releaseNumber, + }) + .mockResolvedValue(); if (doesReleaseSpecFileExist) { await fs.promises.writeFile( @@ -246,7 +254,8 @@ async function setupFollowMonorepoWorkflow({ executeReleasePlanSpy, captureChangesInReleaseBranchSpy, releasePlan, - releaseName, + releaseDate, + releaseNumber, releaseSpecificationPath, }; } @@ -295,7 +304,8 @@ describe('monorepo-workflow-operations', () => { stderr, captureChangesInReleaseBranchSpy, projectDirectoryPath, - releaseName, + releaseDate, + releaseNumber, } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -313,7 +323,10 @@ describe('monorepo-workflow-operations', () => { expect(captureChangesInReleaseBranchSpy).toHaveBeenCalledWith( projectDirectoryPath, - releaseName, + { + releaseDate, + releaseNumber, + }, ); }); }); @@ -688,7 +701,8 @@ describe('monorepo-workflow-operations', () => { stderr, captureChangesInReleaseBranchSpy, projectDirectoryPath, - releaseName, + releaseDate, + releaseNumber, } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -705,7 +719,10 @@ describe('monorepo-workflow-operations', () => { expect(captureChangesInReleaseBranchSpy).toHaveBeenCalledWith( projectDirectoryPath, - releaseName, + { + releaseDate, + releaseNumber, + }, ); }); }); @@ -849,7 +866,8 @@ describe('monorepo-workflow-operations', () => { stderr, captureChangesInReleaseBranchSpy, projectDirectoryPath, - releaseName, + releaseDate, + releaseNumber, } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -867,7 +885,10 @@ describe('monorepo-workflow-operations', () => { expect(captureChangesInReleaseBranchSpy).toHaveBeenCalledWith( projectDirectoryPath, - releaseName, + { + releaseDate, + releaseNumber, + }, ); }); }); @@ -1247,7 +1268,8 @@ describe('monorepo-workflow-operations', () => { stderr, captureChangesInReleaseBranchSpy, projectDirectoryPath, - releaseName, + releaseDate, + releaseNumber, } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -1265,7 +1287,10 @@ describe('monorepo-workflow-operations', () => { expect(captureChangesInReleaseBranchSpy).toHaveBeenCalledWith( projectDirectoryPath, - releaseName, + { + releaseDate, + releaseNumber, + }, ); }); }); diff --git a/src/monorepo-workflow-operations.ts b/src/monorepo-workflow-operations.ts index ffc8e9dc..3be8713e 100644 --- a/src/monorepo-workflow-operations.ts +++ b/src/monorepo-workflow-operations.ts @@ -114,8 +114,8 @@ export async function followMonorepoWorkflow({ }); await executeReleasePlan(project, releasePlan, stderr); await removeFile(releaseSpecificationPath); - await captureChangesInReleaseBranch( - project.directoryPath, - releasePlan.releaseName, - ); + await captureChangesInReleaseBranch(project.directoryPath, { + releaseDate: releasePlan.releaseDate, + releaseNumber: releasePlan.releaseNumber, + }); } diff --git a/src/project.test.ts b/src/project.test.ts index 77673d70..c125a23f 100644 --- a/src/project.test.ts +++ b/src/project.test.ts @@ -12,11 +12,11 @@ jest.mock('./repo'); describe('project', () => { describe('readProject', () => { - it('collects information about a monorepo project', async () => { + it('collects information about the repository URL, release version, and packages in the project', async () => { await withSandbox(async (sandbox) => { const projectDirectoryPath = sandbox.directoryPath; const projectRepositoryUrl = 'https://github.com/some-org/some-repo'; - const rootPackage = buildMockPackage('root', { + const rootPackage = buildMockPackage('root', '20220722.1234.0', { directoryPath: projectDirectoryPath, validatedManifest: buildMockManifest({ workspaces: ['packages/a', 'packages/subpackages/*'], @@ -66,31 +66,68 @@ describe('project', () => { rootPackage, workspacePackages, isMonorepo: true, + releaseInfo: { + releaseDate: new Date(2022, 6, 22), + releaseNumber: 1234, + }, }); }); }); - it('collects information about a polyrepo project', async () => { + it('throws if the release date portion of the root version is not in "..0" format', async () => { await withSandbox(async (sandbox) => { const projectDirectoryPath = sandbox.directoryPath; - const projectRepositoryUrl = 'https://github.com/some-org/some-repo'; - const rootPackage = buildMockPackage('root', { - directoryPath: projectDirectoryPath, - }); - when(jest.spyOn(repoModule, 'getRepositoryHttpsUrl')) + const rootPackage = buildMockPackage('root', '1.2.3'); + when(jest.spyOn(packageModule, 'readPackage')) .calledWith(projectDirectoryPath) - .mockResolvedValue(projectRepositoryUrl); + .mockResolvedValue(rootPackage); + + await expect(readProject(projectDirectoryPath)).rejects.toThrow( + 'Could not extract release info from package "root" version "1.2.3": Must be in "..0" format.', + ); + }); + }); + + it('throws if the release date portion of the root version is technically a valid date but is offset based on what was given', async () => { + await withSandbox(async (sandbox) => { + const projectDirectoryPath = sandbox.directoryPath; + // This evaluates to 2021-11-30 + const rootPackage = buildMockPackage('root', '20220000.1.0'); when(jest.spyOn(packageModule, 'readPackage')) .calledWith(projectDirectoryPath) .mockResolvedValue(rootPackage); - expect(await readProject(projectDirectoryPath)).toStrictEqual({ - directoryPath: projectDirectoryPath, - repositoryUrl: projectRepositoryUrl, - rootPackage, - workspacePackages: {}, - isMonorepo: false, - }); + await expect(readProject(projectDirectoryPath)).rejects.toThrow( + 'Could not extract release info from package "root" version "20220000.1.0": "20220000" must be a valid date in "
" format.', + ); + }); + }); + + it('throws if the release date portion of the root version is not a valid date whatsoever', async () => { + await withSandbox(async (sandbox) => { + const projectDirectoryPath = sandbox.directoryPath; + const rootPackage = buildMockPackage('root', '99999999.1.0'); + when(jest.spyOn(packageModule, 'readPackage')) + .calledWith(projectDirectoryPath) + .mockResolvedValue(rootPackage); + + await expect(readProject(projectDirectoryPath)).rejects.toThrow( + 'Could not extract release info from package "root" version "99999999.1.0": "99999999" must be a valid date in "
" format.', + ); + }); + }); + + it('throws if the release version is 0', async () => { + await withSandbox(async (sandbox) => { + const projectDirectoryPath = sandbox.directoryPath; + const rootPackage = buildMockPackage('root', '20220101.0.0'); + when(jest.spyOn(packageModule, 'readPackage')) + .calledWith(projectDirectoryPath) + .mockResolvedValue(rootPackage); + + await expect(readProject(projectDirectoryPath)).rejects.toThrow( + 'Could not extract release info from package "root" version "20220101.0.0": Release version must be greater than 0.', + ); }); }); }); diff --git a/src/project.ts b/src/project.ts index 7c33c3ac..955edd46 100644 --- a/src/project.ts +++ b/src/project.ts @@ -21,6 +21,19 @@ export interface Project { rootPackage: Package; workspacePackages: Record; isMonorepo: boolean; + releaseInfo: ReleaseInfo; +} + +/** + * Information about the release of the root package of a monorepo extracted + * from its version string. + * + * @property releaseDate - The release date. + * @property releaseNumber - The release number (starting from 1). + */ +interface ReleaseInfo { + releaseDate: Date; + releaseNumber: number; } /** @@ -28,6 +41,64 @@ export interface Project { */ const promisifiedGlob = util.promisify(glob); +/** + * Reads a version string from a SemVer object and extracts the release date and + * release number from it. + * + * @param packageVersionString - The version string of the package. + * @param packageName - The name of the package. + * @returns An object containing the release date and release number from the + * version string. + * @throws If the version string is invalid in some way. + */ +function parseReleaseInfoFrom( + packageVersionString: string, + packageName: string, +): ReleaseInfo { + const match = packageVersionString.match( + /^(?(?\d{4})(?\d{2})(?\d{2}))\.(?\d+)\.0$/u, + ); + const errorMessagePrefix = `Could not extract release info from package "${packageName}" version "${packageVersionString}"`; + + if (match?.groups) { + const releaseYear = Number(match.groups.releaseYearString); + const releaseMonthNumber = Number(match.groups.releaseMonthString); + const releaseDay = Number(match.groups.releaseDayString); + const releaseDate = new Date( + releaseYear, + releaseMonthNumber - 1, + releaseDay, + 0, + 0, + 0, + ); + const releaseNumber = Number(match.groups.releaseNumberString); + + if ( + isNaN(releaseDate.getTime()) || + releaseDate.getFullYear() !== releaseYear || + releaseDate.getMonth() !== releaseMonthNumber - 1 || + releaseDate.getDate() !== releaseDay + ) { + throw new Error( + `${errorMessagePrefix}: "${match.groups.releaseDateString}" must be a valid date in "
" format.`, + ); + } + + if (releaseNumber === 0) { + throw new Error( + `${errorMessagePrefix}: Release version must be greater than 0.`, + ); + } + + return { releaseDate, releaseNumber }; + } + + throw new Error( + `${errorMessagePrefix}: Must be in "..0" format.`, + ); +} + /** * Collects information about a project. For a polyrepo, this information will * only cover the project's `package.json` file; for a monorepo, it will cover @@ -44,6 +115,10 @@ export async function readProject( ): Promise { const repositoryUrl = await getRepositoryHttpsUrl(projectDirectoryPath); const rootPackage = await readPackage(projectDirectoryPath); + const releaseInfo = parseReleaseInfoFrom( + rootPackage.validatedManifest.version.toString(), + rootPackage.validatedManifest.name, + ); const workspaceDirectories = ( await Promise.all( @@ -76,5 +151,6 @@ export async function readProject( rootPackage, workspacePackages, isMonorepo, + releaseInfo, }; } diff --git a/src/release-plan.test.ts b/src/release-plan.test.ts index 0fcda290..a4164e1e 100644 --- a/src/release-plan.test.ts +++ b/src/release-plan.test.ts @@ -11,7 +11,7 @@ describe('release-plan-utils', () => { describe('planRelease', () => { it('calculates final versions for all packages in the release spec', async () => { const project = buildMockProject({ - rootPackage: buildMockPackage('root', '2022.1.1'), + rootPackage: buildMockPackage('root', '20220721.1.0'), workspacePackages: { a: buildMockPackage('a', '1.0.0'), b: buildMockPackage('b', '1.0.0'), @@ -28,7 +28,7 @@ describe('release-plan-utils', () => { }, path: '/path/to/release/spec', }; - const today = new Date('2022-07-21'); + const today = new Date(2022, 7, 1); const releasePlan = await planRelease({ project, @@ -37,11 +37,12 @@ describe('release-plan-utils', () => { }); expect(releasePlan).toMatchObject({ - releaseName: '2022-07-21', + releaseDate: today, + releaseNumber: 2, packages: [ { package: project.rootPackage, - newVersion: '2022.7.21', + newVersion: '20220801.2.0', }, { package: project.workspacePackages.a, @@ -63,6 +64,33 @@ describe('release-plan-utils', () => { }); }); + it('merely bumps the build number in the root version if a release is being created on the same day as a previous release', async () => { + const project = buildMockProject({ + rootPackage: buildMockPackage('root', '20220101.1.0'), + workspacePackages: {}, + }); + const releaseSpecification = { + packages: {}, + path: '/path/to/release/spec', + }; + const today = new Date(2022, 0, 1); + + const releasePlan = await planRelease({ + project, + releaseSpecification, + today, + }); + + expect(releasePlan).toMatchObject({ + packages: [ + { + package: project.rootPackage, + newVersion: '20220101.2.0', + }, + ], + }); + }); + it('records that the changelog for the root package does not need to be updated, while those for the workspace packages do', async () => { const project = buildMockProject({ rootPackage: buildMockPackage('root'), @@ -75,14 +103,14 @@ describe('release-plan-utils', () => { }); const releaseSpecification = { packages: { - a: IncrementableVersionParts.major, - b: IncrementableVersionParts.major, - c: IncrementableVersionParts.patch, - d: new SemVer('1.2.3'), + a: new SemVer('2.0.0'), + b: new SemVer('2.0.0'), + c: new SemVer('2.0.0'), + d: new SemVer('2.0.0'), }, path: '/path/to/release/spec', }; - const today = new Date('2022-07-21'); + const today = new Date(); const releasePlan = await planRelease({ project, @@ -91,7 +119,6 @@ describe('release-plan-utils', () => { }); expect(releasePlan).toMatchObject({ - releaseName: '2022-07-21', packages: [ { package: project.rootPackage, @@ -122,7 +149,8 @@ describe('release-plan-utils', () => { it('runs updatePackage for each package in the release plan', async () => { const project = buildMockProject(); const releasePlan = { - releaseName: 'some-release-name', + releaseDate: new Date(), + releaseNumber: 1, packages: [ { package: buildMockPackage(), diff --git a/src/release-plan.ts b/src/release-plan.ts index bcac7bdb..d95cce4e 100644 --- a/src/release-plan.ts +++ b/src/release-plan.ts @@ -1,5 +1,6 @@ import type { WriteStream } from 'fs'; import { SemVer } from 'semver'; +import { formatISO as formatDateAsISO } from 'date-fns'; import { debug } from './misc-utils'; import { Package, updatePackage } from './package'; import { Project } from './project'; @@ -9,17 +10,16 @@ import { ReleaseSpecification } from './release-specification'; * Instructions for how to update the project in order to prepare it for a new * release. * - * @property releaseName - The name of the new release. For a polyrepo or a - * monorepo with fixed versions, this will be a version string with the shape - * `..`; for a monorepo with independent versions, this - * will be a version string with the shape `..-`. + * @property releaseDate - The date associated with the new release. + * @property releaseNumber - The number of the new release, as 1 + the number of + * the previous release. * @property packages - Information about all of the packages in the project. * For a polyrepo, this consists of the self-same package; for a monorepo it * consists of the root package and any workspace packages. */ export interface ReleasePlan { - releaseName: string; + releaseDate: Date; + releaseNumber: number; packages: PackageReleasePlan[]; } @@ -62,12 +62,11 @@ export async function planRelease({ releaseSpecification: ReleaseSpecification; today: Date; }): Promise { - const newReleaseName = today.toISOString().replace(/T.+$/u, ''); - const newRootVersion = [ - today.getUTCFullYear(), - today.getUTCMonth() + 1, - today.getUTCDate(), - ].join('.'); + const newReleaseDate = formatDateAsISO(today, { + representation: 'date', + }).replace(/\D+/gu, ''); + const newReleaseNumber = project.releaseInfo.releaseNumber + 1; + const newRootVersion = `${newReleaseDate}.${newReleaseNumber}.0`; const rootReleasePlan: PackageReleasePlan = { package: project.rootPackage, @@ -94,7 +93,8 @@ export async function planRelease({ }); return { - releaseName: newReleaseName, + releaseDate: today, + releaseNumber: newReleaseNumber, packages: [rootReleasePlan, ...workspaceReleasePlans], }; } diff --git a/src/repo.test.ts b/src/repo.test.ts index b406b2c0..072d7958 100644 --- a/src/repo.test.ts +++ b/src/repo.test.ts @@ -96,14 +96,14 @@ describe('git-utils', () => { 'getStdoutFromCommand', ); - await captureChangesInReleaseBranch( - '/path/to/project', - 'some-release-name', - ); + await captureChangesInReleaseBranch('/path/to/project', { + releaseDate: new Date(2022, 6, 22), + releaseNumber: 12345, + }); expect(getStdoutFromCommandSpy).toHaveBeenCalledWith( 'git', - ['checkout', '-b', 'release/some-release-name'], + ['checkout', '-b', 'release/2022-07-22/12345'], { cwd: '/path/to/project' }, ); expect(getStdoutFromCommandSpy).toHaveBeenCalledWith( @@ -113,7 +113,7 @@ describe('git-utils', () => { ); expect(getStdoutFromCommandSpy).toHaveBeenCalledWith( 'git', - ['commit', '-m', 'Release some-release-name'], + ['commit', '-m', 'Release 2022-07-22 (R12345)'], { cwd: '/path/to/project' }, ); }); diff --git a/src/repo.ts b/src/repo.ts index e9eb3c70..e6a56496 100644 --- a/src/repo.ts +++ b/src/repo.ts @@ -1,3 +1,4 @@ +import { formatISO as formatDateAsISO } from 'date-fns'; import { getStdoutFromCommand } from './misc-utils'; /** @@ -88,23 +89,34 @@ export async function getRepositoryHttpsUrl( * of the new release). * 3. Switches to that branch. * - * @param repositoryDirectoryPath - The path to the repository directory. - * @param releaseName - The name of the release, which will be used to name the - * commit and the branch. + * @param projectRepositoryPath - The path to the repository directory. + * @param args - The arguments. + * @param args.releaseDate - The release date. + * @param args.releaseNumber - The release number. */ export async function captureChangesInReleaseBranch( - repositoryDirectoryPath: string, - releaseName: string, + projectRepositoryPath: string, + { + releaseDate, + releaseNumber, + }: { + releaseDate: Date; + releaseNumber: number; + }, ) { - await getStdoutFromGitCommandWithin(repositoryDirectoryPath, [ + const releaseDateAsISO = formatDateAsISO(releaseDate, { + representation: 'date', + }); + + await getStdoutFromGitCommandWithin(projectRepositoryPath, [ 'checkout', '-b', - `release/${releaseName}`, + `release/${releaseDateAsISO}/${releaseNumber}`, ]); - await getStdoutFromGitCommandWithin(repositoryDirectoryPath, ['add', '-A']); - await getStdoutFromGitCommandWithin(repositoryDirectoryPath, [ + await getStdoutFromGitCommandWithin(projectRepositoryPath, ['add', '-A']); + await getStdoutFromGitCommandWithin(projectRepositoryPath, [ 'commit', '-m', - `Release ${releaseName}`, + `Release ${releaseDateAsISO} (R${releaseNumber})`, ]); } diff --git a/tests/unit/helpers.ts b/tests/unit/helpers.ts index 3b0adb60..17c58c8b 100644 --- a/tests/unit/helpers.ts +++ b/tests/unit/helpers.ts @@ -44,6 +44,10 @@ export function buildMockProject(overrides: Partial = {}): Project { rootPackage: buildMockPackage('root'), workspacePackages: {}, isMonorepo: false, + releaseInfo: { + releaseDate: new Date(), + releaseNumber: 1, + }, ...overrides, }; } diff --git a/yarn.lock b/yarn.lock index 02499a4c..d32d4018 100644 --- a/yarn.lock +++ b/yarn.lock @@ -881,6 +881,7 @@ __metadata: "@types/yargs": ^17.0.10 "@typescript-eslint/eslint-plugin": ^4.21.0 "@typescript-eslint/parser": ^4.21.0 + date-fns: ^2.29.1 debug: ^4.3.4 deepmerge: ^4.2.2 eslint: ^7.23.0 @@ -2161,6 +2162,13 @@ __metadata: languageName: node linkType: hard +"date-fns@npm:^2.29.1": + version: 2.29.1 + resolution: "date-fns@npm:2.29.1" + checksum: 9d07f77dffc1eb8c213391bde39f2963ffe7c0019d9edde14487882d627224f3a39b963e6e99d0cc58afff220a6a1a7e8864d2789958f4eaa77714de94d4d076 + languageName: node + linkType: hard + "debug@npm:4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" From a4f69eb6da28e0e34ae24d6156482e19cd232c37 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Fri, 26 Aug 2022 14:48:00 -0600 Subject: [PATCH 2/6] Use format: 'basic' instead of regex --- src/release-plan.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/release-plan.ts b/src/release-plan.ts index d95cce4e..b9401430 100644 --- a/src/release-plan.ts +++ b/src/release-plan.ts @@ -63,8 +63,9 @@ export async function planRelease({ today: Date; }): Promise { const newReleaseDate = formatDateAsISO(today, { + format: 'basic', representation: 'date', - }).replace(/\D+/gu, ''); + }); const newReleaseNumber = project.releaseInfo.releaseNumber + 1; const newRootVersion = `${newReleaseDate}.${newReleaseNumber}.0`; From a4e77f3626ddcf01f543c6bdcf7baacbf3e71faf Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Mon, 19 Sep 2022 17:07:40 -0600 Subject: [PATCH 3/6] Change format to ORDINARY.BACKPORT.0 --- package.json | 1 - src/editor.test.ts | 16 +- src/env.test.ts | 2 - src/env.ts | 3 +- src/functional.test.ts | 16 +- src/initial-parameters.test.ts | 89 +----- src/initial-parameters.ts | 11 +- src/main.test.ts | 5 - src/main.ts | 3 +- src/monorepo-workflow-operations.test.ts | 342 ++++++++--------------- src/monorepo-workflow-operations.ts | 7 +- src/project.test.ts | 65 +---- src/project.ts | 98 ++----- src/release-plan.test.ts | 41 +-- src/release-plan.ts | 41 ++- src/repo.test.ts | 7 +- src/repo.ts | 20 +- tests/unit/helpers.ts | 6 +- yarn.lock | 8 - 19 files changed, 212 insertions(+), 569 deletions(-) diff --git a/package.json b/package.json index ead8e5c6..0618cf19 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "dependencies": { "@metamask/action-utils": "^0.0.2", "@metamask/utils": "^2.1.0", - "date-fns": "^2.29.1", "debug": "^4.3.4", "execa": "^5.0.0", "glob": "^8.0.3", diff --git a/src/editor.test.ts b/src/editor.test.ts index db5fda32..728774f6 100644 --- a/src/editor.test.ts +++ b/src/editor.test.ts @@ -11,7 +11,7 @@ describe('editor', () => { it('returns information about the editor from EDITOR if it resolves to an executable', async () => { jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ EDITOR: 'editor', TODAY: undefined }); + .mockReturnValue({ EDITOR: 'editor' }); when(jest.spyOn(miscUtils, 'resolveExecutable')) .calledWith('editor') .mockResolvedValue('/path/to/resolved-editor'); @@ -25,7 +25,7 @@ describe('editor', () => { it('falls back to VSCode if it exists and if EDITOR does not point to an executable', async () => { jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ EDITOR: 'editor', TODAY: undefined }); + .mockReturnValue({ EDITOR: 'editor' }); when(jest.spyOn(miscUtils, 'resolveExecutable')) .calledWith('editor') .mockResolvedValue(null) @@ -41,7 +41,7 @@ describe('editor', () => { it('returns null if resolving EDITOR returns null and resolving VSCode returns null', async () => { jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ EDITOR: 'editor', TODAY: undefined }); + .mockReturnValue({ EDITOR: 'editor' }); when(jest.spyOn(miscUtils, 'resolveExecutable')) .calledWith('editor') .mockResolvedValue(null) @@ -54,7 +54,7 @@ describe('editor', () => { it('returns null if resolving EDITOR returns null and resolving VSCode throws', async () => { jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ EDITOR: 'editor', TODAY: undefined }); + .mockReturnValue({ EDITOR: 'editor' }); when(jest.spyOn(miscUtils, 'resolveExecutable')) .calledWith('editor') .mockResolvedValue(null) @@ -67,7 +67,7 @@ describe('editor', () => { it('returns null if resolving EDITOR throws and resolving VSCode returns null', async () => { jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ EDITOR: 'editor', TODAY: undefined }); + .mockReturnValue({ EDITOR: 'editor' }); when(jest.spyOn(miscUtils, 'resolveExecutable')) .calledWith('editor') .mockRejectedValue(new Error('some error')) @@ -80,7 +80,7 @@ describe('editor', () => { it('returns null if resolving EDITOR throws and resolving VSCode throws', async () => { jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ EDITOR: 'editor', TODAY: undefined }); + .mockReturnValue({ EDITOR: 'editor' }); when(jest.spyOn(miscUtils, 'resolveExecutable')) .calledWith('editor') .mockRejectedValue(new Error('some error')) @@ -93,7 +93,7 @@ describe('editor', () => { it('returns null if EDITOR is unset and resolving VSCode returns null', async () => { jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ EDITOR: undefined, TODAY: undefined }); + .mockReturnValue({ EDITOR: undefined }); when(jest.spyOn(miscUtils, 'resolveExecutable')) .calledWith('code') .mockResolvedValue(null); @@ -104,7 +104,7 @@ describe('editor', () => { it('returns null if EDITOR is unset and resolving VSCode throws', async () => { jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ EDITOR: undefined, TODAY: undefined }); + .mockReturnValue({ EDITOR: undefined }); when(jest.spyOn(miscUtils, 'resolveExecutable')) .calledWith('code') .mockRejectedValue(new Error('some error')); diff --git a/src/env.test.ts b/src/env.test.ts index c40dbc65..02f334a5 100644 --- a/src/env.test.ts +++ b/src/env.test.ts @@ -16,12 +16,10 @@ describe('env', () => { it('returns only the environment variables from process.env that we use in this tool', () => { process.env.EDITOR = 'editor'; - process.env.TODAY = 'today'; process.env.EXTRA = 'extra'; expect(getEnvironmentVariables()).toStrictEqual({ EDITOR: 'editor', - TODAY: 'today', }); }); }); diff --git a/src/env.ts b/src/env.ts index 22d80553..c5253488 100644 --- a/src/env.ts +++ b/src/env.ts @@ -1,6 +1,5 @@ interface Env { EDITOR: string | undefined; - TODAY: string | undefined; } /** @@ -10,7 +9,7 @@ interface Env { * this tool needs to access, whether their values are defined or not. */ export function getEnvironmentVariables(): Env { - return ['EDITOR', 'TODAY'].reduce((object, key) => { + return ['EDITOR'].reduce((object, key) => { return { ...object, [key]: process.env[key] }; }, {} as Env); } diff --git a/src/functional.test.ts b/src/functional.test.ts index 9a4f97ce..ade59e34 100644 --- a/src/functional.test.ts +++ b/src/functional.test.ts @@ -9,7 +9,7 @@ describe('create-release-branch (functional)', () => { packages: { $root$: { name: '@scope/monorepo', - version: '20220101.1.0', + version: '1.0.0', directoryPath: '.', }, a: { @@ -88,7 +88,7 @@ describe('create-release-branch (functional)', () => { expect(await environment.readJsonFile('package.json')).toStrictEqual({ name: '@scope/monorepo', - version: '20220624.2.0', + version: '2.0.0', private: true, workspaces: ['packages/*'], scripts: { foo: 'bar' }, @@ -138,7 +138,7 @@ describe('create-release-branch (functional)', () => { packages: { $root$: { name: '@scope/monorepo', - version: '20220101.1.0', + version: '1.0.0', directoryPath: '.', }, a: { @@ -238,7 +238,7 @@ describe('create-release-branch (functional)', () => { packages: { $root$: { name: '@scope/monorepo', - version: '20220101.1.0', + version: '1.0.0', directoryPath: '.', }, a: { @@ -262,9 +262,9 @@ describe('create-release-branch (functional)', () => { }); // Tests four things: - // * The latest commit should be called "Release YYYY-MM-DD (RN)" + // * The latest commit should be called "Release 1.0.0" // * The latest commit should be the current commit (HEAD) - // * The latest branch should be called "release/YYYY-MM-DD/N" + // * The latest branch should be called "release/1.0.0" // * The latest branch should point to the latest commit const [latestCommitSubject, latestCommitId, latestCommitRevsMarker] = ( @@ -284,9 +284,9 @@ describe('create-release-branch (functional)', () => { '--max-count=1', ]) ).stdout; - expect(latestCommitSubject).toStrictEqual('Release 2022-06-24 (R2)'); + expect(latestCommitSubject).toStrictEqual('Release 2.0.0'); expect(latestCommitRevs).toContain('HEAD'); - expect(latestCommitRevs).toContain('release/2022-06-24/2'); + expect(latestCommitRevs).toContain('release/2.0.0'); expect(latestBranchCommitId).toStrictEqual(latestCommitId); }, ); diff --git a/src/initial-parameters.test.ts b/src/initial-parameters.test.ts index f5b425ea..7cf8357d 100644 --- a/src/initial-parameters.test.ts +++ b/src/initial-parameters.test.ts @@ -32,7 +32,7 @@ describe('initial-parameters', () => { }); jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ TODAY: '2022-06-22', EDITOR: undefined }); + .mockReturnValue({ EDITOR: undefined }); when(jest.spyOn(projectModule, 'readProject')) .calledWith('/path/to/project') .mockResolvedValue(project); @@ -46,7 +46,6 @@ describe('initial-parameters', () => { project, tempDirectoryPath: '/path/to/temp', reset: true, - today: new Date(2022, 5, 22), }); }); @@ -63,7 +62,7 @@ describe('initial-parameters', () => { }); jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ TODAY: undefined, EDITOR: undefined }); + .mockReturnValue({ EDITOR: undefined }); const readProjectSpy = jest .spyOn(projectModule, 'readProject') .mockResolvedValue(project); @@ -84,7 +83,7 @@ describe('initial-parameters', () => { }); jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ TODAY: undefined, EDITOR: undefined }); + .mockReturnValue({ EDITOR: undefined }); when(jest.spyOn(projectModule, 'readProject')) .calledWith('/path/to/project') .mockResolvedValue(project); @@ -110,7 +109,7 @@ describe('initial-parameters', () => { }); jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ TODAY: undefined, EDITOR: undefined }); + .mockReturnValue({ EDITOR: undefined }); when(jest.spyOn(projectModule, 'readProject')) .calledWith('/path/to/project') .mockResolvedValue(project); @@ -136,7 +135,7 @@ describe('initial-parameters', () => { }); jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ TODAY: undefined, EDITOR: undefined }); + .mockReturnValue({ EDITOR: undefined }); when(jest.spyOn(projectModule, 'readProject')) .calledWith('/path/to/project') .mockResolvedValue(project); @@ -160,7 +159,7 @@ describe('initial-parameters', () => { }); jest .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ TODAY: undefined, EDITOR: undefined }); + .mockReturnValue({ EDITOR: undefined }); when(jest.spyOn(projectModule, 'readProject')) .calledWith('/path/to/project') .mockResolvedValue(project); @@ -172,81 +171,5 @@ describe('initial-parameters', () => { expect(config.reset).toBe(false); }); - - it("returns initial parameters including today's date, derived from the TODAY environment variable", async () => { - const project = buildMockProject(); - when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments')) - .calledWith(['arg1', 'arg2']) - .mockResolvedValue({ - projectDirectory: '/path/to/project', - tempDirectory: '/path/to/temp', - reset: true, - }); - jest - .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ TODAY: '2022-01-01', EDITOR: undefined }); - when(jest.spyOn(projectModule, 'readProject')) - .calledWith('/path/to/project') - .mockResolvedValue(project); - - const config = await determineInitialParameters( - ['arg1', 'arg2'], - '/path/to/somewhere', - ); - - expect(config.today).toStrictEqual(new Date(2022, 0, 1)); - }); - - it('uses the current date if TODAY is undefined', async () => { - const project = buildMockProject(); - const today = new Date(2022, 0, 1); - when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments')) - .calledWith(['arg1', 'arg2']) - .mockResolvedValue({ - projectDirectory: '/path/to/project', - tempDirectory: undefined, - reset: true, - }); - jest - .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ TODAY: undefined, EDITOR: undefined }); - when(jest.spyOn(projectModule, 'readProject')) - .calledWith('/path/to/project') - .mockResolvedValue(project); - jest.setSystemTime(today); - - const config = await determineInitialParameters( - ['arg1', 'arg2'], - '/path/to/cwd', - ); - - expect(config.today).toStrictEqual(today); - }); - - it('uses the current date if TODAY is not a parsable date', async () => { - const project = buildMockProject(); - const today = new Date(2022, 0, 1); - when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments')) - .calledWith(['arg1', 'arg2']) - .mockResolvedValue({ - projectDirectory: '/path/to/project', - tempDirectory: undefined, - reset: true, - }); - jest - .spyOn(envModule, 'getEnvironmentVariables') - .mockReturnValue({ TODAY: 'asdfgdasf', EDITOR: undefined }); - when(jest.spyOn(projectModule, 'readProject')) - .calledWith('/path/to/project') - .mockResolvedValue(project); - jest.setSystemTime(today); - - const config = await determineInitialParameters( - ['arg1', 'arg2'], - '/path/to/cwd', - ); - - expect(config.today).toStrictEqual(today); - }); }); }); diff --git a/src/initial-parameters.ts b/src/initial-parameters.ts index 252a8390..f945931e 100644 --- a/src/initial-parameters.ts +++ b/src/initial-parameters.ts @@ -1,15 +1,12 @@ import os from 'os'; import path from 'path'; -import { parseISO as parseDateAsISO } from 'date-fns'; import { readCommandLineArguments } from './command-line-arguments'; -import { getEnvironmentVariables } from './env'; import { readProject, Project } from './project'; interface InitialParameters { project: Project; tempDirectoryPath: string; reset: boolean; - today: Date; } /** @@ -25,7 +22,6 @@ export async function determineInitialParameters( cwd: string, ): Promise { const inputs = await readCommandLineArguments(argv); - const { TODAY } = getEnvironmentVariables(); const projectDirectoryPath = path.resolve(cwd, inputs.projectDirectory); const project = await readProject(projectDirectoryPath); @@ -37,11 +33,6 @@ export async function determineInitialParameters( project.rootPackage.validatedManifest.name.replace('/', '__'), ) : path.resolve(cwd, inputs.tempDirectory); - const parsedTodayTimestamp = - TODAY === undefined ? NaN : parseDateAsISO(TODAY).getTime(); - const today = isNaN(parsedTodayTimestamp) - ? new Date() - : new Date(parsedTodayTimestamp); - return { project, tempDirectoryPath, reset: inputs.reset, today }; + return { project, tempDirectoryPath, reset: inputs.reset }; } diff --git a/src/main.test.ts b/src/main.test.ts index 78699002..539275cb 100644 --- a/src/main.test.ts +++ b/src/main.test.ts @@ -10,7 +10,6 @@ jest.mock('./monorepo-workflow-operations'); describe('main', () => { it('executes the monorepo workflow if the project is a monorepo', async () => { const project = buildMockProject({ isMonorepo: true }); - const today = new Date(); const stdout = fs.createWriteStream('/dev/null'); const stderr = fs.createWriteStream('/dev/null'); jest @@ -19,7 +18,6 @@ describe('main', () => { project, tempDirectoryPath: '/path/to/temp/directory', reset: false, - today, }); const followMonorepoWorkflowSpy = jest .spyOn(monorepoWorkflowOperations, 'followMonorepoWorkflow') @@ -36,7 +34,6 @@ describe('main', () => { project, tempDirectoryPath: '/path/to/temp/directory', firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); @@ -44,7 +41,6 @@ describe('main', () => { it('executes the polyrepo workflow if the project is within a polyrepo', async () => { const project = buildMockProject({ isMonorepo: false }); - const today = new Date(); const stdout = fs.createWriteStream('/dev/null'); const stderr = fs.createWriteStream('/dev/null'); jest @@ -53,7 +49,6 @@ describe('main', () => { project, tempDirectoryPath: '/path/to/temp/directory', reset: false, - today, }); const followMonorepoWorkflowSpy = jest .spyOn(monorepoWorkflowOperations, 'followMonorepoWorkflow') diff --git a/src/main.ts b/src/main.ts index 0e1dcdcf..66a0fe76 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,7 +25,7 @@ export async function main({ stdout: Pick; stderr: Pick; }) { - const { project, tempDirectoryPath, reset, today } = + const { project, tempDirectoryPath, reset } = await determineInitialParameters(argv, cwd); if (project.isMonorepo) { @@ -36,7 +36,6 @@ export async function main({ project, tempDirectoryPath, firstRemovingExistingReleaseSpecification: reset, - today, stdout, stderr, }); diff --git a/src/monorepo-workflow-operations.test.ts b/src/monorepo-workflow-operations.test.ts index 1d19f866..07b6d421 100644 --- a/src/monorepo-workflow-operations.test.ts +++ b/src/monorepo-workflow-operations.test.ts @@ -89,19 +89,25 @@ function buildMockReleaseSpecification({ * * @param overrides - The properties you want to override in the mock release * plan. - * @param overrides.releaseDate - The date of the release. - * @param overrides.releaseNumber - The number of the release. - * @param overrides.packages - Information about all of the packages in the - * project. For a polyrepo, this consists of the self-same package; for a - * monorepo it consists of the root package and any workspace packages. + * @param overrides.newVersion - The new version that should be released, + * encompassing one or more updates to packages within the project. This is + * always a SemVer-compatible string, though the meaning of each number depends + * on the type of project. For a polyrepo package or a monorepo with fixed + * versions, the format of the version string is "MAJOR.MINOR.PATCH"; for a + * monorepo with independent versions, it is "ORDINARY.BACKPORT.0", where + * `BACKPORT` is used to name a release that sits between two ordinary releases, + * and `ORDINARY` is used to name any other (non-backport) release. + * @param overrides.packages - Describes how the packages in the project should + * be updated. For a polyrepo package, this list will only contain the package + * itself; for a monorepo package it will consist of the root package and any + * workspace packages that will be included in the release. * @returns The mock release specification. */ function buildMockReleasePlan({ - releaseDate = new Date(), - releaseNumber = 1, + newVersion = '1.0.0', packages = [], }: Partial = {}): ReleasePlan { - return { releaseDate, releaseNumber, packages }; + return { newVersion, packages }; } /** @@ -139,6 +145,8 @@ function buildMockEditor({ * throw. * @param args.errorUponExecutingReleasePlan - The error that * `executeReleasePlan` will throw. + * @param args.releaseVersion - The new version that the release plan will + * contain. * @returns Mock functions and other data that can be used in tests to make * assertions. */ @@ -150,6 +158,7 @@ async function setupFollowMonorepoWorkflow({ errorUponValidatingReleaseSpec, errorUponPlanningRelease, errorUponExecutingReleasePlan, + releaseVersion = '1.0.0', }: { sandbox: Sandbox; doesReleaseSpecFileExist: boolean; @@ -158,6 +167,7 @@ async function setupFollowMonorepoWorkflow({ errorUponValidatingReleaseSpec?: Error; errorUponPlanningRelease?: Error; errorUponExecutingReleasePlan?: Error; + releaseVersion?: string; }) { const { determineEditorSpy, @@ -176,12 +186,9 @@ async function setupFollowMonorepoWorkflow({ const releaseSpecification = buildMockReleaseSpecification({ path: releaseSpecificationPath, }); - const releaseDate = new Date(2022, 0, 1); - const releaseNumber = 12345; - const releasePlan = buildMockReleasePlan({ releaseDate, releaseNumber }); + const releasePlan = buildMockReleasePlan({ newVersion: releaseVersion }); const projectDirectoryPath = '/path/to/project'; const project = buildMockProject({ directoryPath: projectDirectoryPath }); - const today = new Date(); const stdout = new MockWritable(); const stderr = new MockWritable(); determineEditorSpy.mockResolvedValue(isEditorAvailable ? editor : null); @@ -211,11 +218,11 @@ async function setupFollowMonorepoWorkflow({ if (errorUponPlanningRelease) { when(planReleaseSpy) - .calledWith({ project, releaseSpecification, today }) + .calledWith({ project, releaseSpecification }) .mockRejectedValue(errorUponPlanningRelease); } else { when(planReleaseSpy) - .calledWith({ project, releaseSpecification, today }) + .calledWith({ project, releaseSpecification }) .mockResolvedValue(releasePlan); } @@ -231,8 +238,7 @@ async function setupFollowMonorepoWorkflow({ when(captureChangesInReleaseBranchSpy) .calledWith(projectDirectoryPath, { - releaseDate, - releaseNumber, + releaseVersion, }) .mockResolvedValue(); @@ -246,7 +252,6 @@ async function setupFollowMonorepoWorkflow({ return { project, projectDirectoryPath, - today, stdout, stderr, generateReleaseSpecificationTemplateForMonorepoSpy, @@ -254,8 +259,7 @@ async function setupFollowMonorepoWorkflow({ executeReleasePlanSpy, captureChangesInReleaseBranchSpy, releasePlan, - releaseDate, - releaseNumber, + releaseVersion, releaseSpecificationPath, }; } @@ -267,7 +271,6 @@ describe('monorepo-workflow-operations', () => { await withSandbox(async (sandbox) => { const { project, - today, stdout, stderr, executeReleasePlanSpy, @@ -282,7 +285,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); @@ -299,41 +301,35 @@ describe('monorepo-workflow-operations', () => { await withSandbox(async (sandbox) => { const { project, - today, stdout, stderr, captureChangesInReleaseBranchSpy, projectDirectoryPath, - releaseDate, - releaseNumber, } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, isEditorAvailable: true, + releaseVersion: '4.38.0', }); await followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); expect(captureChangesInReleaseBranchSpy).toHaveBeenCalledWith( projectDirectoryPath, - { - releaseDate, - releaseNumber, - }, + { releaseVersion: '4.38.0' }, ); }); }); it('removes the release spec file after editing, validating, and executing the release spec', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -344,7 +340,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); @@ -355,7 +350,7 @@ describe('monorepo-workflow-operations', () => { it('does not attempt to execute the release spec if it was not successfully edited', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, executeReleasePlanSpy } = + const { project, stdout, stderr, executeReleasePlanSpy } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -368,7 +363,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }), @@ -380,25 +374,19 @@ describe('monorepo-workflow-operations', () => { it('does not attempt to create a new branch if the release spec was not successfully edited', async () => { await withSandbox(async (sandbox) => { - const { - project, - today, - stdout, - stderr, - captureChangesInReleaseBranchSpy, - } = await setupFollowMonorepoWorkflow({ - sandbox, - doesReleaseSpecFileExist: false, - isEditorAvailable: true, - errorUponEditingReleaseSpec: new Error('oops'), - }); + const { project, stdout, stderr, captureChangesInReleaseBranchSpy } = + await setupFollowMonorepoWorkflow({ + sandbox, + doesReleaseSpecFileExist: false, + isEditorAvailable: true, + errorUponEditingReleaseSpec: new Error('oops'), + }); await expect( followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }), @@ -410,7 +398,7 @@ describe('monorepo-workflow-operations', () => { it('removes the release spec file even if it was not successfully edited', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -423,7 +411,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }), @@ -435,20 +422,20 @@ describe('monorepo-workflow-operations', () => { it('throws an error produced while editing the release spec', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr } = - await setupFollowMonorepoWorkflow({ + const { project, stdout, stderr } = await setupFollowMonorepoWorkflow( + { sandbox, doesReleaseSpecFileExist: false, isEditorAvailable: true, errorUponEditingReleaseSpec: new Error('oops'), - }); + }, + ); await expect( followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }), @@ -459,7 +446,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if it was successfully edited but an error is thrown while validating the release spec', async () => { await withSandbox(async (sandbox) => { const errorUponValidatingReleaseSpec = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -472,7 +459,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }), @@ -485,7 +471,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if it was successfully edited but an error is thrown while planning the release', async () => { await withSandbox(async (sandbox) => { const errorUponPlanningRelease = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -498,7 +484,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }), @@ -511,7 +496,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if it was successfully edited but an error is thrown while executing the release plan', async () => { await withSandbox(async (sandbox) => { const errorUponExecutingReleasePlan = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -524,7 +509,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }), @@ -538,7 +522,7 @@ describe('monorepo-workflow-operations', () => { describe('when firstRemovingExistingReleaseSpecification is false, the release spec file does not already exist, and an editor is not available', () => { it('does not attempt to execute the edited release spec', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, executeReleasePlanSpy } = + const { project, stdout, stderr, executeReleasePlanSpy } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -549,7 +533,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); @@ -560,23 +543,17 @@ describe('monorepo-workflow-operations', () => { it('does not attempt to create a new branch', async () => { await withSandbox(async (sandbox) => { - const { - project, - today, - stdout, - stderr, - captureChangesInReleaseBranchSpy, - } = await setupFollowMonorepoWorkflow({ - sandbox, - doesReleaseSpecFileExist: false, - isEditorAvailable: false, - }); + const { project, stdout, stderr, captureChangesInReleaseBranchSpy } = + await setupFollowMonorepoWorkflow({ + sandbox, + doesReleaseSpecFileExist: false, + isEditorAvailable: false, + }); await followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); @@ -587,18 +564,18 @@ describe('monorepo-workflow-operations', () => { it('prints a message', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr } = - await setupFollowMonorepoWorkflow({ + const { project, stdout, stderr } = await setupFollowMonorepoWorkflow( + { sandbox, doesReleaseSpecFileExist: false, isEditorAvailable: false, - }); + }, + ); await followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); @@ -611,7 +588,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -622,7 +599,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); @@ -637,7 +613,6 @@ describe('monorepo-workflow-operations', () => { await withSandbox(async (sandbox) => { const { project, - today, stdout, stderr, waitForUserToEditReleaseSpecificationSpy, @@ -650,7 +625,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); @@ -665,7 +639,6 @@ describe('monorepo-workflow-operations', () => { await withSandbox(async (sandbox) => { const { project, - today, stdout, stderr, executeReleasePlanSpy, @@ -679,7 +652,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); @@ -696,40 +668,34 @@ describe('monorepo-workflow-operations', () => { await withSandbox(async (sandbox) => { const { project, - today, stdout, stderr, captureChangesInReleaseBranchSpy, projectDirectoryPath, - releaseDate, - releaseNumber, } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, + releaseVersion: '4.38.0', }); await followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); expect(captureChangesInReleaseBranchSpy).toHaveBeenCalledWith( projectDirectoryPath, - { - releaseDate, - releaseNumber, - }, + { releaseVersion: '4.38.0' }, ); }); }); it('removes the release spec file after validating and executing the release spec', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -739,7 +705,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }); @@ -751,7 +716,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if an error is thrown while validating the release spec', async () => { await withSandbox(async (sandbox) => { const errorUponValidatingReleaseSpec = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -763,7 +728,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }), @@ -776,7 +740,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if an error is thrown while planning the release', async () => { await withSandbox(async (sandbox) => { const errorUponPlanningRelease = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -788,7 +752,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }), @@ -801,7 +764,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if an error is thrown while executing the release plan', async () => { await withSandbox(async (sandbox) => { const errorUponExecutingReleasePlan = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -813,7 +776,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: false, - today, stdout, stderr, }), @@ -829,7 +791,6 @@ describe('monorepo-workflow-operations', () => { await withSandbox(async (sandbox) => { const { project, - today, stdout, stderr, executeReleasePlanSpy, @@ -844,7 +805,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -861,41 +821,35 @@ describe('monorepo-workflow-operations', () => { await withSandbox(async (sandbox) => { const { project, - today, stdout, stderr, captureChangesInReleaseBranchSpy, projectDirectoryPath, - releaseDate, - releaseNumber, } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, isEditorAvailable: true, + releaseVersion: '4.38.0', }); await followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); expect(captureChangesInReleaseBranchSpy).toHaveBeenCalledWith( projectDirectoryPath, - { - releaseDate, - releaseNumber, - }, + { releaseVersion: '4.38.0' }, ); }); }); it('removes the release spec file after editing, validating, and executing the release spec', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -906,7 +860,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -917,7 +870,7 @@ describe('monorepo-workflow-operations', () => { it('does not attempt to execute the release spec if it was not successfully edited', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, executeReleasePlanSpy } = + const { project, stdout, stderr, executeReleasePlanSpy } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -930,7 +883,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -942,25 +894,19 @@ describe('monorepo-workflow-operations', () => { it('does not attempt to create a new branch if the release spec was not successfully edited', async () => { await withSandbox(async (sandbox) => { - const { - project, - today, - stdout, - stderr, - captureChangesInReleaseBranchSpy, - } = await setupFollowMonorepoWorkflow({ - sandbox, - doesReleaseSpecFileExist: false, - isEditorAvailable: true, - errorUponEditingReleaseSpec: new Error('oops'), - }); + const { project, stdout, stderr, captureChangesInReleaseBranchSpy } = + await setupFollowMonorepoWorkflow({ + sandbox, + doesReleaseSpecFileExist: false, + isEditorAvailable: true, + errorUponEditingReleaseSpec: new Error('oops'), + }); await expect( followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -972,7 +918,7 @@ describe('monorepo-workflow-operations', () => { it('removes the release spec file even if it was not successfully edited', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -985,7 +931,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -997,20 +942,20 @@ describe('monorepo-workflow-operations', () => { it('throws an error produced while editing the release spec', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr } = - await setupFollowMonorepoWorkflow({ + const { project, stdout, stderr } = await setupFollowMonorepoWorkflow( + { sandbox, doesReleaseSpecFileExist: false, isEditorAvailable: true, errorUponEditingReleaseSpec: new Error('oops'), - }); + }, + ); await expect( followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -1021,7 +966,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if it was successfully edited but an error is thrown while validating the release spec', async () => { await withSandbox(async (sandbox) => { const errorUponValidatingReleaseSpec = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -1034,7 +979,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -1047,7 +991,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if it was successfully edited but an error is thrown while planning the release', async () => { await withSandbox(async (sandbox) => { const errorUponPlanningRelease = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -1060,7 +1004,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -1073,7 +1016,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if it was successfully edited but an error is thrown while executing the release plan', async () => { await withSandbox(async (sandbox) => { const errorUponExecutingReleasePlan = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -1086,7 +1029,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -1100,7 +1042,7 @@ describe('monorepo-workflow-operations', () => { describe('when firstRemovingExistingReleaseSpecification is true, the release spec file does not already exist, and an editor is not available', () => { it('does not attempt to execute the edited release spec', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, executeReleasePlanSpy } = + const { project, stdout, stderr, executeReleasePlanSpy } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -1111,7 +1053,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -1122,23 +1063,17 @@ describe('monorepo-workflow-operations', () => { it('does not attempt to create a new branch', async () => { await withSandbox(async (sandbox) => { - const { - project, - today, - stdout, - stderr, - captureChangesInReleaseBranchSpy, - } = await setupFollowMonorepoWorkflow({ - sandbox, - doesReleaseSpecFileExist: false, - isEditorAvailable: false, - }); + const { project, stdout, stderr, captureChangesInReleaseBranchSpy } = + await setupFollowMonorepoWorkflow({ + sandbox, + doesReleaseSpecFileExist: false, + isEditorAvailable: false, + }); await followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -1149,18 +1084,18 @@ describe('monorepo-workflow-operations', () => { it('prints a message', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr } = - await setupFollowMonorepoWorkflow({ + const { project, stdout, stderr } = await setupFollowMonorepoWorkflow( + { sandbox, doesReleaseSpecFileExist: false, isEditorAvailable: false, - }); + }, + ); await followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -1173,7 +1108,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: false, @@ -1184,7 +1119,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -1199,7 +1133,6 @@ describe('monorepo-workflow-operations', () => { await withSandbox(async (sandbox) => { const { project, - today, stdout, stderr, generateReleaseSpecificationTemplateForMonorepoSpy, @@ -1213,7 +1146,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -1231,7 +1163,6 @@ describe('monorepo-workflow-operations', () => { await withSandbox(async (sandbox) => { const { project, - today, stdout, stderr, executeReleasePlanSpy, @@ -1246,7 +1177,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -1263,41 +1193,35 @@ describe('monorepo-workflow-operations', () => { await withSandbox(async (sandbox) => { const { project, - today, stdout, stderr, captureChangesInReleaseBranchSpy, projectDirectoryPath, - releaseDate, - releaseNumber, } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, isEditorAvailable: true, + releaseVersion: '4.38.0', }); await followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); expect(captureChangesInReleaseBranchSpy).toHaveBeenCalledWith( projectDirectoryPath, - { - releaseDate, - releaseNumber, - }, + { releaseVersion: '4.38.0' }, ); }); }); it('removes the release spec file after editing, validating, and executing the release spec', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -1308,7 +1232,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -1319,7 +1242,7 @@ describe('monorepo-workflow-operations', () => { it('does not attempt to execute the release spec if it was not successfully edited', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, executeReleasePlanSpy } = + const { project, stdout, stderr, executeReleasePlanSpy } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -1332,7 +1255,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -1344,25 +1266,19 @@ describe('monorepo-workflow-operations', () => { it('does not attempt to create a new branch if the release spec was not successfully edited', async () => { await withSandbox(async (sandbox) => { - const { - project, - today, - stdout, - stderr, - captureChangesInReleaseBranchSpy, - } = await setupFollowMonorepoWorkflow({ - sandbox, - doesReleaseSpecFileExist: true, - isEditorAvailable: true, - errorUponEditingReleaseSpec: new Error('oops'), - }); + const { project, stdout, stderr, captureChangesInReleaseBranchSpy } = + await setupFollowMonorepoWorkflow({ + sandbox, + doesReleaseSpecFileExist: true, + isEditorAvailable: true, + errorUponEditingReleaseSpec: new Error('oops'), + }); await expect( followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -1374,7 +1290,7 @@ describe('monorepo-workflow-operations', () => { it('removes the release spec file even if it was not successfully edited', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -1387,7 +1303,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -1399,20 +1314,20 @@ describe('monorepo-workflow-operations', () => { it('throws an error produced while editing the release spec', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr } = - await setupFollowMonorepoWorkflow({ + const { project, stdout, stderr } = await setupFollowMonorepoWorkflow( + { sandbox, doesReleaseSpecFileExist: true, isEditorAvailable: true, errorUponEditingReleaseSpec: new Error('oops'), - }); + }, + ); await expect( followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -1423,7 +1338,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if it was successfully edited but an error is thrown while validating the release spec', async () => { await withSandbox(async (sandbox) => { const errorUponValidatingReleaseSpec = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -1436,7 +1351,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -1449,7 +1363,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if it was successfully edited but an error is thrown while planning the release', async () => { await withSandbox(async (sandbox) => { const errorUponPlanningRelease = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -1462,7 +1376,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -1475,7 +1388,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file if it was successfully edited but an error is thrown while executing the release plan', async () => { await withSandbox(async (sandbox) => { const errorUponExecutingReleasePlan = new Error('oops'); - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -1488,7 +1401,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }), @@ -1504,7 +1416,6 @@ describe('monorepo-workflow-operations', () => { await withSandbox(async (sandbox) => { const { project, - today, stdout, stderr, generateReleaseSpecificationTemplateForMonorepoSpy, @@ -1518,7 +1429,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -1534,7 +1444,7 @@ describe('monorepo-workflow-operations', () => { it('does not attempt to execute the edited release spec', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, executeReleasePlanSpy } = + const { project, stdout, stderr, executeReleasePlanSpy } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -1545,7 +1455,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -1556,23 +1465,17 @@ describe('monorepo-workflow-operations', () => { it('does not attempt to create a new branch', async () => { await withSandbox(async (sandbox) => { - const { - project, - today, - stdout, - stderr, - captureChangesInReleaseBranchSpy, - } = await setupFollowMonorepoWorkflow({ - sandbox, - doesReleaseSpecFileExist: true, - isEditorAvailable: false, - }); + const { project, stdout, stderr, captureChangesInReleaseBranchSpy } = + await setupFollowMonorepoWorkflow({ + sandbox, + doesReleaseSpecFileExist: true, + isEditorAvailable: false, + }); await followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -1583,18 +1486,18 @@ describe('monorepo-workflow-operations', () => { it('prints a message', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr } = - await setupFollowMonorepoWorkflow({ + const { project, stdout, stderr } = await setupFollowMonorepoWorkflow( + { sandbox, doesReleaseSpecFileExist: true, isEditorAvailable: false, - }); + }, + ); await followMonorepoWorkflow({ project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); @@ -1607,7 +1510,7 @@ describe('monorepo-workflow-operations', () => { it('does not remove the generated release spec file', async () => { await withSandbox(async (sandbox) => { - const { project, today, stdout, stderr, releaseSpecificationPath } = + const { project, stdout, stderr, releaseSpecificationPath } = await setupFollowMonorepoWorkflow({ sandbox, doesReleaseSpecFileExist: true, @@ -1618,7 +1521,6 @@ describe('monorepo-workflow-operations', () => { project, tempDirectoryPath: sandbox.directoryPath, firstRemovingExistingReleaseSpecification: true, - today, stdout, stderr, }); diff --git a/src/monorepo-workflow-operations.ts b/src/monorepo-workflow-operations.ts index 3be8713e..6466dabe 100644 --- a/src/monorepo-workflow-operations.ts +++ b/src/monorepo-workflow-operations.ts @@ -43,7 +43,6 @@ import { * possible for a release specification that was created in a previous run to * stick around (due to an error). This will ensure that the file is removed * first. - * @param options.today - The current date. * @param options.stdout - A stream that can be used to write to standard out. * @param options.stderr - A stream that can be used to write to standard error. */ @@ -51,14 +50,12 @@ export async function followMonorepoWorkflow({ project, tempDirectoryPath, firstRemovingExistingReleaseSpecification, - today, stdout, stderr, }: { project: Project; tempDirectoryPath: string; firstRemovingExistingReleaseSpecification: boolean; - today: Date; stdout: Pick; stderr: Pick; }) { @@ -110,12 +107,10 @@ export async function followMonorepoWorkflow({ const releasePlan = await planRelease({ project, releaseSpecification, - today, }); await executeReleasePlan(project, releasePlan, stderr); await removeFile(releaseSpecificationPath); await captureChangesInReleaseBranch(project.directoryPath, { - releaseDate: releasePlan.releaseDate, - releaseNumber: releasePlan.releaseNumber, + releaseVersion: releasePlan.newVersion, }); } diff --git a/src/project.test.ts b/src/project.test.ts index c125a23f..2701efdf 100644 --- a/src/project.test.ts +++ b/src/project.test.ts @@ -16,7 +16,7 @@ describe('project', () => { await withSandbox(async (sandbox) => { const projectDirectoryPath = sandbox.directoryPath; const projectRepositoryUrl = 'https://github.com/some-org/some-repo'; - const rootPackage = buildMockPackage('root', '20220722.1234.0', { + const rootPackage = buildMockPackage('root', '4.38.0', { directoryPath: projectDirectoryPath, validatedManifest: buildMockManifest({ workspaces: ['packages/a', 'packages/subpackages/*'], @@ -66,69 +66,12 @@ describe('project', () => { rootPackage, workspacePackages, isMonorepo: true, - releaseInfo: { - releaseDate: new Date(2022, 6, 22), - releaseNumber: 1234, + releaseVersion: { + ordinaryNumber: 4, + backportNumber: 38, }, }); }); }); - - it('throws if the release date portion of the root version is not in "..0" format', async () => { - await withSandbox(async (sandbox) => { - const projectDirectoryPath = sandbox.directoryPath; - const rootPackage = buildMockPackage('root', '1.2.3'); - when(jest.spyOn(packageModule, 'readPackage')) - .calledWith(projectDirectoryPath) - .mockResolvedValue(rootPackage); - - await expect(readProject(projectDirectoryPath)).rejects.toThrow( - 'Could not extract release info from package "root" version "1.2.3": Must be in "..0" format.', - ); - }); - }); - - it('throws if the release date portion of the root version is technically a valid date but is offset based on what was given', async () => { - await withSandbox(async (sandbox) => { - const projectDirectoryPath = sandbox.directoryPath; - // This evaluates to 2021-11-30 - const rootPackage = buildMockPackage('root', '20220000.1.0'); - when(jest.spyOn(packageModule, 'readPackage')) - .calledWith(projectDirectoryPath) - .mockResolvedValue(rootPackage); - - await expect(readProject(projectDirectoryPath)).rejects.toThrow( - 'Could not extract release info from package "root" version "20220000.1.0": "20220000" must be a valid date in "
" format.', - ); - }); - }); - - it('throws if the release date portion of the root version is not a valid date whatsoever', async () => { - await withSandbox(async (sandbox) => { - const projectDirectoryPath = sandbox.directoryPath; - const rootPackage = buildMockPackage('root', '99999999.1.0'); - when(jest.spyOn(packageModule, 'readPackage')) - .calledWith(projectDirectoryPath) - .mockResolvedValue(rootPackage); - - await expect(readProject(projectDirectoryPath)).rejects.toThrow( - 'Could not extract release info from package "root" version "99999999.1.0": "99999999" must be a valid date in "
" format.', - ); - }); - }); - - it('throws if the release version is 0', async () => { - await withSandbox(async (sandbox) => { - const projectDirectoryPath = sandbox.directoryPath; - const rootPackage = buildMockPackage('root', '20220101.0.0'); - when(jest.spyOn(packageModule, 'readPackage')) - .calledWith(projectDirectoryPath) - .mockResolvedValue(rootPackage); - - await expect(readProject(projectDirectoryPath)).rejects.toThrow( - 'Could not extract release info from package "root" version "20220101.0.0": Release version must be greater than 0.', - ); - }); - }); }); }); diff --git a/src/project.ts b/src/project.ts index 955edd46..437de58a 100644 --- a/src/project.ts +++ b/src/project.ts @@ -3,6 +3,19 @@ import glob from 'glob'; import { Package, readPackage } from './package'; import { PackageManifestFieldNames } from './package-manifest'; import { getRepositoryHttpsUrl } from './repo'; +import { SemVer } from './semver'; + +/** + * Information about the release of the root package of a monorepo extracted + * from its version string. + * + * @property releaseDate - The release date. + * @property releaseNumber - The release number (starting from 1). + */ +interface ReleaseVersion { + ordinaryNumber: number; + backportNumber: number; +} /** * Represents the entire codebase on which this tool is operating. @@ -21,19 +34,7 @@ export interface Project { rootPackage: Package; workspacePackages: Record; isMonorepo: boolean; - releaseInfo: ReleaseInfo; -} - -/** - * Information about the release of the root package of a monorepo extracted - * from its version string. - * - * @property releaseDate - The release date. - * @property releaseNumber - The release number (starting from 1). - */ -interface ReleaseInfo { - releaseDate: Date; - releaseNumber: number; + releaseVersion: ReleaseVersion; } /** @@ -42,61 +43,19 @@ interface ReleaseInfo { const promisifiedGlob = util.promisify(glob); /** - * Reads a version string from a SemVer object and extracts the release date and - * release number from it. + * Given a SemVer version object, interprets the "major" part of the version + * as the ordinary release number and the "minor" part as the backport release + * number in the context of the ordinary release. * - * @param packageVersionString - The version string of the package. - * @param packageName - The name of the package. - * @returns An object containing the release date and release number from the - * version string. - * @throws If the version string is invalid in some way. + * @param packageVersion - The version of the package. + * @returns An object containing the ordinary and backport numbers in the + * version. */ -function parseReleaseInfoFrom( - packageVersionString: string, - packageName: string, -): ReleaseInfo { - const match = packageVersionString.match( - /^(?(?\d{4})(?\d{2})(?\d{2}))\.(?\d+)\.0$/u, - ); - const errorMessagePrefix = `Could not extract release info from package "${packageName}" version "${packageVersionString}"`; - - if (match?.groups) { - const releaseYear = Number(match.groups.releaseYearString); - const releaseMonthNumber = Number(match.groups.releaseMonthString); - const releaseDay = Number(match.groups.releaseDayString); - const releaseDate = new Date( - releaseYear, - releaseMonthNumber - 1, - releaseDay, - 0, - 0, - 0, - ); - const releaseNumber = Number(match.groups.releaseNumberString); - - if ( - isNaN(releaseDate.getTime()) || - releaseDate.getFullYear() !== releaseYear || - releaseDate.getMonth() !== releaseMonthNumber - 1 || - releaseDate.getDate() !== releaseDay - ) { - throw new Error( - `${errorMessagePrefix}: "${match.groups.releaseDateString}" must be a valid date in "
" format.`, - ); - } - - if (releaseNumber === 0) { - throw new Error( - `${errorMessagePrefix}: Release version must be greater than 0.`, - ); - } - - return { releaseDate, releaseNumber }; - } - - throw new Error( - `${errorMessagePrefix}: Must be in "..0" format.`, - ); +function examineReleaseVersion(packageVersion: SemVer): ReleaseVersion { + return { + ordinaryNumber: packageVersion.major, + backportNumber: packageVersion.minor, + }; } /** @@ -115,9 +74,8 @@ export async function readProject( ): Promise { const repositoryUrl = await getRepositoryHttpsUrl(projectDirectoryPath); const rootPackage = await readPackage(projectDirectoryPath); - const releaseInfo = parseReleaseInfoFrom( - rootPackage.validatedManifest.version.toString(), - rootPackage.validatedManifest.name, + const releaseVersion = examineReleaseVersion( + rootPackage.validatedManifest.version, ); const workspaceDirectories = ( @@ -151,6 +109,6 @@ export async function readProject( rootPackage, workspacePackages, isMonorepo, - releaseInfo, + releaseVersion, }; } diff --git a/src/release-plan.test.ts b/src/release-plan.test.ts index a4164e1e..03ef4db3 100644 --- a/src/release-plan.test.ts +++ b/src/release-plan.test.ts @@ -11,7 +11,7 @@ describe('release-plan-utils', () => { describe('planRelease', () => { it('calculates final versions for all packages in the release spec', async () => { const project = buildMockProject({ - rootPackage: buildMockPackage('root', '20220721.1.0'), + rootPackage: buildMockPackage('root', '1.0.0'), workspacePackages: { a: buildMockPackage('a', '1.0.0'), b: buildMockPackage('b', '1.0.0'), @@ -28,21 +28,18 @@ describe('release-plan-utils', () => { }, path: '/path/to/release/spec', }; - const today = new Date(2022, 7, 1); const releasePlan = await planRelease({ project, releaseSpecification, - today, }); expect(releasePlan).toMatchObject({ - releaseDate: today, - releaseNumber: 2, + newVersion: '2.0.0', packages: [ { package: project.rootPackage, - newVersion: '20220801.2.0', + newVersion: '2.0.0', }, { package: project.workspacePackages.a, @@ -64,33 +61,6 @@ describe('release-plan-utils', () => { }); }); - it('merely bumps the build number in the root version if a release is being created on the same day as a previous release', async () => { - const project = buildMockProject({ - rootPackage: buildMockPackage('root', '20220101.1.0'), - workspacePackages: {}, - }); - const releaseSpecification = { - packages: {}, - path: '/path/to/release/spec', - }; - const today = new Date(2022, 0, 1); - - const releasePlan = await planRelease({ - project, - releaseSpecification, - today, - }); - - expect(releasePlan).toMatchObject({ - packages: [ - { - package: project.rootPackage, - newVersion: '20220101.2.0', - }, - ], - }); - }); - it('records that the changelog for the root package does not need to be updated, while those for the workspace packages do', async () => { const project = buildMockProject({ rootPackage: buildMockPackage('root'), @@ -110,12 +80,10 @@ describe('release-plan-utils', () => { }, path: '/path/to/release/spec', }; - const today = new Date(); const releasePlan = await planRelease({ project, releaseSpecification, - today, }); expect(releasePlan).toMatchObject({ @@ -149,8 +117,7 @@ describe('release-plan-utils', () => { it('runs updatePackage for each package in the release plan', async () => { const project = buildMockProject(); const releasePlan = { - releaseDate: new Date(), - releaseNumber: 1, + newVersion: '1.0.0', packages: [ { package: buildMockPackage(), diff --git a/src/release-plan.ts b/src/release-plan.ts index b9401430..f8408078 100644 --- a/src/release-plan.ts +++ b/src/release-plan.ts @@ -1,6 +1,5 @@ import type { WriteStream } from 'fs'; import { SemVer } from 'semver'; -import { formatISO as formatDateAsISO } from 'date-fns'; import { debug } from './misc-utils'; import { Package, updatePackage } from './package'; import { Project } from './project'; @@ -10,16 +9,21 @@ import { ReleaseSpecification } from './release-specification'; * Instructions for how to update the project in order to prepare it for a new * release. * - * @property releaseDate - The date associated with the new release. - * @property releaseNumber - The number of the new release, as 1 + the number of - * the previous release. - * @property packages - Information about all of the packages in the project. - * For a polyrepo, this consists of the self-same package; for a monorepo it - * consists of the root package and any workspace packages. + * @property newVersion - The new version that should be released, encompassing + * one or more updates to packages within the project. This is always a + * SemVer-compatible string, though the meaning of each number depends on the + * type of project. For a polyrepo package or a monorepo with fixed versions, + * the format of the version string is "MAJOR.MINOR.PATCH"; for a monorepo with + * independent versions, it is "ORDINARY.BACKPORT.0", where `BACKPORT` is used + * to name a release that sits between two ordinary releases, and `ORDINARY` is + * used to name any other (non-backport) release. + * @property packages - Describes how the packages in the project should be + * updated. For a polyrepo package, this list will only contain the package + * itself; for a monorepo package it will consist of the root package and any + * workspace packages that will be included in the release. */ export interface ReleasePlan { - releaseDate: Date; - releaseNumber: number; + newVersion: string; packages: PackageReleasePlan[]; } @@ -28,8 +32,8 @@ export interface ReleasePlan { * it for a new release. * * @property package - Information about the package. - * @property newVersion - The new version to which the package should be - * updated. + * @property newVersion - The new version for the package, as a + * SemVer-compatible string. * @property shouldUpdateChangelog - Whether or not the changelog for the * package should get updated. For a polyrepo, this will always be true; for a * monorepo, this will be true only for workspace packages (the root package @@ -50,28 +54,20 @@ export interface PackageReleasePlan { * packages and where they can found). * @param args.releaseSpecification - A parsed version of the release spec * entered by the user. - * @param args.today - The current date. * @returns A promise for information about the new release. */ export async function planRelease({ project, releaseSpecification, - today, }: { project: Project; releaseSpecification: ReleaseSpecification; - today: Date; }): Promise { - const newReleaseDate = formatDateAsISO(today, { - format: 'basic', - representation: 'date', - }); - const newReleaseNumber = project.releaseInfo.releaseNumber + 1; - const newRootVersion = `${newReleaseDate}.${newReleaseNumber}.0`; + const newReleaseVersion = `${project.releaseVersion.ordinaryNumber + 1}.0.0`; const rootReleasePlan: PackageReleasePlan = { package: project.rootPackage, - newVersion: newRootVersion, + newVersion: newReleaseVersion, shouldUpdateChangelog: false, }; @@ -94,8 +90,7 @@ export async function planRelease({ }); return { - releaseDate: today, - releaseNumber: newReleaseNumber, + newVersion: newReleaseVersion, packages: [rootReleasePlan, ...workspaceReleasePlans], }; } diff --git a/src/repo.test.ts b/src/repo.test.ts index 072d7958..ffeb5b09 100644 --- a/src/repo.test.ts +++ b/src/repo.test.ts @@ -97,13 +97,12 @@ describe('git-utils', () => { ); await captureChangesInReleaseBranch('/path/to/project', { - releaseDate: new Date(2022, 6, 22), - releaseNumber: 12345, + releaseVersion: '1.0.0', }); expect(getStdoutFromCommandSpy).toHaveBeenCalledWith( 'git', - ['checkout', '-b', 'release/2022-07-22/12345'], + ['checkout', '-b', 'release/1.0.0'], { cwd: '/path/to/project' }, ); expect(getStdoutFromCommandSpy).toHaveBeenCalledWith( @@ -113,7 +112,7 @@ describe('git-utils', () => { ); expect(getStdoutFromCommandSpy).toHaveBeenCalledWith( 'git', - ['commit', '-m', 'Release 2022-07-22 (R12345)'], + ['commit', '-m', 'Release 1.0.0'], { cwd: '/path/to/project' }, ); }); diff --git a/src/repo.ts b/src/repo.ts index e6a56496..bd5fdcfb 100644 --- a/src/repo.ts +++ b/src/repo.ts @@ -1,4 +1,3 @@ -import { formatISO as formatDateAsISO } from 'date-fns'; import { getStdoutFromCommand } from './misc-utils'; /** @@ -91,32 +90,21 @@ export async function getRepositoryHttpsUrl( * * @param projectRepositoryPath - The path to the repository directory. * @param args - The arguments. - * @param args.releaseDate - The release date. - * @param args.releaseNumber - The release number. + * @param args.releaseVersion - The release version. */ export async function captureChangesInReleaseBranch( projectRepositoryPath: string, - { - releaseDate, - releaseNumber, - }: { - releaseDate: Date; - releaseNumber: number; - }, + { releaseVersion }: { releaseVersion: string }, ) { - const releaseDateAsISO = formatDateAsISO(releaseDate, { - representation: 'date', - }); - await getStdoutFromGitCommandWithin(projectRepositoryPath, [ 'checkout', '-b', - `release/${releaseDateAsISO}/${releaseNumber}`, + `release/${releaseVersion}`, ]); await getStdoutFromGitCommandWithin(projectRepositoryPath, ['add', '-A']); await getStdoutFromGitCommandWithin(projectRepositoryPath, [ 'commit', '-m', - `Release ${releaseDateAsISO} (R${releaseNumber})`, + `Release ${releaseVersion}`, ]); } diff --git a/tests/unit/helpers.ts b/tests/unit/helpers.ts index 17c58c8b..dfb5f050 100644 --- a/tests/unit/helpers.ts +++ b/tests/unit/helpers.ts @@ -44,9 +44,9 @@ export function buildMockProject(overrides: Partial = {}): Project { rootPackage: buildMockPackage('root'), workspacePackages: {}, isMonorepo: false, - releaseInfo: { - releaseDate: new Date(), - releaseNumber: 1, + releaseVersion: { + ordinaryNumber: 1, + backportNumber: 0, }, ...overrides, }; diff --git a/yarn.lock b/yarn.lock index d32d4018..02499a4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -881,7 +881,6 @@ __metadata: "@types/yargs": ^17.0.10 "@typescript-eslint/eslint-plugin": ^4.21.0 "@typescript-eslint/parser": ^4.21.0 - date-fns: ^2.29.1 debug: ^4.3.4 deepmerge: ^4.2.2 eslint: ^7.23.0 @@ -2162,13 +2161,6 @@ __metadata: languageName: node linkType: hard -"date-fns@npm:^2.29.1": - version: 2.29.1 - resolution: "date-fns@npm:2.29.1" - checksum: 9d07f77dffc1eb8c213391bde39f2963ffe7c0019d9edde14487882d627224f3a39b963e6e99d0cc58afff220a6a1a7e8864d2789958f4eaa77714de94d4d076 - languageName: node - linkType: hard - "debug@npm:4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" From 7b9e55414ab2cce54cb91d333f72a437cd2a50dd Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Mon, 19 Sep 2022 17:10:40 -0600 Subject: [PATCH 4/6] Remove 'today' entirely from functional tests --- src/functional.test.ts | 2 -- .../functional/helpers/monorepo-environment.ts | 18 +++--------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/functional.test.ts b/src/functional.test.ts index ade59e34..4be3f46d 100644 --- a/src/functional.test.ts +++ b/src/functional.test.ts @@ -41,7 +41,6 @@ describe('create-release-branch (functional)', () => { workspaces: { '.': ['packages/*'], }, - today: new Date(2022, 5, 24), }, async (environment) => { await environment.updateJsonFile('package.json', { @@ -250,7 +249,6 @@ describe('create-release-branch (functional)', () => { workspaces: { '.': ['packages/*'], }, - today: new Date(2022, 5, 24), }, async (environment) => { await environment.runTool({ diff --git a/tests/functional/helpers/monorepo-environment.ts b/tests/functional/helpers/monorepo-environment.ts index 12c3497a..fc082467 100644 --- a/tests/functional/helpers/monorepo-environment.ts +++ b/tests/functional/helpers/monorepo-environment.ts @@ -17,15 +17,12 @@ import { debug, knownKeysOf } from './utils'; * @property packages - The known packages within this repo (including the * root). * @property workspaces - The known workspaces within this repo. - * @property today - The date that will be used for new releases. Will be - * translated to the TODAY environment variables. */ export interface MonorepoEnvironmentOptions< WorkspacePackageNickname extends string, > extends EnvironmentOptions { packages: Record; workspaces: Record; - today?: Date; } /** @@ -55,15 +52,9 @@ export default class MonorepoEnvironment< #packages: MonorepoEnvironmentOptions['packages']; - #today: MonorepoEnvironmentOptions['today']; - - constructor({ - today, - ...rest - }: MonorepoEnvironmentOptions) { - super(rest); - this.#packages = rest.packages; - this.#today = today; + constructor(options: MonorepoEnvironmentOptions) { + super(options); + this.#packages = options.packages; this.readFileWithinPackage = this.localRepo.readFileWithinPackage.bind( this.localRepo, ); @@ -133,9 +124,6 @@ cat "${releaseSpecificationPath}" > "$1" const env = { EDITOR: releaseSpecificationEditorPath, - ...(this.#today === undefined - ? {} - : { TODAY: this.#today.toISOString().replace(/T.+$/u, '') }), }; const result = await this.localRepo.runCommand( From 6f1274cc46e5b10fc69a05c09be03a604450ffeb Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Mon, 19 Sep 2022 17:24:43 -0600 Subject: [PATCH 5/6] Update docs for ReleaseVersion --- src/project.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/project.ts b/src/project.ts index 437de58a..1d22ec4f 100644 --- a/src/project.ts +++ b/src/project.ts @@ -6,11 +6,17 @@ import { getRepositoryHttpsUrl } from './repo'; import { SemVer } from './semver'; /** - * Information about the release of the root package of a monorepo extracted - * from its version string. + * The release version of the root package of a monorepo extracted from its + * version string. * - * @property releaseDate - The release date. - * @property releaseNumber - The release number (starting from 1). + * @property ordinaryNumber - The number assigned to the release if it + * introduces new changes that haven't appeared in any previous release; it will + * be 0 if there haven't been any releases yet. + * @property backportNumber - A backport release is a change ported from one + * ordinary release to a previous ordinary release. This, then, is the number + * which identifies this release relative to other backport releases under the + * same ordinary release, starting from 1; it will be 0 if there aren't any + * backport releases for the ordinary release yet. */ interface ReleaseVersion { ordinaryNumber: number; From dd1189d565a045bfe6fd16fb013df415b2dcd429 Mon Sep 17 00:00:00 2001 From: Elliot Winkler Date: Thu, 22 Sep 2022 09:26:57 -0600 Subject: [PATCH 6/6] Fix typo in test name Co-authored-by: Mark Stacey --- src/initial-parameters.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/initial-parameters.test.ts b/src/initial-parameters.test.ts index 7cf8357d..c4459b0a 100644 --- a/src/initial-parameters.test.ts +++ b/src/initial-parameters.test.ts @@ -124,7 +124,7 @@ describe('initial-parameters', () => { ); }); - it('returns initial parameters including reset: false, derived from a command-line argument of "--reset true"', async () => { + it('returns initial parameters including reset: true, derived from a command-line argument of "--reset true"', async () => { const project = buildMockProject(); when(jest.spyOn(commandLineArgumentsModule, 'readCommandLineArguments')) .calledWith(['arg1', 'arg2'])