angular-docs-cn/aio/scripts/deploy-to-firebase.js
George Kalpakas 6e6eee6d5b refactor(docs-infra): decouple deploying from other operations in deploy-to-firebase.js (#39853)
Previously, the `deploy()` function in `deploy-to-firebase.js` would
also perform other operations (beyond deploying), such as building the
app, checking the generated payload size, testing the PWA score of the
deployed app.

This commit decouples these operations, so that deploying can be
performed independently.

NOTE:
In a subsequent commit, this will be leveraged fix an issue with
`rc.angular.io` redirects in the presence of a ServiceWorker by
deploying the same artifacts to multiple Firebase projects/sites.
See #39760 for more details.

PR Close #39853
2020-12-02 12:51:34 -08:00

245 lines
8.0 KiB
JavaScript

#!/bin/env node
//
// WARNING: `CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN` should NOT be printed.
//
'use strict';
const {cd, cp, exec, set} = require('shelljs');
set('-e');
// Constants
const REPO_SLUG = 'angular/angular';
const NG_REMOTE_URL = `https://github.com/${REPO_SLUG}.git`;
// Exports
module.exports = {
computeDeploymentInfo,
computeInputVars,
getLatestCommit,
};
// Run
if (require.main === module) {
const isDryRun = process.argv[2] === '--dry-run';
const inputVars = computeInputVars(process.env);
const deploymentInfo = computeDeploymentInfo(inputVars);
if (deploymentInfo.skipped) {
console.log(deploymentInfo.reason);
} else {
console.log(
`Git branch : ${inputVars.currentBranch}\n` +
`Git commit : ${inputVars.currentCommit}\n` +
`Build/deploy mode : ${deploymentInfo.deployEnv}\n` +
`Firebase project : ${deploymentInfo.projectId}\n` +
`Firebase site : ${deploymentInfo.siteId}\n` +
`Pre-deploy actions : ${serializeActions(deploymentInfo.preDeployActions)}\n` +
`Post-deploy actions : ${serializeActions(deploymentInfo.postDeployActions)}\n` +
`Deployment URLs : ${deploymentInfo.deployedUrl}\n` +
` https://${deploymentInfo.siteId}.web.app/`);
if (!isDryRun) {
deploy({...inputVars, ...deploymentInfo});
}
}
}
// Helpers
function build({deployedUrl, deployEnv}) {
console.log('\n\n\n==== Build the AIO app. ====\n');
yarn(`build --configuration=${deployEnv} --progress=false`);
console.log('\n\n\n==== Add any mode-specific files into the AIO distribution. ====\n');
cp('-rf', `src/extra-files/${deployEnv}/.`, 'dist/');
console.log('\n\n\n==== Update opensearch descriptor for AIO with `deployedUrl`. ====\n');
yarn(`set-opensearch-url ${deployedUrl.replace(/[^/]$/, '$&/')}`); // The URL must end with `/`.
}
function checkPayloadSize() {
console.log('\n\n\n==== Check payload size and upload the numbers to Firebase DB. ====\n');
yarn('payload-size');
}
function computeDeploymentInfo(
{currentBranch, currentCommit, isPullRequest, repoName, repoOwner, stableBranch}) {
// Do not deploy if we are running in a fork.
if (`${repoOwner}/${repoName}` !== REPO_SLUG) {
return skipDeployment(`Skipping deploy because this is not ${REPO_SLUG}.`);
}
// Do not deploy if this is a PR. PRs are deployed in the `aio_preview` CircleCI job.
if (isPullRequest) {
return skipDeployment('Skipping deploy because this is a PR build.');
}
// Do not deploy if the current commit is not the latest on its branch.
const latestCommit = getLatestCommit(currentBranch);
if (currentCommit !== latestCommit) {
return skipDeployment(
`Skipping deploy because ${currentCommit} is not the latest commit (${latestCommit}).`);
}
// The deployment mode is computed based on the branch we are building.
const currentBranchMajorVersion = computeMajorVersion(currentBranch);
const deploymentInfoPerTarget = {
next: {
deployEnv: 'next',
projectId: 'angular-io',
siteId: 'next-angular-io-site',
deployedUrl: 'https://next.angular.io/',
preDeployActions: [build, checkPayloadSize],
postDeployActions: [testPwaScore],
},
rc: {
deployEnv: 'rc',
projectId: 'angular-io',
siteId: 'rc-angular-io-site',
deployedUrl: 'https://rc.angular.io/',
preDeployActions: [build, checkPayloadSize],
postDeployActions: [testPwaScore],
},
stable: {
deployEnv: 'stable',
projectId: 'angular-io',
siteId: `v${currentBranchMajorVersion}-angular-io-site`,
deployedUrl: 'https://angular.io/',
preDeployActions: [build, checkPayloadSize],
postDeployActions: [testPwaScore],
},
archive: {
deployEnv: 'archive',
projectId: 'angular-io',
siteId: `v${currentBranchMajorVersion}-angular-io-site`,
deployedUrl: `https://v${currentBranchMajorVersion}.angular.io/`,
preDeployActions: [build, checkPayloadSize],
postDeployActions: [testPwaScore],
},
};
if (currentBranch === 'master') {
return deploymentInfoPerTarget.next;
} else if (currentBranch === stableBranch) {
return deploymentInfoPerTarget.stable;
} else {
const stableBranchMajorVersion = computeMajorVersion(stableBranch);
// Find the branch that has highest minor version for the given `currentBranchMajorVersion`.
const mostRecentMinorVersionBranch =
// List the branches that start with the major version.
getRemoteRefs(`refs/heads/${currentBranchMajorVersion}.*.x`)
// Extract the version number.
.map(line => line.split('/')[2])
// Sort by the minor version.
.sort((a, b) => a.split('.')[1] - b.split('.')[1])
// Get the highest version.
.pop();
// Do not deploy if it is not the latest branch for the given major version.
// NOTE: At this point, we know the current branch is not the stable branch.
if (currentBranch !== mostRecentMinorVersionBranch) {
return skipDeployment(
`Skipping deploy of branch "${currentBranch}" to Firebase.\n` +
'There is a more recent branch with the same major version: ' +
`"${mostRecentMinorVersionBranch}"`);
}
return (currentBranchMajorVersion < stableBranchMajorVersion) ?
// This is the latest minor version for a major that is less than the stable major version:
// Deploy as `archive`.
deploymentInfoPerTarget.archive :
// This is the latest minor version for a major that is equal or greater than the stable
// major version, but not the stable version itself:
// Deploy as `rc`.
deploymentInfoPerTarget.rc;
}
// We should never get here.
throw new Error('Failed to determine deployment info.');
}
function computeInputVars({
CI_AIO_MIN_PWA_SCORE: minPwaScore,
CI_BRANCH: currentBranch,
CI_COMMIT: currentCommit,
CI_PULL_REQUEST,
CI_REPO_NAME: repoName,
CI_REPO_OWNER: repoOwner,
CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN: firebaseToken,
CI_STABLE_BRANCH: stableBranch,
}) {
return {
currentBranch,
currentCommit,
firebaseToken,
isPullRequest: CI_PULL_REQUEST !== 'false',
minPwaScore,
repoName,
repoOwner,
stableBranch,
};
}
function computeMajorVersion(branchName) {
return +branchName.split('.', 1)[0];
}
function deploy(data) {
const {
currentCommit,
firebaseToken,
postDeployActions,
preDeployActions,
projectId,
siteId,
} = data;
cd(`${__dirname}/..`);
console.log('\n\n\n==== Run pre-deploy actions. ====\n');
preDeployActions.forEach(fn => fn(data));
console.log('\n\n\n==== Deploy AIO to Firebase hosting. ====\n');
yarn(`firebase use "${projectId}" --token "${firebaseToken}"`);
yarn(`firebase target:apply hosting aio "${siteId}" --token "${firebaseToken}"`);
yarn(
`firebase deploy --only hosting:aio --message "Commit: ${currentCommit}" --non-interactive ` +
`--token "${firebaseToken}"`);
console.log('\n\n\n==== Run post-deploy actions. ====\n');
postDeployActions.forEach(fn => fn(data));
}
function getRemoteRefs(refOrPattern, remote = NG_REMOTE_URL) {
return exec(`git ls-remote ${remote} ${refOrPattern}`, {silent: true}).trim().split('\n');
}
function getLatestCommit(branchName, remote = undefined) {
return getRemoteRefs(branchName, remote)[0].slice(0, 40);
}
function serializeActions(actions) {
return actions.map(fn => fn.name).join(', ');
}
function skipDeployment(reason) {
return {reason, skipped: true};
}
function testPwaScore({deployedUrl, minPwaScore}) {
console.log('\n\n\n==== Run PWA-score tests. ====\n');
yarn(`test-pwa-score "${deployedUrl}" "${minPwaScore}"`);
}
function yarn(cmd) {
// Using `--silent` to ensure no secret env variables are printed.
//
// NOTE:
// This is not strictly necessary, since CircleCI will mask secret environment variables in the
// output (see https://circleci.com/docs/2.0/env-vars/#secrets-masking), but is an extra
// precaution.
return exec(`yarn --silent ${cmd}`);
}