From e6b8ef96794d13a9b8915cfbe578e039dbac1fca Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Mon, 21 Jan 2019 18:16:35 +0100 Subject: [PATCH 1/2] build: fix version bumps not detected in release script --- tools/release/git/git-client.ts | 44 +++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/tools/release/git/git-client.ts b/tools/release/git/git-client.ts index 9ee640afd14b..6048299e0d72 100644 --- a/tools/release/git/git-client.ts +++ b/tools/release/git/git-client.ts @@ -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,73 @@ 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 { + 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; } /** 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; } } From 180fa28d707315eaec22be965a15f3342febe7af Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Mon, 21 Jan 2019 19:31:43 +0100 Subject: [PATCH 2/2] 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. --- tools/release/git/git-client.ts | 16 ++++++++++++ tools/release/publish-release.ts | 42 +++++++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/tools/release/git/git-client.ts b/tools/release/git/git-client.ts index 6048299e0d72..e5d668174184 100644 --- a/tools/release/git/git-client.ts +++ b/tools/release/git/git-client.ts @@ -74,6 +74,22 @@ export class GitClient { 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 this._spawnGitProcess(['push', this.remoteGitUrl, `refs/tags/${tagName}`]).status === 0; diff --git a/tools/release/publish-release.ts b/tools/release/publish-release.ts index f3180ffcc2f5..ed10d31fcd5d 100644 --- a/tools/release/publish-release.ts +++ b/tools/release/publish-release.ts @@ -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.`));