mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
committed by
Jeremy Elbourn
parent
5c959932c0
commit
fe7f95ec10
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import {bold, green, italic, red, yellow} from 'chalk';
|
||||
import {execSync} from 'child_process';
|
||||
import {readFileSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {BaseReleaseTask} from './base-release-task';
|
||||
import {GitClient} from './git/git-client';
|
||||
import {getGithubReleasesUrl} from './git/github-urls';
|
||||
import {isNpmAuthenticated, runInteractiveNpmLogin, runNpmPublish} from './npm/npm-client';
|
||||
import {promptForNpmDistTag} from './prompt/npm-dist-tag-prompt';
|
||||
import {checkReleasePackage} from './release-output/check-packages';
|
||||
import {releasePackages} from './release-output/release-packages';
|
||||
import {parseVersionName, Version} from './version-name/parse-version';
|
||||
|
||||
/** Maximum allowed tries to authenticate NPM. */
|
||||
const MAX_NPM_LOGIN_TRIES = 2;
|
||||
|
||||
/**
|
||||
* Class that can be instantiated in order to create a new release. The tasks requires user
|
||||
* interaction/input through command line prompts.
|
||||
*/
|
||||
class PublishReleaseTask extends BaseReleaseTask {
|
||||
|
||||
/** Path to the project package JSON. */
|
||||
packageJsonPath: string;
|
||||
|
||||
/** Serialized package.json of the specified project. */
|
||||
packageJson: any;
|
||||
|
||||
/** Parsed current version of the project. */
|
||||
currentVersion: Version;
|
||||
|
||||
/** Path to the release output of the project. */
|
||||
releaseOutputPath: string;
|
||||
|
||||
/** Instance of a wrapper that can execute Git commands. */
|
||||
git: GitClient;
|
||||
|
||||
constructor(public projectDir: string,
|
||||
public repositoryOwner: string,
|
||||
public repositoryName: string) {
|
||||
super(new GitClient(projectDir,
|
||||
`https://github.com/${repositoryOwner}/${repositoryName}.git`));
|
||||
|
||||
this.packageJsonPath = join(projectDir, 'package.json');
|
||||
this.releaseOutputPath = join(projectDir, 'dist/releases');
|
||||
|
||||
this.packageJson = JSON.parse(readFileSync(this.packageJsonPath, 'utf-8'));
|
||||
this.currentVersion = parseVersionName(this.packageJson.version);
|
||||
|
||||
if (!this.currentVersion) {
|
||||
console.error(red(`Cannot parse current version in ${italic('package.json')}. Please ` +
|
||||
`make sure "${this.packageJson.version}" is a valid Semver version.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async run() {
|
||||
console.log();
|
||||
console.log(green('-----------------------------------------'));
|
||||
console.log(green(bold(' Angular Material release script')));
|
||||
console.log(green('-----------------------------------------'));
|
||||
console.log();
|
||||
|
||||
const newVersion = this.currentVersion;
|
||||
const newVersionName = this.currentVersion.format();
|
||||
|
||||
// Ensure there are no uncommitted changes. Checking this before switching to a
|
||||
// publish branch is sufficient as unstaged changes are not specific to Git branches.
|
||||
this.verifyNoUncommittedChanges();
|
||||
|
||||
// Branch that will be used to build the output for the release of the current version.
|
||||
const publishBranch = this.switchToPublishBranch(newVersion);
|
||||
|
||||
this.verifyLastCommitVersionBump();
|
||||
this.verifyLocalCommitsMatchUpstream(publishBranch);
|
||||
|
||||
const npmDistTag = await promptForNpmDistTag(newVersion);
|
||||
|
||||
// In case the user wants to publish a stable version to the "next" npm tag, we want
|
||||
// to double-check because usually only pre-release's are pushed to that tag.
|
||||
if (npmDistTag === 'next' && !newVersion.prereleaseLabel) {
|
||||
await this.promptStableVersionForNextTag();
|
||||
}
|
||||
|
||||
this.buildReleasePackages();
|
||||
console.info(green(` ✓ Built the release output.`));
|
||||
|
||||
this.checkReleaseOutput();
|
||||
console.info(green(` ✓ Release output passed validation checks.`));
|
||||
|
||||
// TODO(devversion): find a way to extract the changelog part just for this version.
|
||||
this.git.createTag('HEAD', newVersionName, '');
|
||||
console.info(green(` ✓ Created release tag: "${italic(newVersionName)}"`));
|
||||
|
||||
// Ensure that we are authenticated before running "npm publish" for each package.
|
||||
this.checkNpmAuthentication();
|
||||
|
||||
// Just in order to double-check that the user is sure to publish to NPM, we want
|
||||
// the user to interactively confirm that the script should continue.
|
||||
await this.promptConfirmReleasePublish();
|
||||
|
||||
for (let packageName of releasePackages) {
|
||||
this.publishPackageToNpm(packageName, npmDistTag);
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.info(green(bold(` ✓ Published all packages successfully`)));
|
||||
console.info(yellow(` ⚠ Please push the newly created tag to Github and draft a new ` +
|
||||
`release.`));
|
||||
console.info(yellow(
|
||||
` ${getGithubReleasesUrl(this.repositoryOwner, this.repositoryName)}`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the latest commit on the current branch is a version bump from the
|
||||
* staging script.
|
||||
*/
|
||||
private verifyLastCommitVersionBump() {
|
||||
if (!/chore: bump version/.test(this.git.getCommitTitle('HEAD'))) {
|
||||
console.error(red(` ✘ The latest commit of the current branch does not seem to be a ` +
|
||||
`version bump.`));
|
||||
console.error(red(` Please stage the release using the staging script.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds all release packages that should be published. */
|
||||
private buildReleasePackages() {
|
||||
const binDir = join(this.projectDir, 'node_modules/.bin');
|
||||
const spawnOptions = {cwd: binDir, stdio: 'inherit'};
|
||||
|
||||
// TODO(devversion): I'd prefer disabling the output for those, but it might be only
|
||||
// worth if we consider adding some terminal spinner library (like "ora").
|
||||
execSync('gulp clean', spawnOptions);
|
||||
execSync(`gulp ${releasePackages.map(name => `${name}:build-release`).join(' ')}`,
|
||||
spawnOptions);
|
||||
}
|
||||
|
||||
/** Checks the release output by running the release-output validations. */
|
||||
private checkReleaseOutput() {
|
||||
let hasFailed = false;
|
||||
|
||||
releasePackages.forEach(packageName => {
|
||||
if (!checkReleasePackage(this.releaseOutputPath, packageName)) {
|
||||
hasFailed = true;
|
||||
}
|
||||
});
|
||||
|
||||
// In case any release validation did not pass, abort the publishing because
|
||||
// the issues need to be resolved before publishing.
|
||||
if (hasFailed) {
|
||||
console.error(red(` ✘ Release output does not pass all release validations. Please fix ` +
|
||||
`all failures or reach out to the team.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user whether they are sure that the current stable version should be
|
||||
* released to the "next" NPM dist-tag.
|
||||
*/
|
||||
private async promptStableVersionForNextTag() {
|
||||
if (!await this.promptConfirm(
|
||||
'Are you sure that you want to release a stable version to the "next" tag?')) {
|
||||
console.log();
|
||||
console.log(yellow('Aborting publish...'));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Prompts the user whether he is sure that the script should continue publishing
|
||||
* the release to NPM.
|
||||
*/
|
||||
private async promptConfirmReleasePublish() {
|
||||
if (!await this.promptConfirm('Are you sure that you want to release now?')) {
|
||||
console.log();
|
||||
console.log(yellow('Aborting publish...'));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether NPM is currently authenticated. If not, the user will be prompted to enter
|
||||
* the NPM credentials that are necessary to publish the release. We achieve this by basically
|
||||
* running "npm login" as a child process and piping stdin/stdout/stderr to the current tty.
|
||||
*/
|
||||
private checkNpmAuthentication() {
|
||||
if (isNpmAuthenticated()) {
|
||||
console.info(green(` ✓ NPM is authenticated.`));
|
||||
return;
|
||||
}
|
||||
|
||||
let failedAuthentication = false;
|
||||
console.log(yellow(` ⚠ NPM is currently not authenticated. Running "npm login"..`));
|
||||
|
||||
for (let i = 0; i < MAX_NPM_LOGIN_TRIES; i++) {
|
||||
if (runInteractiveNpmLogin()) {
|
||||
// In case the user was able to login properly, we want to exit the loop as we
|
||||
// don't need to ask for authentication again.
|
||||
break;
|
||||
}
|
||||
|
||||
failedAuthentication = true;
|
||||
console.error(red(` ✘ Could not authenticate successfully. Please try again.`));
|
||||
}
|
||||
|
||||
if (failedAuthentication) {
|
||||
console.error(red(` ✘ Could not authenticate after ${MAX_NPM_LOGIN_TRIES} tries. ` +
|
||||
`Exiting..`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.info(green(` ✓ Successfully authenticated NPM.`));
|
||||
}
|
||||
|
||||
/** Publishes the specified package within the given NPM dist tag. */
|
||||
private publishPackageToNpm(packageName: string, npmDistTag: string) {
|
||||
console.info(green(` ⭮ Publishing "${packageName}"..`));
|
||||
|
||||
const errorOutput = runNpmPublish(join(this.releaseOutputPath, packageName), npmDistTag);
|
||||
|
||||
if (errorOutput) {
|
||||
console.error(red(` ✘ An error occurred while publishing "${packageName}".`));
|
||||
console.error(red(` Please check the terminal output and reach out to the team.`));
|
||||
console.error(red(`\n${errorOutput}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.info(green(` ✓ Successfully published "${packageName}"`));
|
||||
}
|
||||
}
|
||||
|
||||
/** Entry-point for the create release script. */
|
||||
if (require.main === module) {
|
||||
new PublishReleaseTask(join(__dirname, '../../'), 'angular', 'material2').run();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import {bold, red, yellow} from 'chalk';
|
||||
import {existsSync} from 'fs';
|
||||
import {sync as glob} from 'glob';
|
||||
import {join} from 'path';
|
||||
import {checkMaterialPackage, checkReleaseBundle} from './output-validations';
|
||||
|
||||
/** Glob that matches all JavaScript bundle files within a release package. */
|
||||
const releaseBundlesGlob = '+(esm5|esm2015|bundles)/*.js';
|
||||
|
||||
/**
|
||||
* Type that describes a map of package failures. The keys are failure messages and
|
||||
* their value is an array of specifically affected files.
|
||||
*/
|
||||
type PackageFailures = Map<string, string[]>;
|
||||
|
||||
/**
|
||||
* Checks a specified release package against generic and package-specific output validations.
|
||||
* Validations are added in order to ensure that build system changes do not cause an
|
||||
* unexpected release output (e.g. the theming bundle is no longer generated)
|
||||
* @returns Whether the package passed all checks or not.
|
||||
*/
|
||||
export function checkReleasePackage(releasesPath: string, packageName: string): boolean {
|
||||
const packagePath = join(releasesPath, packageName);
|
||||
const bundlePaths = glob(releaseBundlesGlob, {cwd: packagePath, absolute: true});
|
||||
const failures = new Map() as PackageFailures;
|
||||
const addFailure = (message, filePath?) => {
|
||||
failures.set(message, (failures.get(message) || []).concat(filePath));
|
||||
};
|
||||
|
||||
// We want to walk through each bundle within the current package and run
|
||||
// release validations that ensure that the bundles are not invalid.
|
||||
bundlePaths.forEach(bundlePath => {
|
||||
checkReleaseBundle(bundlePath)
|
||||
.forEach(message => addFailure(message, bundlePath));
|
||||
});
|
||||
|
||||
// Special release validation checks for the "material" release package.
|
||||
if (packageName === 'material') {
|
||||
checkMaterialPackage(join(releasesPath, packageName))
|
||||
.forEach(message => addFailure(message));
|
||||
}
|
||||
|
||||
if (!existsSync(join(packagePath, 'LICENSE'))) {
|
||||
addFailure('No license file found in package output.');
|
||||
}
|
||||
|
||||
if (!existsSync(join(packagePath, 'README.md'))) {
|
||||
addFailure('No "README.md" file found in package output.');
|
||||
}
|
||||
|
||||
// In case there are failures for this package, we want to print those
|
||||
// and return a value that implies that there were failures.
|
||||
if (failures.size) {
|
||||
printGroupedFailures(packageName, failures);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Prints the grouped failures for a specified package. */
|
||||
function printGroupedFailures(packageName: string, failures: PackageFailures) {
|
||||
console.error(red(bold(` ⚠ Package: "${packageName}" has failures:`)));
|
||||
failures.forEach((affectedFiles, failureMessage) => {
|
||||
console.error(yellow(` ⮑ ${failureMessage}`));
|
||||
|
||||
if (affectedFiles.length) {
|
||||
affectedFiles.forEach(affectedFile => console.error(yellow(` ${affectedFile}`)));
|
||||
}
|
||||
|
||||
// Add an extra line so that subsequent failure message groups are clearly separated.
|
||||
console.error();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import {existsSync, readFileSync} from 'fs';
|
||||
import {sync as glob} from 'glob';
|
||||
import {join} from 'path';
|
||||
|
||||
/** RegExp that matches Angular component inline styles that contain a sourcemap reference. */
|
||||
const inlineStylesSourcemapRegex = /styles: ?\[["'].*sourceMappingURL=.*["']/;
|
||||
|
||||
/** RegExp that matches Angular component metadata properties that refer to external resources. */
|
||||
const externalReferencesRegex = /(templateUrl|styleUrls): *["'[]/;
|
||||
|
||||
/**
|
||||
* Checks the specified release bundle and ensures that it does not contain
|
||||
* any external resource URLs.
|
||||
*/
|
||||
export function checkReleaseBundle(bundlePath: string): string[] {
|
||||
const bundleContent = readFileSync(bundlePath, 'utf8');
|
||||
let failures: string[] = [];
|
||||
|
||||
if (inlineStylesSourcemapRegex.exec(bundleContent) !== null) {
|
||||
failures.push('Found sourcemap references in component styles.');
|
||||
}
|
||||
|
||||
if (externalReferencesRegex.exec(bundleContent) !== null) {
|
||||
failures.push('Found external component resource references');
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the Angular Material release package and ensures that prebuilt themes
|
||||
* and the theming bundle are built properly.
|
||||
*/
|
||||
export function checkMaterialPackage(packagePath: string): string[] {
|
||||
const prebuiltThemesPath = join(packagePath, 'prebuilt-themes');
|
||||
const themingFilePath = join(packagePath, '_theming.scss');
|
||||
const failures: string[] = [];
|
||||
|
||||
if (glob('*.css', {cwd: prebuiltThemesPath}).length === 0) {
|
||||
failures.push('No prebuilt themes could be found.');
|
||||
}
|
||||
|
||||
if (!existsSync(themingFilePath)) {
|
||||
failures.push('The theming bundle could not be found.');
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** Packages that will be published to NPM by the publish release task. */
|
||||
export const releasePackages = [
|
||||
'cdk',
|
||||
'material',
|
||||
'cdk-experimental',
|
||||
'material-experimental',
|
||||
'material-moment-adapter'
|
||||
];
|
||||
@@ -1,14 +1,13 @@
|
||||
import * as OctokitApi from '@octokit/rest';
|
||||
import {bold, cyan, green, italic, red, yellow} from 'chalk';
|
||||
import {existsSync, readFileSync, writeFileSync} from 'fs';
|
||||
import {prompt} from 'inquirer';
|
||||
import {readFileSync, writeFileSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {BaseReleaseTask} from './base-release-task';
|
||||
import {promptAndGenerateChangelog} from './changelog';
|
||||
import {GitClient} from './git/git-client';
|
||||
import {getGithubBranchCommitsUrl} from './git/github-urls';
|
||||
import {promptForNewVersion} from './prompt/new-version-prompt';
|
||||
import {parseVersionName, Version} from './version-name/parse-version';
|
||||
import {getAllowedPublishBranches} from './version-name/publish-branches';
|
||||
|
||||
/** Default filename for the changelog. */
|
||||
const CHANGELOG_FILE_NAME = 'CHANGELOG.md';
|
||||
@@ -32,7 +31,7 @@ const CHANGELOG_FILE_NAME = 'CHANGELOG.md';
|
||||
* 10) Wait for the user to continue (users can customize generated changelog)
|
||||
* 11) Create a commit that includes all changes in the staging branch.
|
||||
*/
|
||||
class StageReleaseTask {
|
||||
class StageReleaseTask extends BaseReleaseTask {
|
||||
|
||||
/** Path to the project package JSON. */
|
||||
packageJsonPath: string;
|
||||
@@ -52,16 +51,10 @@ class StageReleaseTask {
|
||||
constructor(public projectDir: string,
|
||||
public repositoryOwner: string,
|
||||
public repositoryName: string) {
|
||||
super(new GitClient(projectDir,
|
||||
`https://github.com/${repositoryOwner}/${repositoryName}.git`));
|
||||
|
||||
this.packageJsonPath = join(projectDir, 'package.json');
|
||||
|
||||
console.log(this.projectDir);
|
||||
|
||||
if (!existsSync(this.packageJsonPath)) {
|
||||
console.error(red(`The specified directory is not referring to a project directory. ` +
|
||||
`There must be a ${italic('package.json')} file in the project directory.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
this.packageJson = JSON.parse(readFileSync(this.packageJsonPath, 'utf-8'));
|
||||
this.currentVersion = parseVersionName(this.packageJson.version);
|
||||
|
||||
@@ -72,8 +65,6 @@ class StageReleaseTask {
|
||||
}
|
||||
|
||||
this.githubApi = new OctokitApi();
|
||||
this.git = new GitClient(projectDir,
|
||||
`https://github.com/${repositoryOwner}/${repositoryName}.git`);
|
||||
}
|
||||
|
||||
async run() {
|
||||
@@ -121,16 +112,10 @@ class StageReleaseTask {
|
||||
`changes that apply to the public library release. When done, proceed to the prompt below.`));
|
||||
console.log();
|
||||
|
||||
const {shouldContinue} = await prompt<{shouldContinue: boolean}>({
|
||||
type: 'confirm',
|
||||
name: 'shouldContinue',
|
||||
message: 'Do you want to proceed and commit the changes?'
|
||||
});
|
||||
|
||||
if (!shouldContinue) {
|
||||
if (!await this.promptConfirm('Do you want to proceed and commit the changes?')) {
|
||||
console.log();
|
||||
console.log(yellow('Aborting release staging...'));
|
||||
process.exit(1);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
this.git.stageAllChanges();
|
||||
@@ -144,68 +129,6 @@ class StageReleaseTask {
|
||||
// TODO(devversion): automatic push and PR open URL shortcut.
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is on the expected publish branch. If the user is on a different branch,
|
||||
* this function automatically tries to checkout the publish branch.
|
||||
*/
|
||||
private 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. */
|
||||
private 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(` ✘ Cannot stage release. 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. */
|
||||
private verifyNoUncommittedChanges() {
|
||||
if (this.git.hasUncommittedChanges()) {
|
||||
console.error(red(` ✘ Cannot stage release. There are changes which are not committed ` +
|
||||
`and should be discarded.`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Updates the version of the project package.json and writes the changes to disk. */
|
||||
private updatePackageJsonVersion(newVersionName: string) {
|
||||
const newPackageJson = {...this.packageJson, version: newVersionName};
|
||||
@@ -229,8 +152,8 @@ class StageReleaseTask {
|
||||
console.error(red(` Please have a look at: ${githubCommitsUrl}`));
|
||||
process.exit(1);
|
||||
} else if (state === 'pending') {
|
||||
console.error(red(` ✘ Cannot stage release yet. Commit "${commitRef}" still has ` +
|
||||
`pending github statuses that need to succeed before staging a release.`));
|
||||
console.error(red(` ✘ Commit "${commitRef}" still has pending github statuses that ` +
|
||||
`need to succeed before staging a release.`));
|
||||
console.error(red(` Please have a look at: ${githubCommitsUrl}`));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user