Skip to content

Commit fe7f95e

Browse files
devversionjelbourn
authored andcommitted
build: add script to publish release (#14565)
Closes #13455
1 parent 5c95993 commit fe7f95e

File tree

11 files changed

+561
-87
lines changed

11 files changed

+561
-87
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"breaking-changes": "gulp breaking-changes",
2525
"gulp": "gulp",
2626
"stage-release": "ts-node --project tools/release/ tools/release/stage-release.ts",
27+
"publish-release": "ts-node --project tools/release/ tools/release/publish-release.ts",
2728
"preinstall": "node ./tools/npm/check-npm.js"
2829
},
2930
"version": "7.2.0",

tools/release/base-release-task.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {green, italic, red, yellow} from 'chalk';
2+
import {prompt} from 'inquirer';
3+
import {GitClient} from './git/git-client';
4+
import {Version} from './version-name/parse-version';
5+
import {getAllowedPublishBranches} from './version-name/publish-branches';
6+
7+
/**
8+
* Base release task class that contains shared methods that are commonly used across
9+
* the staging and publish script.
10+
*/
11+
export class BaseReleaseTask {
12+
13+
constructor(public git: GitClient) {}
14+
15+
/** Checks if the user is on an allowed publish branch for the specified version. */
16+
protected switchToPublishBranch(newVersion: Version): string {
17+
const allowedBranches = getAllowedPublishBranches(newVersion);
18+
const currentBranchName = this.git.getCurrentBranch();
19+
20+
// If current branch already matches one of the allowed publish branches, just continue
21+
// by exiting this function and returning the currently used publish branch.
22+
if (allowedBranches.includes(currentBranchName)) {
23+
console.log(green(` ✓ Using the "${italic(currentBranchName)}" branch.`));
24+
return currentBranchName;
25+
}
26+
27+
// In case there are multiple allowed publish branches for this version, we just
28+
// exit and let the user decide which branch they want to release from.
29+
if (allowedBranches.length !== 1) {
30+
console.warn(yellow(' ✘ You are not on an allowed publish branch.'));
31+
console.warn(yellow(` Please switch to one of the following branches: ` +
32+
`${allowedBranches.join(', ')}`));
33+
process.exit(0);
34+
}
35+
36+
// For this version there is only *one* allowed publish branch, so we could
37+
// automatically switch to that branch in case the user isn't on it yet.
38+
const defaultPublishBranch = allowedBranches[0];
39+
40+
if (!this.git.checkoutBranch(defaultPublishBranch)) {
41+
console.error(red(` ✘ Could not switch to the "${italic(defaultPublishBranch)}" ` +
42+
`branch.`));
43+
console.error(red(` Please ensure that the branch exists or manually switch to the ` +
44+
`branch.`));
45+
process.exit(1);
46+
}
47+
48+
console.log(green(` ✓ Switched to the "${italic(defaultPublishBranch)}" branch.`));
49+
}
50+
51+
/** Verifies that the local branch is up to date with the given publish branch. */
52+
protected verifyLocalCommitsMatchUpstream(publishBranch: string) {
53+
const upstreamCommitSha = this.git.getRemoteCommitSha(publishBranch);
54+
const localCommitSha = this.git.getLocalCommitSha('HEAD');
55+
56+
// Check if the current branch is in sync with the remote branch.
57+
if (upstreamCommitSha !== localCommitSha) {
58+
console.error(red(` ✘ The current branch is not in sync with the remote branch. Please ` +
59+
`make sure your local branch "${italic(publishBranch)}" is up to date.`));
60+
process.exit(1);
61+
}
62+
}
63+
64+
/** Verifies that there are no uncommitted changes in the project. */
65+
protected verifyNoUncommittedChanges() {
66+
if (this.git.hasUncommittedChanges()) {
67+
console.error(red(` ✘ There are changes which are not committed and should be ` +
68+
`discarded.`));
69+
process.exit(1);
70+
}
71+
}
72+
73+
/** Prompts the user with a confirmation question and a specified message. */
74+
protected async promptConfirm(message: string): Promise<boolean> {
75+
return (await prompt<{result: boolean}>({
76+
type: 'confirm',
77+
name: 'result',
78+
message: message,
79+
})).result;
80+
}
81+
}

