Skip to content

Commit

Permalink
its working!
Browse files Browse the repository at this point in the history
  • Loading branch information
edwardfoyle committed Apr 4, 2024
1 parent 91c949c commit 72679d6
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 61 deletions.
20 changes: 8 additions & 12 deletions scripts/components/git_client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { $ as chainableExeca, execa } from 'execa';
import { $ as chainableExeca } from 'execa';
import { writeFile } from 'fs/promises';
import { EOL } from 'os';
import * as path from 'path';
import { releaseTagToNameAndVersion } from './release_tag_to_name_and_version.js';

/**
*
Expand Down Expand Up @@ -64,7 +65,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}`;
};

/**
Expand All @@ -81,7 +82,7 @@ export class GitClient {

checkout = async (ref: string, paths: string[] = []) => {
const additionalArgs = paths.length > 0 ? ['--', ...paths] : [];
await execa('git', ['checkout', ref, ...additionalArgs]);
await this.execWithIO`git checkout ${ref} ${additionalArgs}`;
};

status = async () => {
Expand Down Expand Up @@ -134,16 +135,11 @@ export class GitClient {
await this.validateReleaseCommitHash(releaseCommitHash);
const releaseTags = await this.getTagsAtCommit(releaseCommitHash);

/**
* Local function to convert a release tag string to just the package name
* Release tags are formatted as <packageName>@<version>. For example: @aws-amplify/[email protected]
*/
const releaseTagToPackageName = (releaseTag: string) =>
releaseTag.slice(0, releaseTag.lastIndexOf('@'));

// create a set of just the package names (strip off the version suffix) associated with this release commit
const packageNamesRemaining = new Set(
releaseTags.map(releaseTagToPackageName)
releaseTags
.map(releaseTagToNameAndVersion)
.map((nameAndVersion) => nameAndVersion.packageName)
);

// initialize the release commit cursor to the commit of the release before the input releaseCommitHash
Expand All @@ -162,7 +158,7 @@ export class GitClient {
releaseCommitCursor
);
releaseTagsAtCursor.forEach((releaseTag) => {
const packageName = releaseTagToPackageName(releaseTag);
const { packageName } = releaseTagToNameAndVersion(releaseTag);
if (packageNamesRemaining.has(packageName)) {
// this means we've found the previous version of "packageNameRemaining" that was released in releaseCommitHash
// so we add it to the return list and remove it from the search set
Expand Down
15 changes: 13 additions & 2 deletions scripts/components/npm_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ import { $ as chainableExeca } from 'execa';
import { writeFile } from 'fs/promises';
import { EOL } from 'os';

/**
* Type for the response of `npm show <package> --json`
* We can add to this as we need to access additional config in a type-safe way
*/
export type PackageInfo = {
// we have to match the output payload of npm show
// eslint-disable-next-line @typescript-eslint/naming-convention
'dist-tags': Record<string, string>;
deprecated?: string;
};

/**
*
*/
Expand Down Expand Up @@ -43,10 +54,10 @@ export class NpmClient {
.execWithIO`npm dist-tag add ${packageVersionSpecifier} ${distTag}`;
};

getPackageVersionInfo = async (packageVersionSpecifier: string) => {
getPackageInfo = async (packageVersionSpecifier: string) => {
const { stdout: jsonString } = await this
.exec`npm show ${packageVersionSpecifier} --json`;
return JSON.parse(jsonString) as Record<string, unknown>;
return JSON.parse(jsonString) as PackageInfo;
};

init = async () => {
Expand Down
73 changes: 55 additions & 18 deletions scripts/components/release_lifecycle_manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeEach, describe, it } from 'node:test';
import { beforeEach, describe, it, mock } from 'node:test';
import { mkdir, writeFile } from 'fs/promises';
import { randomUUID } from 'crypto';
import { EOL, tmpdir } from 'os';
Expand All @@ -12,6 +12,8 @@ import {
} from './package-json/package_json.js';
import { runVersion } from '../version_runner.js';
import { runPublish } from '../publish_runner.js';
import { ReleaseLifecycleManager } from './release_lifecycle_manager.js';
import { GithubClient } from './github_client.js';

/**
* This test suite is more of an integration test than a unit test.
Expand All @@ -21,6 +23,11 @@ import { runPublish } from '../publish_runner.js';
* Since all of these tests are sharing the same git tree, we're running in serial to avoid conflicts (mostly around duplicate tag names)
*/
void describe('ReleaseLifecycleManager', async () => {
let gitClient: GitClient;
let npmClient: NpmClient;

let cantaloupePackageName: string;
let platypusPackageName: string;
// before(async () => {
// await import('../start_npm_proxy.js');
// });
Expand All @@ -29,6 +36,20 @@ void describe('ReleaseLifecycleManager', async () => {
// await import('../stop_npm_proxy.js');
// });

/**
* This setup initializes a "sandbox" git repo that has a js mono repo with 2 packages, cantaloupe and platypus
* 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:
*
* minor bump of cantaloupe only ● <- HEAD, [email protected], cantaloupe@latest
* |
* minor bump of cantaloupe and platypus ● <- [email protected], [email protected], platypus@latest
* |
* minor bump of cantaloupe and platypus ● <- [email protected], [email protected]
* |
* initial commit ● <- [email protected], [email protected]
*
*/
beforeEach(async ({ name: testName }) => {
// create temp dir
const shortId = randomUUID().split('-')[0];
Expand All @@ -40,31 +61,31 @@ void describe('ReleaseLifecycleManager', async () => {
await mkdir(testWorkingDir);
console.log(testWorkingDir);

const gitClient = new GitClient(testWorkingDir);
const npmClient = new NpmClient(null, testWorkingDir);
gitClient = new GitClient(testWorkingDir);
npmClient = new NpmClient(null, testWorkingDir);

const $ = chainableExeca({ stdio: 'inherit', cwd: testWorkingDir });
const runVersionInTestDir = () => runVersion([], testWorkingDir);
const runPublishInTestDir = () =>
runPublish({ useLocalRegistry: true }, testWorkingDir);

// converting to lowercase because npm init creates packages with all lowercase
const packageABaseName =
`${testNameNormalized}-package-a-${shortId}`.toLocaleLowerCase();
const packageBBaseName =
`${testNameNormalized}-package-b-${shortId}`.toLocaleLowerCase();
cantaloupePackageName =
`${testNameNormalized}-cantaloupe-${shortId}`.toLocaleLowerCase();
platypusPackageName =
`${testNameNormalized}-platypus-${shortId}`.toLocaleLowerCase();

await gitClient.init();
await npmClient.init();

await npmClient.initWorkspacePackage(packageABaseName);
await npmClient.initWorkspacePackage(cantaloupePackageName);
await setPackageToPublic(
path.join(testWorkingDir, 'packages', packageABaseName)
path.join(testWorkingDir, 'packages', cantaloupePackageName)
);

await npmClient.initWorkspacePackage(packageBBaseName);
await npmClient.initWorkspacePackage(platypusPackageName);
await setPackageToPublic(
path.join(testWorkingDir, 'packages', packageBBaseName)
path.join(testWorkingDir, 'packages', platypusPackageName)
);

await npmClient.install(['@changesets/cli']);
Expand All @@ -76,32 +97,50 @@ void describe('ReleaseLifecycleManager', async () => {
await commitVersionBumpChangeset(
testWorkingDir,
gitClient,
[packageABaseName, packageBBaseName],
[cantaloupePackageName, platypusPackageName],
'minor'
);
await runVersionInTestDir();
await gitClient.commitAllChanges('Version Packages (first release)');
await runPublishInTestDir();

await commitVersionBumpChangeset(
testWorkingDir,
gitClient,
[packageABaseName, packageBBaseName],
[cantaloupePackageName, platypusPackageName],
'minor'
);
await runVersionInTestDir();
await gitClient.commitAllChanges('Version Packages (second release)');
await runPublishInTestDir();

await commitVersionBumpChangeset(
testWorkingDir,
gitClient,
[packageABaseName],
[cantaloupePackageName],
'minor'
);
await runVersionInTestDir();
await gitClient.commitAllChanges('Version Packages (third release)');
await runPublishInTestDir();
});

void it('dummy test', () => {});
void it('dummy test', async () => {
const githubClient = new GithubClient('garbage');
mock.method(githubClient, 'createPr', async () => ({ prUrl: 'testPrUrl' }));
mock.method(gitClient, 'push', async () => {});
const releaseLifecycleManager = new ReleaseLifecycleManager(
'HEAD',
githubClient,
gitClient,
npmClient
);
await releaseLifecycleManager.deprecateRelease('cantaloupe is rotten');

// expect latest of cantaloupe to point back to 1.2.0 and 1.3.0 to be marked deprecated
// platypus should not be modified
// const await npmClient.getPackageInfo(cantaloupePackageName);
});
});

const setPackageToPublic = async (packagePath: string) => {
Expand All @@ -118,9 +157,7 @@ const commitVersionBumpChangeset = async (
packageNames: string[],
bump: VersionBump
) => {
const message = `${bump} version bump for packages ${packageNames.join(
', '
)}`;
const message = `${bump} bump for ${packageNames.join(', ')}`;
await writeFile(
path.join(projectPath, '.changeset', `${randomUUID()}.md`),
getChangesetContent(packageNames, bump, message)
Expand Down
75 changes: 49 additions & 26 deletions scripts/components/release_lifecycle_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { GitClient } from './git_client.js';
import { NpmClient } from './npm_client.js';
import { getDistTagFromReleaseTag } from './get_dist_tag_from_release_tag.js';
import { GithubClient } from './github_client.js';
import { releaseTagToNameAndVersion } from './release_tag_to_name_and_version.js';

type DeprecationAction = {
releaseTagToDeprecate: string;
previousReleaseTag: string;
distTagsToMove: string[];
};

/**
*
Expand Down Expand Up @@ -43,13 +50,37 @@ export class ReleaseLifecycleManager {

// if this deprecation is starting from HEAD, we are deprecating the most recent release and need to point dist-tags back to their previous state
// if we are deprecating a past release, then the dist-tags have moved on to newer versions and we do not need to reset them
const releaseTagsToRestoreDistTagPointers =
const previousReleaseTags =
this.gitRefToStartReleaseSearchFrom === 'HEAD'
? await this.gitClient.getPreviousReleaseTags(
releaseCommitHashToDeprecate
)
: [];

const deprecationActions: DeprecationAction[] = [];

for (const releaseTag of releaseTagsToDeprecate) {
const { version: versionBeingDeprecated, packageName } =
releaseTagToNameAndVersion(releaseTag);
const deprecationAction: DeprecationAction = {
releaseTagToDeprecate: releaseTag,
previousReleaseTag: previousReleaseTags.find((prevTag) =>
prevTag.includes(packageName)
)!, // this is safe because gitClient.getPreviousReleaseTags already ensures that we have found a previous version for all packages
distTagsToMove: [],
};
const { ['dist-tags']: distTags } = await this.npmClient.getPackageInfo(
releaseTag
);
Object.entries(distTags).forEach(([tagName, versionAtTag]) => {
// if this tag points to the version being deprecated, add that tag to the list of tags to move to the previous version
if (versionAtTag === versionBeingDeprecated) {
deprecationAction.distTagsToMove.push(tagName);
}
});
deprecationActions.push(deprecationAction);
}

// first create the changeset revert PR
// this PR restores the changeset files that were part of the release but does NOT revert the package.json and changelog changes
const prBranch = `revert_changeset/${releaseCommitHashToDeprecate}`;
Expand All @@ -74,35 +105,28 @@ export class ReleaseLifecycleManager {

console.log(`Created deprecation PR at ${prUrl}`);

if (releaseTagsToRestoreDistTagPointers.length > 0) {
console.log(
`Pointing dist-tags back to previous versions:${EOL}${releaseTagsToRestoreDistTagPointers.join(
EOL
)}${EOL}`
);
}

console.log(
`Deprecating package versions:${EOL}${releaseTagsToDeprecate.join(
EOL
)}${EOL}`
);
console.log(JSON.stringify(deprecationActions, null, 2));

// if anything fails before this point, we haven't actually modified anything on NPM yet.
// now we actually update the npm dist tags and mark the packages as deprecated

for (const releaseTag of releaseTagsToRestoreDistTagPointers) {
const distTag = getDistTagFromReleaseTag(releaseTag);
console.log(
`Restoring dist tag "${distTag}" to package version ${releaseTag}`
for (const {
distTagsToMove,
previousReleaseTag,
releaseTagToDeprecate,
} of deprecationActions) {
for (const distTagToMove of distTagsToMove) {
console.log(
`Restoring dist tag "${distTagToMove}" to package version ${previousReleaseTag}`
);
await this.npmClient.setDistTag(previousReleaseTag, distTagToMove);
console.log(`Done!${EOL}`);
}
console.log(`Deprecating package version ${releaseTagToDeprecate}`);
await this.npmClient.deprecatePackage(
releaseTagToDeprecate,
deprecationMessage
);
await this.npmClient.setDistTag(releaseTag, distTag);
console.log(`Done!${EOL}`);
}

for (const releaseTag of releaseTagsToDeprecate) {
console.log(`Deprecating package version ${releaseTag}`);
await this.npmClient.deprecatePackage(releaseTag, deprecationMessage);
console.log(`Done!${EOL}`);
}
};
Expand Down Expand Up @@ -197,6 +221,5 @@ export class ReleaseLifecycleManager {
The release deprecation workflow requires a clean working tree to create the rollback PR.
`);
}
await this.npmClient.configureNpmRc();
};
}
10 changes: 10 additions & 0 deletions scripts/components/release_tag_to_name_and_version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Splits a release tag of <packageName>@<version> into its constituent parts
*/
export const releaseTagToNameAndVersion = (releaseTag: string) => {
const indexOfLastAt = releaseTag.lastIndexOf('@');
return {
packageName: releaseTag.slice(0, indexOfLastAt),
version: releaseTag.slice(indexOfLastAt + 1),
};
};
11 changes: 9 additions & 2 deletions scripts/deprecate_release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ const deprecationMessage = getInput('deprecationMessage', {
const searchForReleaseStartingFrom = getInput('searchForReleaseStartingFrom', {
required: true,
});
const useNpmRegistry = getInput('useNpmRegistry', { required: true });
const useNpmRegistry =
getInput('useNpmRegistry', { required: true }) === 'true';

if (useNpmRegistry) {
console.log(
Expand All @@ -22,11 +23,17 @@ if (useNpmRegistry) {
);
}

const npmClient = new NpmClient(
useNpmRegistry ? loadNpmTokenFromEnvVar() : null
);

await npmClient.configureNpmRc();

const releaseLifecycleManager = new ReleaseLifecycleManager(
searchForReleaseStartingFrom,
new GithubClient(),
new GitClient(),
new NpmClient(useNpmRegistry ? loadNpmTokenFromEnvVar() : null)
npmClient
);

try {
Expand Down
Loading

0 comments on commit 72679d6

Please sign in to comment.