@anthusai/papyrus 5.12.0 → 5.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/infra/amplify-app-shell.js +54 -4
- package/infra/amplify-app-shell.ts +64 -6
- package/infra/site-config.js +34 -3
- package/infra/site-config.ts +46 -3
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
|
@@ -4,7 +4,7 @@ import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
|
4
4
|
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
|
|
5
5
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider.js";
|
|
6
6
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs.js";
|
|
7
|
-
import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
7
|
+
import { PROSPECTUS_PAPYRUS_SITE_ROUTES, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config.js";
|
|
8
8
|
function environmentVariables(variables) {
|
|
9
9
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
10
10
|
}
|
|
@@ -68,6 +68,9 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
68
68
|
if (!config.reader)
|
|
69
69
|
throw new Error(`site ${config.siteId} has neither a cms section nor a reader`);
|
|
70
70
|
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
71
|
+
if (config.autoDeploy) {
|
|
72
|
+
this.addGithubDeployRole(config, [{ appId: this.readerAppId, branch: config.reader.branchName ?? "main" }]);
|
|
73
|
+
}
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
73
76
|
const cmsAppName = config.cms.appName ?? `${config.siteId}-cms`;
|
|
@@ -165,7 +168,8 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
165
168
|
appId: cmsApp.attrAppId,
|
|
166
169
|
branchName: "main",
|
|
167
170
|
stage: "PRODUCTION",
|
|
168
|
-
|
|
171
|
+
// Auto-build off when GitHub Actions starts the job on merge (autoDeploy): nothing may double-build.
|
|
172
|
+
enableAutoBuild: !config.autoDeploy,
|
|
169
173
|
enablePerformanceMode: false,
|
|
170
174
|
environmentVariables: environmentVariables({
|
|
171
175
|
...config.cms.environment,
|
|
@@ -247,6 +251,12 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
247
251
|
}
|
|
248
252
|
const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
|
|
249
253
|
this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
|
|
254
|
+
if (config.autoDeploy) {
|
|
255
|
+
this.addGithubDeployRole(config, [
|
|
256
|
+
{ appId: cmsApp.attrAppId, branch: "main" },
|
|
257
|
+
...(this.readerAppId && config.reader ? [{ appId: this.readerAppId, branch: config.reader.branchName ?? "main" }] : []),
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
250
260
|
if (config.github) {
|
|
251
261
|
this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
|
|
252
262
|
}
|
|
@@ -294,6 +304,46 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
294
304
|
description: `Role the Papyrus CLI assumes for ${config.siteId} (profile role_arn)`,
|
|
295
305
|
});
|
|
296
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* The merge-to-main deploy role: assumable only by a GitHub Actions run on `main` of the site's own
|
|
309
|
+
* repository (OIDC, no keys), and able to do exactly one thing: start and read Amplify jobs on the
|
|
310
|
+
* production branch of the site's apps.
|
|
311
|
+
*/
|
|
312
|
+
addGithubDeployRole(config, targets) {
|
|
313
|
+
const { owner, repo } = resolveGithubRepository(config);
|
|
314
|
+
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, "GithubDeployOidcProvider", this.formatArn({
|
|
315
|
+
service: "iam",
|
|
316
|
+
region: "",
|
|
317
|
+
resource: "oidc-provider",
|
|
318
|
+
resourceName: GITHUB_OIDC_PROVIDER_HOST,
|
|
319
|
+
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
|
|
320
|
+
}));
|
|
321
|
+
const deployRole = new iam.Role(this, "GithubDeployRole", {
|
|
322
|
+
roleName: `${config.siteId}-github-deploy`,
|
|
323
|
+
description: `Merge-to-main deploy for ${owner}/${repo}: GitHub OIDC on refs/heads/main only; amplify:StartJob and GetJob on the production branch of the site's apps only`,
|
|
324
|
+
maxSessionDuration: Duration.hours(1),
|
|
325
|
+
assumedBy: new iam.OpenIdConnectPrincipal(provider, {
|
|
326
|
+
StringEquals: {
|
|
327
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com",
|
|
328
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: githubOidcSubjects(config, "main"),
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
});
|
|
332
|
+
deployRole.addToPolicy(new iam.PolicyStatement({
|
|
333
|
+
sid: "StartAndReadProductionJobs",
|
|
334
|
+
actions: ["amplify:StartJob", "amplify:GetJob"],
|
|
335
|
+
resources: targets.map((target) => this.formatArn({
|
|
336
|
+
service: "amplify",
|
|
337
|
+
resource: "apps",
|
|
338
|
+
resourceName: `${target.appId}/branches/${target.branch}/jobs/*`,
|
|
339
|
+
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
|
|
340
|
+
})),
|
|
341
|
+
}));
|
|
342
|
+
new CfnOutput(this, "GithubDeployRoleArn", {
|
|
343
|
+
value: deployRole.roleArn,
|
|
344
|
+
description: `Role for the merge-to-main deploy workflow in ${owner}/${repo}`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
297
347
|
addGithubCiRole(config, github, cmsAppId, amplifyAppIds) {
|
|
298
348
|
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, "GithubOidcProvider", this.formatArn({
|
|
299
349
|
service: "iam",
|
|
@@ -309,7 +359,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
309
359
|
assumedBy: new iam.OpenIdConnectPrincipal(provider, {
|
|
310
360
|
StringEquals: { [`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com" },
|
|
311
361
|
StringLike: {
|
|
312
|
-
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.
|
|
362
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.flatMap((branch) => githubOidcSubjects(config, branch)),
|
|
313
363
|
},
|
|
314
364
|
}),
|
|
315
365
|
});
|
|
@@ -361,7 +411,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
361
411
|
appId: readerApp.attrAppId,
|
|
362
412
|
branchName: readerBranchName,
|
|
363
413
|
stage: "PRODUCTION",
|
|
364
|
-
enableAutoBuild:
|
|
414
|
+
enableAutoBuild: !config.autoDeploy,
|
|
365
415
|
environmentVariables: environmentVariables({
|
|
366
416
|
...(reader.environment ?? {}),
|
|
367
417
|
PAPYRUS_SITE_BRAND: config.brand,
|
|
@@ -5,7 +5,7 @@ import * as amplify from "aws-cdk-lib/aws-amplify";
|
|
|
5
5
|
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
|
|
6
6
|
import { GITHUB_OIDC_PROVIDER_HOST } from "./github-oidc-provider";
|
|
7
7
|
import { cmsProductionBuildSpec, cmsStagingBuildSpec, readerBuildSpec } from "./build-specs";
|
|
8
|
-
import { AmplifyAppShellSiteConfig, PROSPECTUS_PAPYRUS_SITE_ROUTES, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
8
|
+
import { AmplifyAppShellSiteConfig, PROSPECTUS_PAPYRUS_SITE_ROUTES, ReaderCustomHeaderRule, hasCmsApp, isStagingEnabled, resolveRedirectSubDomainPrefix, resolveCmsHostName, githubOidcSubjects, resolveGithubRepository, resolveProspectusAccountsEndpoint, resolveRevalidateSecretParameterName, resolveStackName, resolveStagingDomainName, resolveStoragePreviewPrefix } from "./site-config";
|
|
9
9
|
|
|
10
10
|
function environmentVariables(variables: Record<string, string>): amplify.CfnBranch.EnvironmentVariableProperty[] {
|
|
11
11
|
return Object.entries(variables).map(([name, value]) => ({ name, value }));
|
|
@@ -72,6 +72,9 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
72
72
|
if (!hasCmsApp(config)) {
|
|
73
73
|
if (!config.reader) throw new Error(`site ${config.siteId} has neither a cms section nor a reader`);
|
|
74
74
|
this.readerAppId = this.addReaderApp(config, config.reader);
|
|
75
|
+
if (config.autoDeploy) {
|
|
76
|
+
this.addGithubDeployRole(config, [{ appId: this.readerAppId, branch: config.reader.branchName ?? "main" }]);
|
|
77
|
+
}
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -185,7 +188,8 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
185
188
|
appId: cmsApp.attrAppId,
|
|
186
189
|
branchName: "main",
|
|
187
190
|
stage: "PRODUCTION",
|
|
188
|
-
|
|
191
|
+
// Auto-build off when GitHub Actions starts the job on merge (autoDeploy): nothing may double-build.
|
|
192
|
+
enableAutoBuild: !config.autoDeploy,
|
|
189
193
|
enablePerformanceMode: false,
|
|
190
194
|
environmentVariables: environmentVariables({
|
|
191
195
|
...config.cms.environment,
|
|
@@ -274,6 +278,13 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
274
278
|
const amplifyAppIds = [cmsApp.attrAppId, ...(this.readerAppId ? [this.readerAppId] : [])];
|
|
275
279
|
this.addAuthoringRole(config, cmsApp.attrAppId, amplifyAppIds);
|
|
276
280
|
|
|
281
|
+
if (config.autoDeploy) {
|
|
282
|
+
this.addGithubDeployRole(config, [
|
|
283
|
+
{ appId: cmsApp.attrAppId, branch: "main" },
|
|
284
|
+
...(this.readerAppId && config.reader ? [{ appId: this.readerAppId, branch: config.reader.branchName ?? "main" }] : []),
|
|
285
|
+
]);
|
|
286
|
+
}
|
|
287
|
+
|
|
277
288
|
if (config.github) {
|
|
278
289
|
this.addGithubCiRole(config, config.github, cmsApp.attrAppId, amplifyAppIds);
|
|
279
290
|
}
|
|
@@ -326,6 +337,55 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
326
337
|
});
|
|
327
338
|
}
|
|
328
339
|
|
|
340
|
+
/**
|
|
341
|
+
* The merge-to-main deploy role: assumable only by a GitHub Actions run on `main` of the site's own
|
|
342
|
+
* repository (OIDC, no keys), and able to do exactly one thing: start and read Amplify jobs on the
|
|
343
|
+
* production branch of the site's apps.
|
|
344
|
+
*/
|
|
345
|
+
private addGithubDeployRole(config: AmplifyAppShellSiteConfig, targets: { appId: string; branch: string }[]): void {
|
|
346
|
+
const { owner, repo } = resolveGithubRepository(config);
|
|
347
|
+
const provider = iam.OpenIdConnectProvider.fromOpenIdConnectProviderArn(
|
|
348
|
+
this,
|
|
349
|
+
"GithubDeployOidcProvider",
|
|
350
|
+
this.formatArn({
|
|
351
|
+
service: "iam",
|
|
352
|
+
region: "",
|
|
353
|
+
resource: "oidc-provider",
|
|
354
|
+
resourceName: GITHUB_OIDC_PROVIDER_HOST,
|
|
355
|
+
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
const deployRole = new iam.Role(this, "GithubDeployRole", {
|
|
359
|
+
roleName: `${config.siteId}-github-deploy`,
|
|
360
|
+
description: `Merge-to-main deploy for ${owner}/${repo}: GitHub OIDC on refs/heads/main only; amplify:StartJob and GetJob on the production branch of the site's apps only`,
|
|
361
|
+
maxSessionDuration: Duration.hours(1),
|
|
362
|
+
assumedBy: new iam.OpenIdConnectPrincipal(provider, {
|
|
363
|
+
StringEquals: {
|
|
364
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com",
|
|
365
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: githubOidcSubjects(config, "main"),
|
|
366
|
+
},
|
|
367
|
+
}),
|
|
368
|
+
});
|
|
369
|
+
deployRole.addToPolicy(
|
|
370
|
+
new iam.PolicyStatement({
|
|
371
|
+
sid: "StartAndReadProductionJobs",
|
|
372
|
+
actions: ["amplify:StartJob", "amplify:GetJob"],
|
|
373
|
+
resources: targets.map((target) =>
|
|
374
|
+
this.formatArn({
|
|
375
|
+
service: "amplify",
|
|
376
|
+
resource: "apps",
|
|
377
|
+
resourceName: `${target.appId}/branches/${target.branch}/jobs/*`,
|
|
378
|
+
arnFormat: ArnFormat.SLASH_RESOURCE_NAME,
|
|
379
|
+
}),
|
|
380
|
+
),
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
new CfnOutput(this, "GithubDeployRoleArn", {
|
|
384
|
+
value: deployRole.roleArn,
|
|
385
|
+
description: `Role for the merge-to-main deploy workflow in ${owner}/${repo}`,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
329
389
|
private addGithubCiRole(
|
|
330
390
|
config: AmplifyAppShellSiteConfig,
|
|
331
391
|
github: NonNullable<AmplifyAppShellSiteConfig["github"]>,
|
|
@@ -351,9 +411,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
351
411
|
assumedBy: new iam.OpenIdConnectPrincipal(provider, {
|
|
352
412
|
StringEquals: { [`${GITHUB_OIDC_PROVIDER_HOST}:aud`]: "sts.amazonaws.com" },
|
|
353
413
|
StringLike: {
|
|
354
|
-
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.
|
|
355
|
-
(branch) => `repo:${github.owner}/${github.repo}:ref:refs/heads/${branch}`,
|
|
356
|
-
),
|
|
414
|
+
[`${GITHUB_OIDC_PROVIDER_HOST}:sub`]: github.branches.flatMap((branch) => githubOidcSubjects(config, branch)),
|
|
357
415
|
},
|
|
358
416
|
}),
|
|
359
417
|
});
|
|
@@ -420,7 +478,7 @@ export class AmplifyAppShellStack extends Stack {
|
|
|
420
478
|
appId: readerApp.attrAppId,
|
|
421
479
|
branchName: readerBranchName,
|
|
422
480
|
stage: "PRODUCTION",
|
|
423
|
-
enableAutoBuild:
|
|
481
|
+
enableAutoBuild: !config.autoDeploy,
|
|
424
482
|
environmentVariables: environmentVariables({
|
|
425
483
|
...(reader.environment ?? {}),
|
|
426
484
|
PAPYRUS_SITE_BRAND: config.brand,
|
package/infra/site-config.js
CHANGED
|
@@ -2,6 +2,22 @@ export const READER_REDIRECT_STATUSES = ["301", "302", "200", "404", "404-200"];
|
|
|
2
2
|
export function hasCmsApp(config) {
|
|
3
3
|
return config.cms !== undefined;
|
|
4
4
|
}
|
|
5
|
+
export function resolveGithubRepository(config) {
|
|
6
|
+
if (config.github)
|
|
7
|
+
return { owner: config.github.owner, repo: config.github.repo };
|
|
8
|
+
const [owner, repo] = config.repository.replace("https://github.com/", "").split("/");
|
|
9
|
+
return { owner, repo };
|
|
10
|
+
}
|
|
11
|
+
/** The OIDC `sub` values a GitHub Actions run on `ref` of the site's repository can present. */
|
|
12
|
+
export function githubOidcSubjects(config, ref) {
|
|
13
|
+
const { owner, repo } = resolveGithubRepository(config);
|
|
14
|
+
const subjects = [`repo:${owner}/${repo}:ref:refs/heads/${ref}`];
|
|
15
|
+
const { ownerId, repoId } = config.github ?? {};
|
|
16
|
+
if (ownerId !== undefined && repoId !== undefined) {
|
|
17
|
+
subjects.push(`repo:${owner}@${ownerId}/${repo}@${repoId}:ref:refs/heads/${ref}`);
|
|
18
|
+
}
|
|
19
|
+
return subjects;
|
|
20
|
+
}
|
|
5
21
|
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
6
22
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
7
23
|
const REPOSITORY_PATTERN = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
@@ -302,7 +318,7 @@ function validateOauthRedirectUrls(config) {
|
|
|
302
318
|
}
|
|
303
319
|
export function parseSiteConfig(raw) {
|
|
304
320
|
const record = requireObject(raw, "site");
|
|
305
|
-
rejectUnknownKeys(record, ["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"], "site");
|
|
321
|
+
rejectUnknownKeys(record, ["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "autoDeploy", "github"], "site");
|
|
306
322
|
const siteId = requireString(record.siteId, "siteId", SITE_ID_PATTERN, "kebab-case");
|
|
307
323
|
const repository = requireString(record.repository, "repository", REPOSITORY_PATTERN, "https://github.com/<org>/<repo>");
|
|
308
324
|
const repositoryAccessTokenSecretArn = record.repositoryAccessTokenSecretArn === undefined
|
|
@@ -352,12 +368,15 @@ export function parseSiteConfig(raw) {
|
|
|
352
368
|
if (cms?.stagingPreview !== undefined && cms.staging === false) {
|
|
353
369
|
fail("cms.stagingPreview", "must not be set when cms.staging is false");
|
|
354
370
|
}
|
|
371
|
+
const autoDeploy = record.autoDeploy ?? true;
|
|
372
|
+
if (typeof autoDeploy !== "boolean")
|
|
373
|
+
fail("autoDeploy", "must be a boolean");
|
|
355
374
|
let github;
|
|
356
375
|
if (record.github !== undefined) {
|
|
357
376
|
if (cms === undefined)
|
|
358
377
|
fail("github", "requires cms (the CI role is scoped to the CMS app)");
|
|
359
378
|
const githubRecord = requireObject(record.github, "github");
|
|
360
|
-
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
379
|
+
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra", "ownerId", "repoId"], "github");
|
|
361
380
|
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
362
381
|
const repo = requireString(githubRecord.repo, "github.repo", GITHUB_REPO_PATTERN);
|
|
363
382
|
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
@@ -378,7 +397,18 @@ export function parseSiteConfig(raw) {
|
|
|
378
397
|
const ciCanDeployInfra = githubRecord.ciCanDeployInfra ?? false;
|
|
379
398
|
if (typeof ciCanDeployInfra !== "boolean")
|
|
380
399
|
fail("github.ciCanDeployInfra", "must be a boolean");
|
|
381
|
-
|
|
400
|
+
const optionalId = (value, name) => {
|
|
401
|
+
if (value === undefined)
|
|
402
|
+
return undefined;
|
|
403
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0)
|
|
404
|
+
fail(name, "must be a positive integer (GitHub numeric id)");
|
|
405
|
+
return value;
|
|
406
|
+
};
|
|
407
|
+
const ownerId = optionalId(githubRecord.ownerId, "github.ownerId");
|
|
408
|
+
const repoId = optionalId(githubRecord.repoId, "github.repoId");
|
|
409
|
+
if ((ownerId === undefined) !== (repoId === undefined))
|
|
410
|
+
fail("github", "ownerId and repoId must be set together");
|
|
411
|
+
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra, ...(ownerId !== undefined ? { ownerId, repoId } : {}) };
|
|
382
412
|
}
|
|
383
413
|
const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
|
|
384
414
|
if (hasDomain && hostedZoneId === undefined) {
|
|
@@ -399,6 +429,7 @@ export function parseSiteConfig(raw) {
|
|
|
399
429
|
reader,
|
|
400
430
|
papyrusVersion,
|
|
401
431
|
storagePreviewPrefix,
|
|
432
|
+
autoDeploy: autoDeploy,
|
|
402
433
|
github,
|
|
403
434
|
};
|
|
404
435
|
if (hasCmsApp(config))
|
package/infra/site-config.ts
CHANGED
|
@@ -69,11 +69,25 @@ export type AmplifyAppShellSiteConfig = {
|
|
|
69
69
|
};
|
|
70
70
|
papyrusVersion: string;
|
|
71
71
|
storagePreviewPrefix?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Merge-to-main deploy from GitHub Actions through OIDC. When true (the default) the stack creates the
|
|
74
|
+
* `<siteId>-github-deploy` role and turns off Amplify's own auto-build on the production branches, so the
|
|
75
|
+
* public repository's workflow is the only trigger. Set false for a site whose repository is private:
|
|
76
|
+
* private repositories get no Actions minutes, so Amplify's own repository-connected auto-build stays on.
|
|
77
|
+
*/
|
|
78
|
+
autoDeploy: boolean;
|
|
72
79
|
github?: {
|
|
73
80
|
owner: string;
|
|
74
81
|
repo: string;
|
|
75
82
|
branches: string[];
|
|
76
83
|
ciCanDeployInfra: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* GitHub's numeric ids of the owner and the repository. Repositories with the immutable OIDC subject
|
|
86
|
+
* (`repo:<owner>@<ownerId>/<repo>@<repoId>:ref:...`, see `gh api repos/<owner>/<repo>/actions/oidc/customization/sub`)
|
|
87
|
+
* present that form instead of `repo:<owner>/<repo>:ref:...`; with both ids set, roles trust both forms.
|
|
88
|
+
*/
|
|
89
|
+
ownerId?: number;
|
|
90
|
+
repoId?: number;
|
|
77
91
|
};
|
|
78
92
|
};
|
|
79
93
|
|
|
@@ -83,6 +97,23 @@ export function hasCmsApp(config: AmplifyAppShellSiteConfig): config is CmsSiteC
|
|
|
83
97
|
return config.cms !== undefined;
|
|
84
98
|
}
|
|
85
99
|
|
|
100
|
+
export function resolveGithubRepository(config: Pick<AmplifyAppShellSiteConfig, "repository" | "github">): { owner: string; repo: string } {
|
|
101
|
+
if (config.github) return { owner: config.github.owner, repo: config.github.repo };
|
|
102
|
+
const [owner, repo] = config.repository.replace("https://github.com/", "").split("/");
|
|
103
|
+
return { owner, repo };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The OIDC `sub` values a GitHub Actions run on `ref` of the site's repository can present. */
|
|
107
|
+
export function githubOidcSubjects(config: Pick<AmplifyAppShellSiteConfig, "repository" | "github">, ref: string): string[] {
|
|
108
|
+
const { owner, repo } = resolveGithubRepository(config);
|
|
109
|
+
const subjects = [`repo:${owner}/${repo}:ref:refs/heads/${ref}`];
|
|
110
|
+
const { ownerId, repoId } = config.github ?? {};
|
|
111
|
+
if (ownerId !== undefined && repoId !== undefined) {
|
|
112
|
+
subjects.push(`repo:${owner}@${ownerId}/${repo}@${repoId}:ref:refs/heads/${ref}`);
|
|
113
|
+
}
|
|
114
|
+
return subjects;
|
|
115
|
+
}
|
|
116
|
+
|
|
86
117
|
export const DEFAULT_CI_BRANCHES = ["main", "staging"];
|
|
87
118
|
|
|
88
119
|
const SITE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
@@ -399,7 +430,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
399
430
|
const record = requireObject(raw, "site");
|
|
400
431
|
rejectUnknownKeys(
|
|
401
432
|
record,
|
|
402
|
-
["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "github"],
|
|
433
|
+
["siteId", "repository", "repositoryAccessTokenSecretArn", "brand", "frontend", "hostedZoneId", "stackName", "cms", "reader", "papyrusVersion", "storagePreviewPrefix", "autoDeploy", "github"],
|
|
403
434
|
"site",
|
|
404
435
|
);
|
|
405
436
|
|
|
@@ -452,11 +483,14 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
452
483
|
fail("cms.stagingPreview", "must not be set when cms.staging is false");
|
|
453
484
|
}
|
|
454
485
|
|
|
486
|
+
const autoDeploy = record.autoDeploy ?? true;
|
|
487
|
+
if (typeof autoDeploy !== "boolean") fail("autoDeploy", "must be a boolean");
|
|
488
|
+
|
|
455
489
|
let github: AmplifyAppShellSiteConfig["github"];
|
|
456
490
|
if (record.github !== undefined) {
|
|
457
491
|
if (cms === undefined) fail("github", "requires cms (the CI role is scoped to the CMS app)");
|
|
458
492
|
const githubRecord = requireObject(record.github, "github");
|
|
459
|
-
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra"], "github");
|
|
493
|
+
rejectUnknownKeys(githubRecord, ["owner", "repo", "branches", "ciCanDeployInfra", "ownerId", "repoId"], "github");
|
|
460
494
|
const owner = requireString(githubRecord.owner, "github.owner", GITHUB_OWNER_PATTERN, "plain GitHub login");
|
|
461
495
|
const repo = requireString(githubRecord.repo, "github.repo", GITHUB_REPO_PATTERN);
|
|
462
496
|
if (repository !== `https://github.com/${owner}/${repo}`) {
|
|
@@ -476,7 +510,15 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
476
510
|
}
|
|
477
511
|
const ciCanDeployInfra = githubRecord.ciCanDeployInfra ?? false;
|
|
478
512
|
if (typeof ciCanDeployInfra !== "boolean") fail("github.ciCanDeployInfra", "must be a boolean");
|
|
479
|
-
|
|
513
|
+
const optionalId = (value: unknown, name: string): number | undefined => {
|
|
514
|
+
if (value === undefined) return undefined;
|
|
515
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) fail(name, "must be a positive integer (GitHub numeric id)");
|
|
516
|
+
return value as number;
|
|
517
|
+
};
|
|
518
|
+
const ownerId = optionalId(githubRecord.ownerId, "github.ownerId");
|
|
519
|
+
const repoId = optionalId(githubRecord.repoId, "github.repoId");
|
|
520
|
+
if ((ownerId === undefined) !== (repoId === undefined)) fail("github", "ownerId and repoId must be set together");
|
|
521
|
+
github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean, ...(ownerId !== undefined ? { ownerId, repoId } : {}) };
|
|
480
522
|
}
|
|
481
523
|
|
|
482
524
|
const hasDomain = cms?.domainName !== undefined || reader?.domainName !== undefined;
|
|
@@ -499,6 +541,7 @@ export function parseSiteConfig(raw: unknown): AmplifyAppShellSiteConfig {
|
|
|
499
541
|
reader,
|
|
500
542
|
papyrusVersion,
|
|
501
543
|
storagePreviewPrefix,
|
|
544
|
+
autoDeploy: autoDeploy as boolean,
|
|
502
545
|
github,
|
|
503
546
|
};
|
|
504
547
|
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED