Skip to content

Commit 1f3324e

Browse files
devversionvivian-hu-zz
authored andcommitted
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.
1 parent 88601fa commit 1f3324e

File tree

2 files changed

+78
-24
lines changed

2 files changed

+78
-24
lines changed

tools/release/git/git-client.ts

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {spawnSync} from 'child_process';
1+
import {spawnSync, SpawnSyncReturns} from 'child_process';
22

33
/**
44
* Class that can be used to execute Git commands within a given project directory.
@@ -10,65 +10,89 @@ export class GitClient {
1010

1111
constructor(public projectDir: string, public remoteGitUrl: string) {}
1212

13+
/**
14+
* Spawns a child process running Git. The "stderr" output is inherited and will be printed
15+
* in case of errors. This makes it easier to debug failed commands.
16+
*/
17+
private _spawnGitProcess(args: string[]): SpawnSyncReturns<string> {
18+
return spawnSync('git', args, {
19+
cwd: this.projectDir,
20+
stdio: ['pipe', 'pipe', 'inherit'],
21+
encoding: 'utf8',
22+
});
23+
}
24+
1325
/** Gets the currently checked out branch for the project directory. */
1426
getCurrentBranch() {
15-
return spawnSync('git', ['symbolic-ref', '--short', 'HEAD'], {cwd: this.projectDir})
16-
.stdout.toString().trim();
27+
return this._spawnGitProcess(['symbolic-ref', '--short', 'HEAD']).stdout.trim();
1728
}
1829

1930
/** Gets the commit SHA for the specified remote repository branch. */
2031
getRemoteCommitSha(branchName: string): string {
21-
return spawnSync('git', ['ls-remote', this.remoteGitUrl, '-h', `refs/heads/${branchName}`],
22-
{cwd: this.projectDir}).stdout.toString().split('\t')[0].trim();
32+
return this._spawnGitProcess(['ls-remote', this.remoteGitUrl, '-h',
33+
`refs/heads/${branchName}`])
34+
.stdout.split('\t')[0].trim();
2335
}
2436

2537
/** Gets the latest commit SHA for the specified git reference. */
2638
getLocalCommitSha(refName: string) {
27-
return spawnSync('git', ['rev-parse', refName], {cwd: this.projectDir})
28-
.stdout.toString().trim();
39+
return this._spawnGitProcess(['rev-parse', refName]).stdout.trim();
2940
}
3041

3142
/** Gets whether the current Git repository has uncommitted changes. */
3243
hasUncommittedChanges(): boolean {
33-
return spawnSync('git', ['diff-index', '--quiet', 'HEAD'], {cwd: this.projectDir}).status !== 0;
44+
return this._spawnGitProcess(['diff-index', '--quiet', 'HEAD']).status !== 0;
3445
}
3546

3647
/** Checks out an existing branch with the specified name. */
3748
checkoutBranch(branchName: string): boolean {
38-
return spawnSync('git', ['checkout', branchName], {cwd: this.projectDir}).status === 0;
49+
return this._spawnGitProcess(['checkout', branchName]).status === 0;
3950
}
4051

4152
/** Creates a new branch which is based on the previous active branch. */
4253
checkoutNewBranch(branchName: string): boolean {
43-
return spawnSync('git', ['checkout', '-b', branchName], {cwd: this.projectDir}).status === 0;
54+
return this._spawnGitProcess(['checkout', '-b', branchName]).status === 0;
4455
}
4556

4657
/** Stages all changes by running `git add -A`. */
4758
stageAllChanges(): boolean {
48-
return spawnSync('git', ['add', '-A'], {cwd: this.projectDir}).status === 0;
59+
return this._spawnGitProcess(['add', '-A']).status === 0;
4960
}
5061

5162
/** Creates a new commit within the current branch with the given commit message. */
5263
createNewCommit(message: string): boolean {
53-
return spawnSync('git', ['commit', '-m', message], {cwd: this.projectDir}).status === 0;
64+
return this._spawnGitProcess(['commit', '-m', message]).status === 0;
5465
}
5566

5667
/** Gets the title of a specified commit reference. */
5768
getCommitTitle(commitRef: string): string {
58-
return spawnSync('git', ['log', '-n1', '--format', '%s', commitRef], {cwd: this.projectDir})
59-
.stdout.toString().trim();
69+
return this._spawnGitProcess(['log', '-n1', '--format="%s"', commitRef]).stdout.trim();
6070
}
6171

