6e6eee6d5b
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
245 lines
8.0 KiB
JavaScript
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}`);
|
|
}
|