fix(docs-infra): fix redirecting `rc.angular.io` to `angular.io` when no active RC (#39853)

Currently there is an issue with redirecting `rc.angular.io` to
`angular.io` when there is no active RC. If a user has visited
`rc.angular.io` before and has a ServiceWorker registered for that
subdomain, they will never "see" the redirect to `angular.io`.

This commit fixes the problem by doing an additional deployment from the
stable branch to the `rc-angular-io-site` Firebase site when there is no
active RC. This additional deployment will ensure that:
1. Users will be temporarily redirected from `rc.angular.io` to
   `angular.io`.
2. Users with a registered ServiceWorker (who don't see the redirect)
   will have their ServiceWorker unregistered on the next visit.
3. The content on both sites is identical.

See #39760 for more details on the problem and the solution.

NOTE:
As mentioned in #39760, for this fix to take affect, we need to remove
the redirect from `rc.angular.io` to `angular.io` in the Firebase
console for site `rc-angular-io-site`.

Fixes #39760

PR Close #39853
This commit is contained in:
George Kalpakas 2020-11-30 13:59:12 +02:00 committed by Misko Hevery
parent 1e39e493fb
commit cf1f5a1e37
2 changed files with 104 additions and 3 deletions

View File

@ -4,7 +4,7 @@
// //
'use strict'; 'use strict';
const {cd, cp, exec, set} = require('shelljs'); const {cd, cp, exec, mv, sed, set} = require('shelljs');
set('-e'); set('-e');
@ -17,6 +17,7 @@ const NG_REMOTE_URL = `https://github.com/${REPO_SLUG}.git`;
module.exports = { module.exports = {
computeDeploymentsInfo, computeDeploymentsInfo,
computeInputVars, computeInputVars,
computeMajorVersion,
getLatestCommit, getLatestCommit,
getMostRecentMinorBranch, getMostRecentMinorBranch,
}; };
@ -122,6 +123,17 @@ function computeDeploymentsInfo(
preDeployActions: [build, checkPayloadSize], preDeployActions: [build, checkPayloadSize],
postDeployActions: [testPwaScore], postDeployActions: [testPwaScore],
}, },
// Config for deploying the stable build to the RC Firebase site when there is no active RC.
// See https://github.com/angular/angular/issues/39760 for more info on the purpose of this
// special deployment.
noActiveRc: {
deployEnv: 'stable',
projectId: 'angular-io',
siteId: 'rc-angular-io-site',
deployedUrl: 'https://rc.angular.io/',
preDeployActions: [removeServiceWorker, redirectToAngularIo],
postDeployActions: [testNoActiveRcDeployment],
},
archive: { archive: {
deployEnv: 'archive', deployEnv: 'archive',
projectId: 'angular-io', projectId: 'angular-io',
@ -149,7 +161,17 @@ function computeDeploymentsInfo(
// If the current branch is the stable branch, deploy as `stable`. // If the current branch is the stable branch, deploy as `stable`.
if (currentBranch === stableBranch) { if (currentBranch === stableBranch) {
return [deploymentInfoPerTarget.stable]; return (rcBranch !== null) ?
// There is an active RC version. Only deploy to the `stable` project/site.
[deploymentInfoPerTarget.stable] :
// There is no active RC version. In addition to deploying to the `stable` project/site,
// deploy to `rc` to ensure it redirects to `stable`.
// See https://github.com/angular/angular/issues/39760 for more info on the purpose of this
// special deployment.
[
deploymentInfoPerTarget.stable,
deploymentInfoPerTarget.noActiveRc,
];
} }
// If we get here, it means that the current branch is neither `master`, nor the RC or stable // If we get here, it means that the current branch is neither `master`, nor the RC or stable
@ -263,6 +285,21 @@ function getLatestCommit(branchName, remote = undefined) {
return getRemoteRefs(branchName, remote)[0].slice(0, 40); return getRemoteRefs(branchName, remote)[0].slice(0, 40);
} }
function redirectToAngularIo() {
// Update the Firebase hosting configuration redirect all non-file requests (i.e. requests that do
// not contain a dot in their last path segment) to `angular.io`.
// See https://firebase.google.com/docs/hosting/full-config#redirects.
const redirectRule =
'{"type": 302, "regex": "^(.*/[^./]*)$", "destination": "https://angular.io:1"}';
sed('-i', /(\s*)"redirects": \[/, `$&\n$1 ${redirectRule},\n`, 'firebase.json');
}
function removeServiceWorker() {
// Rename the SW manifest (`ngsw.json`). This will cause the ServiceWorker to unregister itself.
// See https://angular.io/guide/service-worker-devops#fail-safe.
mv('dist/ngsw.json', 'dist/ngsw.json.bak');
}
function serializeActions(actions) { function serializeActions(actions) {
return actions.map(fn => fn.name).join(', '); return actions.map(fn => fn.name).join(', ');
} }
@ -271,6 +308,41 @@ function skipDeployment(reason) {
return {reason, skipped: true}; return {reason, skipped: true};
} }
function testNoActiveRcDeployment({deployedUrl}) {
const deployedOrigin = deployedUrl.replace(/\/$/, '');
// Ensure a request for `ngsw.json` returns 404.
const ngswJsonUrl = `${deployedOrigin}/ngsw.json`;
const ngswJsonScript = `https.get('${ngswJsonUrl}', res => console.log(res.statusCode))`;
const ngswJsonActualStatusCode = exec(`node --eval "${ngswJsonScript}"`, {silent: true}).trim();
const ngswJsonExpectedStatusCode = '404';
if (ngswJsonActualStatusCode !== ngswJsonExpectedStatusCode) {
throw new Error(
`Expected '${ngswJsonUrl}' to return a status code of '${ngswJsonExpectedStatusCode}', ` +
`but it returned '${ngswJsonActualStatusCode}'.`);
}
// Ensure a request for `foo/bar` is redirected to `https://angular.io/foo/bar`.
const fooBarUrl = `${deployedOrigin}/foo/bar?baz=qux`;
const fooBarScript =
`https.get('${fooBarUrl}', res => console.log(res.statusCode, res.headers.location))`;
const [fooBarActualStatusCode, fooBarActualRedirectUrl] =
exec(`node --eval "${fooBarScript}"`, {silent: true}).trim().split(' ');
const fooBarExpectedStatusCode = '302';
const fooBarExpectedRedirectUrl = 'https://angular.io/foo/bar?baz=qux';
if (fooBarActualStatusCode !== fooBarExpectedStatusCode) {
throw new Error(
`Expected '${fooBarUrl}' to return a status code of '${fooBarExpectedStatusCode}', but ` +
`it returned '${fooBarActualStatusCode}'.`);
} else if (fooBarActualRedirectUrl !== fooBarExpectedRedirectUrl) {
throw new Error(
`Expected '${fooBarUrl}' to be redirected to '${fooBarExpectedRedirectUrl}', but it was ` +
`but it was redirected to '${fooBarActualRedirectUrl}'.`);
}
}
function testPwaScore({deployedUrl, minPwaScore}) { function testPwaScore({deployedUrl, minPwaScore}) {
console.log('\n\n\n==== Run PWA-score tests. ====\n'); console.log('\n\n\n==== Run PWA-score tests. ====\n');
yarn(`test-pwa-score "${deployedUrl}" "${minPwaScore}"`); yarn(`test-pwa-score "${deployedUrl}" "${minPwaScore}"`);

View File

@ -5,6 +5,7 @@ const {execSync} = require('child_process');
const { const {
computeDeploymentsInfo, computeDeploymentsInfo,
computeInputVars, computeInputVars,
computeMajorVersion,
getLatestCommit, getLatestCommit,
getMostRecentMinorBranch, getMostRecentMinorBranch,
} = require('./deploy-to-firebase'); } = require('./deploy-to-firebase');
@ -104,7 +105,7 @@ describe('deploy-to-firebase:', () => {
]); ]);
}); });
it('stable - deploy success', () => { it('stable - deploy success - active RC', () => {
expect(getDeploymentsInfoFor({ expect(getDeploymentsInfoFor({
CI_REPO_OWNER: 'angular', CI_REPO_OWNER: 'angular',
CI_REPO_NAME: 'angular', CI_REPO_NAME: 'angular',
@ -124,6 +125,34 @@ describe('deploy-to-firebase:', () => {
]); ]);
}); });
it('stable - deploy success - no active RC', () => {
expect(getDeploymentsInfoFor({
CI_REPO_OWNER: 'angular',
CI_REPO_NAME: 'angular',
CI_PULL_REQUEST: 'false',
CI_BRANCH: mostRecentMinorBranch,
CI_STABLE_BRANCH: mostRecentMinorBranch,
CI_COMMIT: latestCommits[mostRecentMinorBranch],
})).toEqual([
{
deployEnv: 'stable',
projectId: 'angular-io',
siteId: `v${computeMajorVersion(mostRecentMinorBranch)}-angular-io-site`,
deployedUrl: 'https://angular.io/',
preDeployActions: ['function:build', 'function:checkPayloadSize'],
postDeployActions: ['function:testPwaScore'],
},
{
deployEnv: 'stable',
projectId: 'angular-io',
siteId: 'rc-angular-io-site',
deployedUrl: 'https://rc.angular.io/',
preDeployActions: ['function:removeServiceWorker', 'function:redirectToAngularIo'],
postDeployActions: ['function:testNoActiveRcDeployment'],
},
]);
});
it('stable - skip deploy - commit not HEAD', () => { it('stable - skip deploy - commit not HEAD', () => {
expect(getDeploymentsInfoFor({ expect(getDeploymentsInfoFor({
CI_REPO_OWNER: 'angular', CI_REPO_OWNER: 'angular',