From 82fc01021338b30c565d0ab73da986da350892ee Mon Sep 17 00:00:00 2001 From: Edward Foyle Date: Fri, 5 Apr 2024 10:58:09 -0700 Subject: [PATCH] all tests passing --- scripts/components/git_client.ts | 29 ++- scripts/components/npm_client.ts | 6 +- scripts/components/release_lifecycle.test.ts | 223 +++++++++++++++++-- 3 files changed, 227 insertions(+), 31 deletions(-) diff --git a/scripts/components/git_client.ts b/scripts/components/git_client.ts index 4d49424331..61e6887875 100644 --- a/scripts/components/git_client.ts +++ b/scripts/components/git_client.ts @@ -57,11 +57,16 @@ export class GitClient { /** * Switches to branchName. Creates the branch if it does not exist. - * - * Resets the branch to the original one at the end of the process */ switchToBranch = async (branchName: string) => { - await this.exec`git switch -C ${branchName}`; + const { stdout: branchResult } = await this + .exec`git branch -l ${branchName}`; + const branchExists = branchResult.trim().length > 0; + if (branchExists) { + await this.execWithIO`git switch ${branchName}`; + } else { + await this.execWithIO`git switch -c ${branchName}`; + } }; /** @@ -70,7 +75,7 @@ export class GitClient { commitAllChanges = async (message: string) => { await this.configure(); await this.execWithIO`git add .`; - await this.execWithIO`git commit --message ${message}`; + await this.execWithIO`git commit --message ${message} --allow-empty`; }; /** @@ -147,18 +152,16 @@ export class GitClient { .map((nameAndVersion) => nameAndVersion.packageName) ); - // initialize the release commit cursor to the commit of the release before the input releaseCommitHash - let releaseCommitCursor = await this.getNearestReleaseCommit( - releaseCommitHash, - { - inclusive: false, - } - ); + let releaseCommitCursor = releaseCommitHash; // the method return value that we will append release tags to in the loop const previousReleaseTags: string[] = []; while (packageNamesRemaining.size > 0) { + releaseCommitCursor = await this.getNearestReleaseCommit( + releaseCommitCursor, + { inclusive: false } + ); const releaseTagsAtCursor = await this.getTagsAtCommit( releaseCommitCursor ); @@ -171,10 +174,6 @@ export class GitClient { packageNamesRemaining.delete(packageName); } }); - releaseCommitCursor = await this.getNearestReleaseCommit( - releaseCommitCursor, - { inclusive: false } - ); } return previousReleaseTags; diff --git a/scripts/components/npm_client.ts b/scripts/components/npm_client.ts index 8dcf906cc4..8f2946d754 100644 --- a/scripts/components/npm_client.ts +++ b/scripts/components/npm_client.ts @@ -15,7 +15,9 @@ export type PackageInfo = { }; /** + * Client for programmatically interacting with the local npm cli. * + * Note that this class is not guaranteed to be a singleton so it should not store any mutable internal state */ export class NpmClient { /** @@ -50,7 +52,9 @@ export class NpmClient { }; unDeprecatePackage = async (packageVersionSpecifier: string) => { - await this.execWithIO`npm deprecate ${packageVersionSpecifier} ""`; + // explicitly specifying an empty deprecation message is the official way to "un-deprecate" a package + // see https://docs.npmjs.com/cli/v8/commands/npm-deprecate + await this.execWithIO`npm deprecate ${packageVersionSpecifier} ${''}`; }; setDistTag = async (packageVersionSpecifier: string, distTag: string) => { diff --git a/scripts/components/release_lifecycle.test.ts b/scripts/components/release_lifecycle.test.ts index 9f696a7f1d..ef694c2938 100644 --- a/scripts/components/release_lifecycle.test.ts +++ b/scripts/components/release_lifecycle.test.ts @@ -16,6 +16,7 @@ import { GithubClient } from './github_client.js'; import assert from 'node:assert'; import { ReleaseDeprecator } from './release_deprecator.js'; import { DistTagMover } from './dist_tag_mover.js'; +import { ReleaseRestorer } from './release_restorer.js'; /** * This test suite is more of an integration test than a unit test. @@ -43,11 +44,11 @@ void describe('ReleaseLifecycleManager', async () => { * It seeds the local npm proxy with a few releases of these packages. * When its done, the state of the git refs npm dist-tags should be as follows: * - * third release commit ● <- HEAD, cantaloupe@1.3.0, cantaloupe@latest + * third release commit ● <- HEAD, cantaloupe@1.2.0, cantaloupe@latest * | * minor bump of cantaloupe only ● * | - * second release commit ● <- cantaloupe@1.2.0, platypus@1.2.0, platypus@latest + * second release commit ● <- platypus@1.2.0, platypus@latest * | * minor bump of cantaloupe and platypus ● * | @@ -55,7 +56,7 @@ void describe('ReleaseLifecycleManager', async () => { * | * minor bump of cantaloupe and platypus ● * | - * initial commit ● <- cantaloupe@1.0.0, platypus@1.0.0 + * baseline release ● <- cantaloupe@1.0.0, platypus@1.0.0 * */ beforeEach(async ({ name: testName }) => { @@ -99,7 +100,7 @@ void describe('ReleaseLifecycleManager', async () => { await npmClient.install(['@changesets/cli']); await $`npx changeset init`; - await gitClient.commitAllChanges('initial commit'); + await gitClient.commitAllChanges('Version Packages (baseline release)'); await runPublishInTestDir(); await commitVersionBumpChangeset( @@ -115,7 +116,7 @@ void describe('ReleaseLifecycleManager', async () => { await commitVersionBumpChangeset( testWorkingDir, gitClient, - [cantaloupePackageName, platypusPackageName], + [platypusPackageName], 'minor' ); await runVersionInTestDir(); @@ -131,33 +132,225 @@ void describe('ReleaseLifecycleManager', async () => { await runVersionInTestDir(); await gitClient.commitAllChanges('Version Packages (third release)'); await runPublishInTestDir(); + + // sanity check initial state + await expectDistTagAtVersion( + npmClient, + cantaloupePackageName, + '1.2.0', + 'latest' + ); + await expectDistTagAtVersion( + npmClient, + platypusPackageName, + '1.2.0', + 'latest' + ); }); - void it('dummy test', async () => { + void it('can deprecate and restore packages using npm metadata', async () => { const githubClient = new GithubClient('garbage'); mock.method(githubClient, 'createPr', async () => ({ prUrl: 'testPrUrl' })); mock.method(gitClient, 'push', async () => {}); - const releaseDeprecator = new ReleaseDeprecator( + const distTagMover = new DistTagMover(npmClient); + const releaseDeprecator1 = new ReleaseDeprecator( 'HEAD', 'the cantaloupe is rotten', githubClient, gitClient, npmClient, - new DistTagMover(npmClient) + distTagMover + ); + await releaseDeprecator1.deprecateRelease(); + + // expect cantaloupe@1.2.0 to be deprecated and cantaloupe@latest = 1.1.0 + await expectDeprecated( + npmClient, + cantaloupePackageName, + '1.2.0', + 'the cantaloupe is rotten' + ); + await expectDistTagAtVersion( + npmClient, + cantaloupePackageName, + '1.1.0', + 'latest' + ); + + // now deprecate the platypus release + + await gitClient.switchToBranch('main'); + const releaseDeprecator2 = new ReleaseDeprecator( + 'HEAD~', + 'RIP platypus', + githubClient, + gitClient, + npmClient, + distTagMover + ); + + await releaseDeprecator2.deprecateRelease(); + + // expect platypus@1.2.0 to be deprecated and platypus@latest = 1.1.0 + await expectDeprecated( + npmClient, + platypusPackageName, + '1.2.0', + 'RIP platypus' + ); + await expectDistTagAtVersion( + npmClient, + platypusPackageName, + '1.1.0', + 'latest' + ); + + // now deprecate the 1.1.0 release of both packages + + await gitClient.switchToBranch('main'); + const releaseDeprecator3 = new ReleaseDeprecator( + 'HEAD~3', + 'real big mess', + githubClient, + gitClient, + npmClient, + distTagMover + ); + + await releaseDeprecator3.deprecateRelease(); + + // expect platypus@1.1.0 and cantaloupe@1.1.0 to be deprecated and @latest points to 1.0.0 for both + await expectDeprecated( + npmClient, + platypusPackageName, + '1.1.0', + 'real big mess' + ); + await expectDistTagAtVersion( + npmClient, + platypusPackageName, + '1.0.0', + 'latest' + ); + await expectDeprecated( + npmClient, + cantaloupePackageName, + '1.1.0', + 'real big mess' + ); + await expectDistTagAtVersion( + npmClient, + cantaloupePackageName, + '1.0.0', + 'latest' + ); + + /* To validate the restore scenarios, we now "undo" the rollbacks */ + + await gitClient.switchToBranch('main'); + const releaseRestorer1 = new ReleaseRestorer( + 'HEAD~3', + githubClient, + gitClient, + npmClient, + distTagMover + ); + + await releaseRestorer1.restoreRelease(); + + // expect platypus@1.1.0 and cantaloupe@1.1.0 to be @latest and no longer deprecated + await expectNotDeprecated(npmClient, platypusPackageName, '1.1.0'); + await expectDistTagAtVersion( + npmClient, + platypusPackageName, + '1.1.0', + 'latest' + ); + await expectNotDeprecated(npmClient, cantaloupePackageName, '1.1.0'); + await expectDistTagAtVersion( + npmClient, + cantaloupePackageName, + '1.1.0', + 'latest' + ); + + await gitClient.switchToBranch('main'); + const releaseRestorer2 = new ReleaseRestorer( + 'HEAD~', + githubClient, + gitClient, + npmClient, + distTagMover + ); + + await releaseRestorer2.restoreRelease(); + + // expect platypus@1.2.0 to @latest and no longer deprecated + await expectNotDeprecated(npmClient, platypusPackageName, '1.2.0'); + await expectDistTagAtVersion( + npmClient, + platypusPackageName, + '1.2.0', + 'latest' ); - await releaseDeprecator.deprecateRelease(); - // switch back to the original branch + await gitClient.switchToBranch('main'); + const releaseRestorer3 = new ReleaseRestorer( + 'HEAD', + githubClient, + gitClient, + npmClient, + distTagMover + ); - // expect latest of cantaloupe to point back to 1.2.0 and 1.3.0 to be marked deprecated + await releaseRestorer3.restoreRelease(); - const { 'dist-tags': distTags, deprecated } = - await npmClient.getPackageInfo(`${cantaloupePackageName}@1.3.0`); - assert.equal(distTags.latest, '1.2.0'); - assert.equal(deprecated, 'the cantaloupe is rotten'); + // expect cantaloupe@1.2.0 to be @latest and no longer deprecated + await expectNotDeprecated(npmClient, cantaloupePackageName, '1.2.0'); + await expectDistTagAtVersion( + npmClient, + cantaloupePackageName, + '1.2.0', + 'latest' + ); + + // We are now back to the original state having deprecated and then restored 3 releases }); }); +const expectDeprecated = async ( + npmClient: NpmClient, + packageName: string, + version: string, + deprecationMessage: string +) => { + const { deprecated } = await npmClient.getPackageInfo( + `${packageName}@${version}` + ); + assert.equal(deprecated, deprecationMessage); +}; + +const expectNotDeprecated = async ( + npmClient: NpmClient, + packageName: string, + version: string +) => { + const { deprecated } = await npmClient.getPackageInfo( + `${packageName}@${version}` + ); + assert.equal(deprecated, undefined); +}; + +const expectDistTagAtVersion = async ( + npmClient: NpmClient, + packageName: string, + version: string, + distTag: string +) => { + const { 'dist-tags': distTags } = await npmClient.getPackageInfo(packageName); + assert.equal(distTags[distTag], version); +}; + const setPackageToPublic = async (packagePath: string) => { const packageJson = await readPackageJson(packagePath); packageJson.publishConfig = {