@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.
@@ -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
- enableAutoBuild: true,
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.map((branch) => `repo:${github.owner}/${github.repo}:ref:refs/heads/${branch}`),
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: true,
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
- enableAutoBuild: true,
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.map(
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: true,
481
+ enableAutoBuild: !config.autoDeploy,
424
482
  environmentVariables: environmentVariables({
425
483
  ...(reader.environment ?? {}),
426
484
  PAPYRUS_SITE_BRAND: config.brand,
@@ -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
- github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra };
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))
@@ -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
- github = { owner, repo, branches, ciCanDeployInfra: ciCanDeployInfra as boolean };
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
@@ -406,7 +406,7 @@
406
406
  "optional": true
407
407
  }
408
408
  },
409
- "version": "5.12.0",
409
+ "version": "5.13.0",
410
410
  "dependencies": {
411
411
  "@aws-amplify/adapter-nextjs": "^1.7.3",
412
412
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.12.0",
2
+ "version": "5.13.0",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",