forked from angular/angular
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.mjs
486 lines (439 loc) · 20.2 KB
/
index.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
#!/bin/env node
//
// WARNING: `CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN` should NOT be printed.
//
/*
* The following table summarizes the deployment targets per branch and RC phase (i.e. what Firebase
* project/site each branch is deployed to and with what config/tweaks).
*
* For more details on each deployment target, see the `deploymentInfoPerTarget` object inside the
* `computeDeploymentsInfo()` function.
* For additional information/terminology, see also:
* - [Angular Branching and Versioning: A Practical Guide](../../../docs/BRANCHES.md)
* - [Angular Development Phases](https://docs.google.com/document/d/197kVillDwx-RZtSVOBtPb4BBIAw0E9RT3q3v6DZkykU)
*
* |--------------------|-------------------------------------------------------------------|
* | TABLE: | Is there an active RC? |
* | Where should we |---------------------------------|---------------------------------|
* | deploy to/as? | NO | YES |
* |-----------|--------|---------------------------------|---------------------------------|
* | | LTS | archive | archive |
* | |--------|---------------------------------|---------------------------------|
* | | PATCH | stable | stable |
* | What | | redirectVersionDomainToStable | redirectVersionDomainToStable |
* | branch | | redirectRcToStable | |
* | are we |--------|---------------------------------|---------------------------------|
* | deploying | RC | - | rc |
* | from? | | | redirectVersionDomainToRc(*) |
* | |--------|---------------------------------|---------------------------------|
* | | MAIN | next | next |
* | | | redirectVersionDomainToNext(**) | redirectVersionDomainToNext(**) |
* |-----------|--------|---------------------------------|---------------------------------|
*
* (*): Only if `v<RC>` > `v<STABLE>`.
* (**): Only if (no active RC and `v<NEXT>` > `v<STABLE>`) or (active RC and `v<NEXT>` > `v<RC>`).
*
* NOTES:
* - The `v<X>-angular-io-site` Firebase site should be created (and connected to the
* `v<X>.angular.io` subdomain) before the version in the `main` branch's `package.json` is
* updated to a new major.
* - When a new major version is released, the deploy CI jobs for the new stable branch (prev. RC
* or next) and the old stable branch must be run AFTER the new stable version has been
* published to NPM, because the NPM info is used to determine what the stable version is.
* In the future, we could make the branch version info retrieval more robust, DRY and
* future-proof (and independent of NPM releases) by re-using the `ng-dev release info`
* [implementation](https://github.com/angular/dev-infra/blob/92778223953e029d1723febf282bb265b4e2a56f/ng-dev/release/info/cli.ts).
* (This would require `ng-dev` to expose an API for requesting the info (instead of printing it
* in human-readable format to stdout).)
*/
import path from 'path';
import sh from 'shelljs';
import {fileURLToPath} from 'url';
import post from './post-deploy-actions.mjs';
import pre from './pre-deploy-actions.mjs';
import u from './utils.mjs';
sh.set('-e');
// Constants
const inBazelTest = !!process.env.TEST_SRCDIR;
const DIRNAME = !inBazelTest
? u.getDirname(import.meta.url)
: path.join('.', 'aio', 'scripts', 'deploy-to-firebase');
const ROOT_PKG_PATH = `${DIRNAME}/../../../package.json`;
// Exports
export {
computeDeploymentsInfo,
computeInputVars,
skipDeployment,
validateDeploymentsInfo,
};
// Run
// ESM alternative for CommonJS' `require.main === module`. For simplicity, we assume command
// references the full file path (including the file extension).
// See https://stackoverflow.com/questions/45136831/node-js-require-main-module#answer-60309682 for
// more details.
const isMain = inBazelTest ||
(fileURLToPath(import.meta.url) === process.argv[1]);
if (isMain) {
const isDryRun = process.argv[2] === '--dry-run';
const inputVars = computeInputVars(process.env);
const deploymentsInfo = computeDeploymentsInfo(inputVars);
const totalDeployments = deploymentsInfo.length;
validateDeploymentsInfo(deploymentsInfo);
console.log(`Deployments (${totalDeployments}): ${listDeployTargetNames(deploymentsInfo)}`);
deploymentsInfo.forEach((deploymentInfo, idx) => {
const logLine1 = `Deployment ${idx + 1} of ${totalDeployments}: ${deploymentInfo.name}`;
console.log(`\n\n\n${logLine1}\n${'-'.repeat(logLine1.length)}`);
if (deploymentInfo.type === '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 computeDeploymentsInfo(
{currentBranch, currentCommit, isPullRequest, repoName, repoOwner, stableBranch}) {
// Do not deploy if we are running in a fork.
if (`${repoOwner}/${repoName}` !== u.REPO_SLUG) {
return [skipDeployment(`Skipping deploy because this is not ${u.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 = u.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 currentVersionPattern = /^\d+\.\d+\.x$/.test(currentBranch) ?
currentBranch : // The current branch name is a version pattern.
u.loadJson(ROOT_PKG_PATH).version; // We need to retrieve the version from `package.json`.
const currentBranchMajorVersion = u.computeMajorVersion(currentVersionPattern);
const stableBranchMajorVersion = u.computeMajorVersion(stableBranch);
const deploymentInfoPerTarget = {
// PRIMARY DEPLOY TARGETS
//
// These targets are responsible for building the app (and setting the theme/mode).
// Unless deployment is skipped, exactly one primary target should be used at a time and it
// should be the first item of the returned deploy target list.
next: {
name: 'next',
type: 'primary',
deployEnv: 'next',
projectId: 'angular-io',
siteId: 'next-angular-io-site',
deployedUrl: 'https://next.angular.io/',
preDeployActions: [pre.build, pre.checkPayloadSize],
postDeployActions: [post.testPwaScore],
},
rc: {
name: 'rc',
type: 'primary',
deployEnv: 'rc',
projectId: 'angular-io',
siteId: 'rc-angular-io-site',
deployedUrl: 'https://rc.angular.io/',
preDeployActions: [pre.build, pre.checkPayloadSize],
postDeployActions: [post.testPwaScore],
},
stable: {
name: 'stable',
type: 'primary',
deployEnv: 'stable',
projectId: 'angular-io',
siteId: 'stable-angular-io-site',
deployedUrl: 'https://angular.io/',
preDeployActions: [pre.build, pre.checkPayloadSize],
postDeployActions: [post.testPwaScore],
},
archive: {
name: 'archive',
type: 'primary',
deployEnv: 'archive',
projectId: 'angular-io',
siteId: `v${currentBranchMajorVersion}-angular-io-site`,
deployedUrl: `https://v${currentBranchMajorVersion}.angular.io/`,
preDeployActions: [pre.build, pre.checkPayloadSize],
postDeployActions: [post.testPwaScore],
},
// SECONDARY DEPLOY TARGETS
//
// These targets can be used to re-deploy the build artifacts from a primary target (potentially
// with small tweaks) to a different project/site.
// Unless deployment is skipped, zero or more secondary targets can be used at a time, but they
// should all match the primary target's `deployEnv`.
//
// TIP:
// Since there can be multiple secondary deployments (each tweaking the primary one in different
// ways), it is a good idea to ensure that any pre-deploy actions are undone in the post-deploy
// phase.
redirectVersionDomainToNext: {
name: 'redirectVersionDomainToNext',
type: 'secondary',
deployEnv: 'next',
projectId: 'angular-io',
siteId: `v${currentBranchMajorVersion}-angular-io-site`,
deployedUrl: `https://v${currentBranchMajorVersion}.angular.io/`,
preDeployActions: [pre.redirectAllToNext],
postDeployActions: [pre.undo.redirectAllToNext, post.testRedirectToNext],
},
redirectVersionDomainToRc: {
name: 'redirectVersionDomainToRc',
type: 'secondary',
deployEnv: 'rc',
projectId: 'angular-io',
siteId: `v${currentBranchMajorVersion}-angular-io-site`,
deployedUrl: `https://v${currentBranchMajorVersion}.angular.io/`,
preDeployActions: [pre.redirectAllToRc],
postDeployActions: [pre.undo.redirectAllToRc, post.testRedirectToRc],
},
redirectVersionDomainToStable: {
name: 'redirectVersionDomainToStable',
type: 'secondary',
deployEnv: 'stable',
projectId: 'angular-io',
siteId: `v${currentBranchMajorVersion}-angular-io-site`,
deployedUrl: `https://v${currentBranchMajorVersion}.angular.io/`,
preDeployActions: [pre.redirectAllToStable],
postDeployActions: [pre.undo.redirectAllToStable, post.testRedirectToStable],
},
// 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.
redirectRcToStable: {
name: 'redirectRcToStable',
type: 'secondary',
deployEnv: 'stable',
projectId: 'angular-io',
siteId: 'rc-angular-io-site',
deployedUrl: 'https://rc.angular.io/',
preDeployActions: [pre.disableServiceWorker, pre.redirectNonFilesToStable],
postDeployActions: [
pre.undo.redirectNonFilesToStable,
pre.undo.disableServiceWorker,
post.testNoActiveRcDeployment,
],
},
};
// Determine if there is an active RC version by checking whether the most recent minor branch is
// the stable branch or not.
const mostRecentMinorBranch = u.getMostRecentMinorBranch();
const rcBranch = (mostRecentMinorBranch !== stableBranch) ? mostRecentMinorBranch : null;
const isRcActive = rcBranch !== null;
// If the current branch is `main`, deploy as `next`.
if (currentBranch === 'main') {
// In order to determine whether to also deploy to `v<NEXT>-angular-io-site` we need to compare
// `v<NEXT>` with either `v<RC>` (if there is an active RC) or `v<STABLE>`.
const otherVersion = isRcActive ? u.computeMajorVersion(rcBranch) : stableBranchMajorVersion;
return (currentBranchMajorVersion > otherVersion) ?
// The next major version is greater than the RC or stable major version.
// Deploy to both `next-angular-io-site` and `v<NEXT>-angular-io-site`.
[
deploymentInfoPerTarget.next,
deploymentInfoPerTarget.redirectVersionDomainToNext,
] :
// The next major version is not greater than the RC or stable major version.
// Only deploy to `next-angular-io-site` (since `v<NEXT>-angular-io-site` is probably
// `v<RC>-angular-io-site` or `v<STABLE>-angular-io-site` and we don't want to overwrite the
// RC or stable deployment).
[
deploymentInfoPerTarget.next,
];
}
// If the current branch is the RC branch, deploy as `rc`.
if (currentBranch === rcBranch) {
return (currentBranchMajorVersion > stableBranchMajorVersion) ?
// The RC major version is greater than the stable major version.
// Deploy to both `rc-angular-io-site` and `v<RC>-angular-io-site`.
[
deploymentInfoPerTarget.rc,
deploymentInfoPerTarget.redirectVersionDomainToRc,
] :
// The RC major version is not greater than the stable major version.
// Only deploy to `rc-angular-io-site` (since `v<RC>-angular-io-site` is probably
// `v<STABLE>-angular-io-site` and we don't want to overwrite the stable deployment).
[
deploymentInfoPerTarget.rc,
];
}
// If the current branch is the stable branch, deploy as `stable`.
if (currentBranch === stableBranch) {
return isRcActive ?
// There is an active RC version. Only deploy to the `stable` projects/sites.
[
deploymentInfoPerTarget.stable,
deploymentInfoPerTarget.redirectVersionDomainToStable,
] :
// There is no active RC version. In addition to deploying to the `stable` projects/sites,
// 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.redirectVersionDomainToStable,
deploymentInfoPerTarget.redirectRcToStable,
];
}
// If we get here, it means that the current branch is neither `main`, nor the RC or stable
// branches. At this point, we may only deploy as `archive` and only if the following criteria are
// met:
// 1. The current branch must have the highest minor version among all branches with the same
// major version.
// 2. The current branch must have a major version that is lower than the stable major version.
// Do not deploy if it is not the branch with the highest minor for the given major version.
const mostRecentMinorBranchForMajor = u.getMostRecentMinorBranch(currentBranchMajorVersion);
if (currentBranch !== mostRecentMinorBranchForMajor) {
return [
skipDeployment(
`Skipping deploy of branch "${currentBranch}" to Firebase.\n` +
'There is a more recent branch with the same major version: ' +
`"${mostRecentMinorBranchForMajor}"`),
];
}
// Do not deploy if it does not have a lower major version than stable.
if (currentBranchMajorVersion >= stableBranchMajorVersion) {
return [
skipDeployment(
`Skipping deploy of branch "${currentBranch}" to Firebase.\n` +
'This branch has an equal or higher major version than the stable branch ' +
`("${stableBranch}") and is not the most recent minor branch.`),
];
}
// This is the highest minor version for a major that is lower than the stable major version:
// Deploy as `archive`.
return [deploymentInfoPerTarget.archive];
}
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 deploy(data) {
const {
currentCommit,
firebaseToken,
postDeployActions,
preDeployActions,
projectId,
siteId,
} = data;
sh.cd(`${DIRNAME}/../..`);
u.logSectionHeader('Run pre-deploy actions.');
preDeployActions.forEach(fn => fn(data));
u.logSectionHeader('Deploy AIO to Firebase hosting.');
const firebase = cmd => u.yarn(`firebase ${cmd} --token "${firebaseToken}"`);
firebase(`use "${projectId}"`);
firebase('target:clear hosting aio');
firebase(`target:apply hosting aio "${siteId}"`);
firebase(
`deploy --only database,hosting:aio --message "Commit: ${currentCommit}" --non-interactive`);
u.logSectionHeader('Run post-deploy actions.');
postDeployActions.forEach(fn => fn(data));
}
function listDeployTargetNames(deploymentsList) {
return deploymentsList.map(({name = '<no name>'}) => name).join(', ') || '-';
}
function serializeActions(actions) {
return actions.map(fn => fn.name).join(', ');
}
function skipDeployment(reason) {
return {name: 'skipped', type: 'skipped', reason};
}
function validateDeploymentsInfo(deploymentsList) {
const knownTargetTypes = ['primary', 'secondary', 'skipped'];
const requiredPropertiesForSkipped = ['name', 'type', 'reason'];
const requiredPropertiesForNonSkipped = [
'name', 'type', 'deployEnv', 'projectId', 'siteId', 'deployedUrl', 'preDeployActions',
'postDeployActions',
];
const primaryTargets = deploymentsList.filter(({type}) => type === 'primary');
const secondaryTargets = deploymentsList.filter(({type}) => type === 'secondary');
const skippedTargets = deploymentsList.filter(({type}) => type === 'skipped');
const otherTargets = deploymentsList.filter(({type}) => !knownTargetTypes.includes(type));
// Check that all targets have a known `type`.
if (otherTargets.length > 0) {
throw new Error(
`Expected all deploy targets to have a type of ${knownTargetTypes.join(' or ')}, but ` +
`found ${otherTargets.length} targets with an unknown type: ` +
otherTargets.map(({name = '<no name>', type}) => `${name} (type: ${type})`).join(', '));
}
// Check that all targets have the required properties.
for (const target of deploymentsList) {
const requiredProperties = (target.type === 'skipped') ?
requiredPropertiesForSkipped : requiredPropertiesForNonSkipped;
const missingProperties = requiredProperties.filter(prop => target[prop] === undefined);
if (missingProperties.length > 0) {
throw new Error(
`Expected deploy target '${target.name || '<no name>'}' to have all required ` +
`properties, but it is missing '${missingProperties.join('\', \'')}'.`);
}
}
// If there are skipped targets...
if (skippedTargets.length > 0) {
// ...check that exactly one target has been specified.
if (deploymentsList.length > 1) {
throw new Error(
`Expected a single skipped deploy target, but found ${deploymentsList.length} targets ` +
`in total: ${listDeployTargetNames(deploymentsList)}`);
}
// There is only one skipped deploy target and it is valid (i.e. has all required properties).
return;
}
// Check that exactly one primary target has been specified.
if (primaryTargets.length !== 1) {
throw new Error(
`Expected exactly one primary deploy target, but found ${primaryTargets.length}: ` +
listDeployTargetNames(primaryTargets));
}
const primaryTarget = primaryTargets[0];
const primaryIndex = deploymentsList.indexOf(primaryTarget);
// Check that the primary target is the first item in the list.
if (primaryIndex !== 0) {
throw new Error(
`Expected the primary target (${primaryTarget.name}) to be the first item in the deploy ` +
`target list, but it was found at index ${primaryIndex} (0-based): ` +
listDeployTargetNames(deploymentsList));
}
const nonMatchingSecondaryTargets =
secondaryTargets.filter(({deployEnv}) => deployEnv !== primaryTarget.deployEnv);
// Check that all secondary targets (if any) match the primary target's `deployEnv`.
if (nonMatchingSecondaryTargets.length > 0) {
throw new Error(
'Expected all secondary deploy targets to match the primary target\'s `deployEnv` ' +
`(${primaryTarget.deployEnv}), but ${nonMatchingSecondaryTargets.length} targets do not: ` +
nonMatchingSecondaryTargets.map(t => `${t.name} (deployEnv: ${t.deployEnv})`).join(', '));
}
}