6272
/** Creates a tag for the specified commit reference. */
6373
createTag(commitRef: string, tagName: string, message: string): boolean {
64-
return spawnSync('git', ['tag', tagName, '-m', message], {cwd: this.projectDir}).status === 0;
74+
return this._spawnGitProcess(['tag', tagName, '-m', message]).status === 0;
75+
}
76+
77+
/** Checks whether the specified tag exists locally. */
78+
hasLocalTag(tagName: string) {
79+
return this._spawnGitProcess(['rev-parse', `refs/tags/${tagName}`]).status === 0;
80+
}
81+
82+
/** Gets the Git SHA of the specified local tag. */
83+
getShaOfLocalTag(tagName: string) {
84+
return this._spawnGitProcess(['rev-parse', `refs/tags/${tagName}`]).stdout.trim();
85+
}
86+
87+
/** Gets the Git SHA of the specified remote tag. */
88+
getShaOfRemoteTag(tagName: string): string {
89+
return this._spawnGitProcess(['ls-remote', this.remoteGitUrl, '-t', `refs/tags/${tagName}`])
90+
.stdout.split('\t')[0].trim();
6591
}
6692

6793
/** Pushes the specified tag to the remote git repository. */
6894
pushTagToRemote(tagName: string): boolean {
69-
return spawnSync('git', ['push', this.remoteGitUrl, `refs/tags/${tagName}`], {
70-
cwd: this.projectDir
71-
}).status === 0;
95+
return this._spawnGitProcess(['push', this.remoteGitUrl, `refs/tags/${tagName}`]).status === 0;
7296
}
7397
}
7498

tools/release/publish-release.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ class PublishReleaseTask extends BaseReleaseTask {
100100
}
101101

102102
// Create and push the release tag before publishing to NPM.
103-
this.createAndPushReleaseTag(newVersionName, releaseNotes);
103+
this.createReleaseTag(newVersionName, releaseNotes);
104+
this.pushReleaseTag(newVersionName);
104105

105106
// Ensure that we are authenticated before running "npm publish" for each package.
106107
this.checkNpmAuthentication();
@@ -158,7 +159,6 @@ class PublishReleaseTask extends BaseReleaseTask {
158159
}
159160
}
160161

161-
162162
/**
163163
* Prompts the user whether he is sure that the script should continue publishing
164164
* the release to NPM.
@@ -221,15 +221,45 @@ class PublishReleaseTask extends BaseReleaseTask {
221221
console.info(green(` ✓ Successfully published "${packageName}"`));
222222
}
223223

224-
/** Creates a specified tag and pushes it to the remote repository */
225-
private createAndPushReleaseTag(tagName: string, releaseNotes: string) {
226-
if (!this.git.createTag('HEAD', tagName, releaseNotes)) {
224+
/** Creates the specified release tag locally. */
225+
private createReleaseTag(tagName: string, releaseNotes: string) {
226+
if (this.git.hasLocalTag(tagName)) {
227+
const expectedSha = this.git.getLocalCommitSha('HEAD');
228+
229+
if (this.git.getShaOfLocalTag(tagName) !== expectedSha) {
230+
console.error(red(` ✘ Tag "${tagName}" already exists locally, but does not refer ` +
231+
`to the version bump commit. Please delete the tag if you want to proceed.`));
232+
process.exit(1);
233+
}
234+
235+
console.info(green(` ✓ Release tag already exists: "${italic(tagName)}"`));
236+
} else if (this.git.createTag('HEAD', tagName, releaseNotes)) {
237+
console.info(green(` ✓ Created release tag: "${italic(tagName)}"`));
238+
} else {
227239
console.error(red(` ✘ Could not create the "${tagName}" tag.`));
228240
console.error(red(` Please make sure there is no existing tag with the same name.`));
229241
process.exit(1);
230242
}
231243

232-
console.info(green(` ✓ Created release tag: "${italic(tagName)}"`));
244+
}
245+
246+
/** Pushes the release tag to the remote repository. */
247+
private pushReleaseTag(tagName: string) {
248+
const remoteTagSha = this.git.getShaOfRemoteTag(tagName);
249+
const expectedSha = this.git.getLocalCommitSha('HEAD');
250+
251+
// The remote tag SHA is empty if the tag does not exist in the remote repository.
252+
if (remoteTagSha) {
253+
if (remoteTagSha !== expectedSha) {
254+
console.error(red(` ✘ Tag "${tagName}" already exists on the remote, but does not ` +
255+
`refer to the version bump commit.`));
256+
console.error(red(` Please delete the tag on the remote if you want to proceed.`));
257+
process.exit(1);
258+
}
259+
260+
console.info(green(` ✓ Release tag already exists remotely: "${italic(tagName)}"`));
261+
return;
262+
}
233263

234264
if (!this.git.pushTagToRemote(tagName)) {
235265
console.error(red(` ✘ Could not push the "${tagName} "tag upstream.`));

0 commit comments

Comments
 (0)