Skip to content

Commit

Permalink
all tests passing
Browse files Browse the repository at this point in the history
  • Loading branch information
edwardfoyle committed Apr 5, 2024
1 parent 490d9ed commit 82fc010
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 31 deletions.
29 changes: 14 additions & 15 deletions scripts/components/git_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
}
};

/**
Expand All @@ -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`;
};

/**
Expand Down Expand Up @@ -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
);
Expand All @@ -171,10 +174,6 @@ export class GitClient {
packageNamesRemaining.delete(packageName);
}
});
releaseCommitCursor = await this.getNearestReleaseCommit(
releaseCommitCursor,
{ inclusive: false }
);
}

return previousReleaseTags;
Expand Down
6 changes: 5 additions & 1 deletion scripts/components/npm_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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) => {
Expand Down
223 changes: 208 additions & 15 deletions scripts/components/release_lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -43,19 +44,19 @@ 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 ● <- [email protected], [email protected], platypus@latest
* second release commit ● <- [email protected], platypus@latest
* |
* minor bump of cantaloupe and platypus ●
* |
* first release commit ● <- [email protected], [email protected]
* |
* minor bump of cantaloupe and platypus ●
* |
* initial commit ● <- [email protected], [email protected]
* baseline release ● <- [email protected], [email protected]
*
*/
beforeEach(async ({ name: testName }) => {
Expand Down Expand Up @@ -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(
Expand All @@ -115,7 +116,7 @@ void describe('ReleaseLifecycleManager', async () => {
await commitVersionBumpChangeset(
testWorkingDir,
gitClient,
[cantaloupePackageName, platypusPackageName],
[platypusPackageName],
'minor'
);
await runVersionInTestDir();
Expand All @@ -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 [email protected] 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 [email protected] 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 [email protected] and [email protected] 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 [email protected] and [email protected] 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 [email protected] 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 [email protected] 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 = {
Expand Down

0 comments on commit 82fc010

Please sign in to comment.