tools/release/git/git-client.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,5 +52,16 @@ export class GitClient {
5252
createNewCommit(message: string): boolean {
5353
return spawnSync('git', ['commit', '-m', message], {cwd: this.projectDir}).status === 0;
5454
}
55+
56+
/** Gets the title of a specified commit reference. */
57+
getCommitTitle(commitRef: string): string {
58+
return spawnSync('git', ['log', '-n1', '--format', '%s', commitRef], {cwd: this.projectDir})
59+
.stdout.toString().trim();
60+
}
61+
62+
/** Creates a tag for the specified commit reference. */
63+
createTag(commitRef: string, tagName: string, message: string): boolean {
64+
return spawnSync('git', ['tag', tagName, '-m', message], {cwd: this.projectDir}).status === 0;
65+
}
5566
}
5667

tools/release/git/github-urls.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22
export function getGithubBranchCommitsUrl(owner: string, repository: string, branchName: string) {
33
return `https://github.com/${owner}/${repository}/commits/${branchName}`;
44
}
5+
6+
/** Gets a Github URL that refers list of releases within the specified repository. */
7+
export function getGithubReleasesUrl(owner: string, repository: string) {
8+
return `https://github.com/${owner}/${repository}/releases`;
9+
}

tools/release/npm/npm-client.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {spawnSync} from 'child_process';
2+
3+
/**
4+
* Process environment that does not refer to Yarn's package registry. Since the scripts are
5+
* usually run through Yarn, we need to update the "npm_config_registry" so that NPM is able to
6+
* properly run "npm login" and "npm publish".
7+
*/
8+
const npmClientEnvironment = {
9+
...process.env,
10+
// See https://docs.npmjs.com/misc/registry for the official documentation of the NPM registry.
11+
npm_config_registry: 'https://registry.npmjs.org',
12+
};
13+
14+
/** Checks whether NPM is currently authenticated. */
15+
export function isNpmAuthenticated(): boolean {
16+
return spawnSync('npm', ['whoami'], {
17+
shell: true,
18+
env: npmClientEnvironment,
19+
}).stdout.toString() !== '';
20+
}
21+
22+
/** Runs "npm login" interactively by piping stdin/stderr/stdout to the current tty. */
23+
export function runInteractiveNpmLogin(): boolean {
24+
return spawnSync('npm', ['login'], {
25+
stdio: 'inherit',
26+
shell: true,
27+
env: npmClientEnvironment,
28+
}).status === 0;
29+
}
30+
31+
/** Runs NPM publish within a specified directory */
32+
export function runNpmPublish(packagePath: string, distTag: string): string | null {
33+
const result = spawnSync('npm', ['publish', '--access', 'public', '--tag', distTag], {
34+
cwd: packagePath,
35+
shell: true,
36+
env: npmClientEnvironment,
37+
});
38+
39+
// We only want to return an error if the exit code is not zero. NPM by default prints the
40+
// logging messages to "stdout" and therefore just checking for "stdout" is not reliable.
41+
if (result.status !== 0) {
42+
return result.stderr.toString();
43+
}
44+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import {prompt} from 'inquirer';
2+
import {Version} from '../version-name/parse-version';
3+
4+
/** Inquirer choice for selecting the "latest" npm dist-tag. */
5+
const LATEST_TAG_CHOICE = {value: 'latest', name: 'Latest'};
6+
7+
/** Inquirer choice for selecting the "next" npm dist-tag. */
8+
const NEXT_TAG_CHOICE = {value: 'next', name: 'Next'};
9+
10+
/**
11+
* Prompts the current user-input interface for a npm dist-tag. The provided npm-dist tag
12+
* will be validated against the specified version and prevents that any pre-releases
13+
* will be published under the "latest" npm dist tag. Read more about conventions for
14+
* NPM dist tags here: https://docs.npmjs.com/cli/dist-tag
15+
*/
16+
export async function promptForNpmDistTag(version: Version): Promise<string> {
17+
const {distTag} = await prompt<{distTag: string}>({
18+
type: 'list',
19+
name: 'distTag',
20+
message: 'What is the NPM dist-tag you want to publish to?',
21+
choices: getDistTagChoicesForVersion(version),
22+
});
23+
24+
return distTag;
25+
}
26+
27+
/**
28+
* Determines all allowed npm dist-tag choices for a specified version. For example,
29+
* a pre-release version should be never published to the "latest" tag.
30+
*/
31+
export function getDistTagChoicesForVersion(version: Version) {
32+
const {prereleaseLabel} = version;
33+
34+
if (!prereleaseLabel) {
35+
return [LATEST_TAG_CHOICE, NEXT_TAG_CHOICE];
36+
}
37+
38+
return [NEXT_TAG_CHOICE];
39+
}
40+

0 commit comments

Comments
 (0)