mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
build: allow recovering from failed publish script (#14906)
* build: fix version bumps not detected in release script * build: allow recovering from failed publish script * Currently if anything fails after the creating the local git tag, it's not possible to just re-run the release script because the script fails with an error saying that the local tag already exists. We can be smart about this and just use the existing tag if it refers to the expected SHA. * Additionally if anything fails after pushing the release tag to the remote, it's not possible to re-run the release script because the tag exists remotely. We can be smart about this and just use the remote tag if it refers to the expected SHA.
This commit is contained in:
committed by
vivian-hu
parent
88601fa51d
commit
1f3324ec1f
@@ -1,4 +1,4 @@
|
||||
import {spawnSync} from 'child_process';
|
||||
import {spawnSync, SpawnSyncReturns} from 'child_process';
|
||||
|
||||
/**
|
||||
* Class that can be used to execute Git commands within a given project directory.
|
||||
@@ -10,65 +10,89 @@ export class GitClient {
|
||||
|
||||
constructor(public projectDir: string, public remoteGitUrl: string) {}
|
||||
|
||||
/**
|
||||
* Spawns a child process running Git. The "stderr" output is inherited and will be printed
|
||||
* in case of errors. This makes it easier to debug failed commands.
|
||||
*/
|
||||
private _spawnGitProcess(args: string[]): SpawnSyncReturns<string> {
|
||||
return spawnSync('git', args, {
|
||||
cwd: this.projectDir,
|
||||
stdio: ['pipe', 'pipe', 'inherit'],
|
||||
encoding: 'utf8',
|
||||
});
|
||||
}
|
||||
|
||||
/** Gets the currently checked out branch for the project directory. */
|
||||
getCurrentBranch() {
|
||||
return spawnSync('git', ['symbolic-ref', '--short', 'HEAD'], {cwd: this.projectDir})
|
||||
.stdout.toString().trim();
|
||||
return this._spawnGitProcess(['symbolic-ref', '--short', 'HEAD']).stdout.trim();
|
||||
}
|
||||
|
||||
/** Gets the commit SHA for the specified remote repository branch. */
|
||||
getRemoteCommitSha(branchName: string): string {
|
||||
return spawnSync('git', ['ls-remote', this.remoteGitUrl, '-h', `refs/heads/${branchName}`],
|
||||
{cwd: this.projectDir}).stdout.toString().split('\t')[0].trim();
|
||||
return this._spawnGitProcess(['ls-remote', this.remoteGitUrl, '-h',
|
||||
`refs/heads/${branchName}`])
|
||||
.stdout.split('\t')[0].trim();
|
||||
}
|
||||
|
||||
/** Gets the latest commit SHA for the specified git reference. */
|
||||
getLocalCommitSha(refName: string) {
|
||||
return spawnSync('git', ['rev-parse', refName], {cwd: this.projectDir})
|
||||
.stdout.toString().trim();
|
||||
return this._spawnGitProcess(['rev-parse', refName]).stdout.trim();
|
||||
}
|
||||
|
||||
/** Gets whether the current Git repository has uncommitted changes. */
|
||||
hasUncommittedChanges(): boolean {
|
||||
return spawnSync('git', ['diff-index', '--quiet', 'HEAD'], {cwd: this.projectDir}).status !== 0;
|
||||
return this._spawnGitProcess(['diff-index', '--quiet', 'HEAD']).status !== 0;
|
||||
}
|
||||
|
||||
/** Checks out an existing branch with the specified name. */
|
||||
checkoutBranch(branchName: string): boolean {
|
||||
return spawnSync('git', ['checkout', branchName], {cwd: this.projectDir}).status === 0;
|
||||
return this._spawnGitProcess(['checkout', branchName]).status === 0;
|
||||
}
|
||||
|
||||
/** Creates a new branch which is based on the previous active branch. */
|
||||
checkoutNewBranch(branchName: string): boolean {
|
||||
return spawnSync('git', ['checkout', '-b', branchName], {cwd: this.projectDir}).status === 0;
|
||||
return this._spawnGitProcess(['checkout', '-b', branchName]).status === 0;
|
||||
}
|
||||
|
||||
/** Stages all changes by running `git add -A`. */
|
||||
stageAllChanges(): boolean {
|
||||
return spawnSync('git', ['add', '-A'], {cwd: this.projectDir}).status === 0;
|
||||
return this._spawnGitProcess(['add', '-A']).status === 0;
|
||||
}
|
||||
|
||||
/** Creates a new commit within the current branch with the given commit message. */
|
||||
createNewCommit(message: string): boolean {
|
||||
return spawnSync('git', ['commit', '-m', message], {cwd: this.projectDir}).status === 0;
|
||||
return this._spawnGitProcess(['commit', '-m', message]).status === 0;
|
||||
}
|
||||
|
||||
/** Gets the title of a specified commit reference. */
|
||||
getCommitTitle(commitRef: string): string {
|
||||
return spawnSync('git', ['log', '-n1', '--format', '%s', commitRef], {cwd: this.projectDir})
|
||||
.stdout.toString().trim();
|
||||
return this._spawnGitProcess(['log', '-n1', '--format="%s"', commitRef]).stdout.trim();
|
||||
}
|
||||
|
||||
/** Creates a tag for the specified commit reference. */
|
||||
createTag(commitRef: string, tagName: string, message: string): boolean {
|
||||
return spawnSync('git', ['tag', tagName, '-m', message], {cwd: this.projectDir}).status === 0;
|
||||
return this._spawnGitProcess(['tag', tagName, '-m', message]).status === 0;
|
||||
}
|
||||
|
||||
/** Checks whether the specified tag exists locally. */
|
||||
hasLocalTag(tagName: string) {
|
||||
return this._spawnGitProcess(['rev-parse', `refs/tags/${tagName}`]).status === 0;
|
||||
}
|
||||
|
||||
/** Gets the Git SHA of the specified local tag. */
|
||||
getShaOfLocalTag(tagName: string) {
|
||||
return this._spawnGitProcess(['rev-parse', `refs/tags/${tagName}`]).stdout.trim();
|
||||
}
|
||||
|
||||
/** Gets the Git SHA of the specified remote tag. */
|
||||
getShaOfRemoteTag(tagName: string): string {
|
||||
return this._spawnGitProcess(['ls-remote', this.remoteGitUrl, '-t', `refs/tags/${tagName}`])
|
||||
.stdout.split('\t')[0].trim();
|
||||
}
|
||||
|
||||
/** Pushes the specified tag to the remote git repository. */
|
||||
pushTagToRemote(tagName: string): boolean {
|
||||
return spawnSync('git', ['push', this.remoteGitUrl, `refs/tags/${tagName}`], {
|
||||
cwd: this.projectDir
|
||||
}).status === 0;
|
||||
return this._spawnGitProcess(['push', this.remoteGitUrl, `refs/tags/${tagName}`]).status === 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,8 @@ class PublishReleaseTask extends BaseReleaseTask {
|
||||
}
|
||||
|
||||
// Create and push the release tag before publishing to NPM.
|
||||
this.createAndPushReleaseTag(newVersionName, releaseNotes);
|
||||
this.createReleaseTag(newVersionName, releaseNotes);
|
||||
this.pushReleaseTag(newVersionName);
|
||||
|
||||
// Ensure that we are authenticated before running "npm publish" for each package.
|
||||
this.checkNpmAuthentication();
|
||||
@@ -158,7 +159,6 @@ class PublishReleaseTask extends BaseReleaseTask {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prompts the user whether he is sure that the script should continue publishing
|
||||
* the release to NPM.
|
||||
@@ -221,15 +221,45 @@ class PublishReleaseTask extends BaseReleaseTask {
|
||||
console.info(green(` ✓ Successfully published "${packageName}"`));
|
||||
}
|
||||
|
||||
/** Creates a specified tag and pushes it to the remote repository */
|
||||
private createAndPushReleaseTag(tagName: string, releaseNotes: string) {
|
||||
if (!this.git.createTag('HEAD', tagName, releaseNotes)) {
|
||||
/** Creates the specified release tag locally. */
|
||||
private createReleaseTag(tagName: string, releaseNotes: string) {
|
||||
if (this.git.hasLocalTag(tagName)) {
|
||||
const expectedSha = this.git.getLocalCommitSha('HEAD');
|
||||
|
||||
if (this.git.getShaOfLocalTag(tagName) !== expectedSha) {
|
||||
console.error(red(` ✘ Tag "${tagName}" already exists locally, but does not refer ` +
|
||||
`to the version bump commit. Please delete the tag if you want to proceed.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.info(green(` ✓ Release tag already exists: "${italic(tagName)}"`));
|
||||
} else if (this.git.createTag('HEAD', tagName, releaseNotes)) {
|
||||
console.info(green(` ✓ Created release tag: "${italic(tagName)}"`));
|
||||
} else {
|
||||
console.error(red(` ✘ Could not create the "${tagName}" tag.`));
|
||||
console.error(red(` Please make sure there is no existing tag with the same name.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.info(green(` ✓ Created release tag: "${italic(tagName)}"`));
|
||||
}
|
||||
|
||||
/** Pushes the release tag to the remote repository. */
|
||||
private pushReleaseTag(tagName: string) {
|
||||
const remoteTagSha = this.git.getShaOfRemoteTag(tagName);
|
||||
const expectedSha = this.git.getLocalCommitSha('HEAD');
|
||||
|
||||
// The remote tag SHA is empty if the tag does not exist in the remote repository.
|
||||
if (remoteTagSha) {
|
||||
if (remoteTagSha !== expectedSha) {
|
||||
console.error(red(` ✘ Tag "${tagName}" already exists on the remote, but does not ` +
|
||||
`refer to the version bump commit.`));
|
||||
console.error(red(` Please delete the tag on the remote if you want to proceed.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.info(green(` ✓ Release tag already exists remotely: "${italic(tagName)}"`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.git.pushTagToRemote(tagName)) {
|
||||
console.error(red(` ✘ Could not push the "${tagName} "tag upstream.`));
|
||||
|
||||
Reference in New Issue
Block a user