Skip to content

build: add script to publish release #14565

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"breaking-changes": "gulp breaking-changes",
"gulp": "gulp",
"stage-release": "ts-node --project tools/release/ tools/release/stage-release.ts",
"publish-release": "ts-node --project tools/release/ tools/release/publish-release.ts",
"preinstall": "node ./tools/npm/check-npm.js"
},
"version": "7.2.0",
Expand Down
81 changes: 81 additions & 0 deletions tools/release/base-release-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {green, italic, red, yellow} from 'chalk';
import {prompt} from 'inquirer';
import {GitClient} from './git/git-client';
import {Version} from './version-name/parse-version';
import {getAllowedPublishBranches} from './version-name/publish-branches';

/**
* Base release task class that contains shared methods that are commonly used across
* the staging and publish script.
*/
export class BaseReleaseTask {

constructor(public git: GitClient) {}

/** Checks if the user is on an allowed publish branch for the specified version. */
protected switchToPublishBranch(newVersion: Version): string {
const allowedBranches = getAllowedPublishBranches(newVersion);
const currentBranchName = this.git.getCurrentBranch();

// If current branch already matches one of the allowed publish branches, just continue
// by exiting this function and returning the currently used publish branch.
if (allowedBranches.includes(currentBranchName)) {
console.log(green(` ✓ Using the "${italic(currentBranchName)}" branch.`));
return currentBranchName;
}

// In case there are multiple allowed publish branches for this version, we just
// exit and let the user decide which branch they want to release from.
if (allowedBranches.length !== 1) {
console.warn(yellow(' ✘ You are not on an allowed publish branch.'));
console.warn(yellow(` Please switch to one of the following branches: ` +
`${allowedBranches.join(', ')}`));
process.exit(0);
}

// For this version there is only *one* allowed publish branch, so we could
// automatically switch to that branch in case the user isn't on it yet.
const defaultPublishBranch = allowedBranches[0];

if (!this.git.checkoutBranch(defaultPublishBranch)) {
console.error(red(` ✘ Could not switch to the "${italic(defaultPublishBranch)}" ` +
`branch.`));
console.error(red(` Please ensure that the branch exists or manually switch to the ` +
`branch.`));
process.exit(1);
}

console.log(green(` ✓ Switched to the "${italic(defaultPublishBranch)}" branch.`));
}

/** Verifies that the local branch is up to date with the given publish branch. */
protected verifyLocalCommitsMatchUpstream(publishBranch: string) {
const upstreamCommitSha = this.git.getRemoteCommitSha(publishBranch);
const localCommitSha = this.git.getLocalCommitSha('HEAD');

// Check if the current branch is in sync with the remote branch.
if (upstreamCommitSha !== localCommitSha) {
console.error(red(` ✘ The current branch is not in sync with the remote branch. Please ` +
`make sure your local branch "${italic(publishBranch)}" is up to date.`));
process.exit(1);
}
}

/** Verifies that there are no uncommitted changes in the project. */
protected verifyNoUncommittedChanges() {
if (this.git.hasUncommittedChanges()) {
console.error(red(` ✘ There are changes which are not committed and should be ` +
`discarded.`));
process.exit(1);
}
}

/** Prompts the user with a confirmation question and a specified message. */
protected async promptConfirm(message: string): Promise<boolean> {
return (await prompt<{result: boolean}>({
type: 'confirm',
name: 'result',
message: message,
})).result;
}
}
11 changes: 11 additions & 0 deletions tools/release/git/git-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,16 @@ export class GitClient {
createNewCommit(message: string): boolean {
return spawnSync('git', ['commit', '-m', message], {cwd: this.projectDir}).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();
}

/** 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;
}
}

5 changes: 5 additions & 0 deletions tools/release/git/github-urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
export function getGithubBranchCommitsUrl(owner: string, repository: string, branchName: string) {
return `https://github.com/${owner}/${repository}/commits/${branchName}`;
}

/** Gets a Github URL that refers list of releases within the specified repository. */
export function getGithubReleasesUrl(owner: string, repository: string) {
return `https://github.com/${owner}/${repository}/releases`;
}
44 changes: 44 additions & 0 deletions tools/release/npm/npm-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {spawnSync} from 'child_process';

/**
* Process environment that does not refer to Yarn's package registry. Since the scripts are
* usually run through Yarn, we need to update the "npm_config_registry" so that NPM is able to
* properly run "npm login" and "npm publish".
*/
const npmClientEnvironment = {
...process.env,
// See https://docs.npmjs.com/misc/registry for the official documentation of the NPM registry.
npm_config_registry: 'https://registry.npmjs.org',
};

/** Checks whether NPM is currently authenticated. */
export function isNpmAuthenticated(): boolean {
return spawnSync('npm', ['whoami'], {
shell: true,
env: npmClientEnvironment,
}).stdout.toString() !== '';
}

/** Runs "npm login" interactively by piping stdin/stderr/stdout to the current tty. */
export function runInteractiveNpmLogin(): boolean {
return spawnSync('npm', ['login'], {
stdio: 'inherit',
shell: true,
env: npmClientEnvironment,
}).status === 0;
}

/** Runs NPM publish within a specified directory */
export function runNpmPublish(packagePath: string, distTag: string): string | null {
const result = spawnSync('npm', ['publish', '--access', 'public', '--tag', distTag], {
cwd: packagePath,
shell: true,
env: npmClientEnvironment,
});

// We only want to return an error if the exit code is not zero. NPM by default prints the
// logging messages to "stdout" and therefore just checking for "stdout" is not reliable.
if (result.status !== 0) {
return result.stderr.toString();
}
}
40 changes: 40 additions & 0 deletions tools/release/prompt/npm-dist-tag-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {prompt} from 'inquirer';
import {Version} from '../version-name/parse-version';

/** Inquirer choice for selecting the "latest" npm dist-tag. */
const LATEST_TAG_CHOICE = {value: 'latest', name: 'Latest'};

/** Inquirer choice for selecting the "next" npm dist-tag. */
const NEXT_TAG_CHOICE = {value: 'next', name: 'Next'};

/**
* Prompts the current user-input interface for a npm dist-tag. The provided npm-dist tag
* will be validated against the specified version and prevents that any pre-releases
* will be published under the "latest" npm dist tag. Read more about conventions for
* NPM dist tags here: https://docs.npmjs.com/cli/dist-tag
*/
export async function promptForNpmDistTag(version: Version): Promise<string> {
const {distTag} = await prompt<{distTag: string}>({
type: 'list',
name: 'distTag',
message: 'What is the NPM dist-tag you want to publish to?',
choices: getDistTagChoicesForVersion(version),
});

return distTag;
}

/**
* Determines all allowed npm dist-tag choices for a specified version. For example,
* a pre-release version should be never published to the "latest" tag.
*/
export function getDistTagChoicesForVersion(version: Version) {
const {prereleaseLabel} = version;

if (!prereleaseLabel) {
return [LATEST_TAG_CHOICE, NEXT_TAG_CHOICE];
}

return [NEXT_TAG_CHOICE];
}

Loading