@biffo/cli 0.41.8 → 0.41.10

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.
Files changed (3) hide show
  1. package/core.version +1 -1
  2. package/dist/index.js +341 -212
  3. package/package.json +1 -1
package/core.version CHANGED
@@ -1 +1 @@
1
- 0.41.8
1
+ 0.41.10
package/dist/index.js CHANGED
@@ -51,12 +51,13 @@ function findTemplateRoot(startDir) {
51
51
  dir = parent;
52
52
  }
53
53
  }
54
- function resolveTemplateRoot(fromDir) {
55
- const start = fromDir ?? dirname(fileURLToPath(import.meta.url));
54
+ function resolveTemplateRoot(options = {}) {
55
+ const start = options.fromDir ?? dirname(fileURLToPath(import.meta.url));
56
56
  const root = findTemplateRoot(start);
57
57
  if (!root) {
58
+ const guidance = options.guidance ?? "Point this command at a biffo-template checkout at the target version.";
58
59
  throw new Error(
59
- `Could not locate a Biffo template root (a directory with ${CORE_MANIFEST_FILE} and core.version) above ${start}. Pass --template <path> to a biffo-template checkout at the target version.`
60
+ `Could not locate a Biffo template root (a directory with ${CORE_MANIFEST_FILE} and core.version) above ${start}. ` + guidance
60
61
  );
61
62
  }
62
63
  return root;
@@ -236,6 +237,7 @@ var log = {
236
237
  };
237
238
 
238
239
  // src/commands/core-diff.ts
240
+ var MISSING_TEMPLATE_ROOT_GUIDANCE = "Pass --template <path> to a biffo-template checkout, e.g. `biffo core diff --template /path/to/biffo-template`.";
239
241
  var coreDiffCommand = new Command("diff").description(
240
242
  "Show which template-owned files an upgrade would change in this instance (read-only)"
241
243
  ).option("--cwd <path>", "Instance repo root to inspect (defaults to the current directory)").option(
@@ -253,7 +255,7 @@ var coreDiffCommand = new Command("diff").description(
253
255
  }
254
256
  });
255
257
  async function runCoreDiff(options) {
256
- const templateRoot = options.templateRoot ?? resolveTemplateRoot();
258
+ const templateRoot = options.templateRoot ?? resolveTemplateRoot({ guidance: MISSING_TEMPLATE_ROOT_GUIDANCE });
257
259
  const manifest = readCoreManifest(templateRoot);
258
260
  const templateVersion = readCoreVersionFile(join3(templateRoot, "core.version"));
259
261
  const instanceVersion = readInstanceCoreVersion(options.cwd);
@@ -1508,6 +1510,7 @@ function materializeTemplateAtTag(repo, version, git = defaultGit) {
1508
1510
  }
1509
1511
 
1510
1512
  // src/commands/core-upgrade.ts
1513
+ var MISSING_TEMPLATE_ROOT_GUIDANCE2 = "Pass --template-repo <path> to a biffo-template git checkout, e.g. `biffo core upgrade --template-repo /path/to/biffo-template`.";
1511
1514
  var coreUpgradeCommand = new Command3("upgrade").description("Three-way-merge template-owned files for a core upgrade; preview it or open a PR").option("--cwd <path>", "Instance repo root to upgrade (defaults to the current directory)").option(
1512
1515
  "--template-repo <path>",
1513
1516
  "Path to a biffo-template git checkout whose core-v* tags supply the base/target trees (defaults to the template this CLI ships with)"
@@ -1570,7 +1573,7 @@ async function runCoreUpgrade(options, deps = defaultDeps()) {
1570
1573
  }
1571
1574
  async function runCoreUpgradeResolved(options, deps, cleanups) {
1572
1575
  const materialize = deps.materialize ?? materializeTemplateAtTag;
1573
- const templateRepo = options.templateRepo ? resolve3(options.templateRepo) : resolveTemplateRoot();
1576
+ const templateRepo = options.templateRepo ? resolve3(options.templateRepo) : resolveTemplateRoot({ guidance: MISSING_TEMPLATE_ROOT_GUIDANCE2 });
1574
1577
  const instanceVersion = readInstanceCoreVersion(options.cwd);
1575
1578
  let theirsDir;
1576
1579
  let toVersion;
@@ -2701,6 +2704,287 @@ function registerNonInteractive(command) {
2701
2704
  return command;
2702
2705
  }
2703
2706
 
2707
+ // src/lib/root-sibling.ts
2708
+ var ROOT_SIBLING_NAME = "app";
2709
+ var RESERVED_SIBLING_NAMES = ["admin", "login", ROOT_SIBLING_NAME];
2710
+ function isRootPathPrefix(pathPrefix) {
2711
+ return pathPrefix === "";
2712
+ }
2713
+ function registryNameFor(pathPrefix) {
2714
+ return isRootPathPrefix(pathPrefix) ? ROOT_SIBLING_NAME : pathPrefix;
2715
+ }
2716
+ function displayPath(pathPrefix) {
2717
+ return isRootPathPrefix(pathPrefix) ? "/" : `/${pathPrefix}`;
2718
+ }
2719
+ function basePathFor(pathPrefix) {
2720
+ return isRootPathPrefix(pathPrefix) ? "" : `/${pathPrefix}`;
2721
+ }
2722
+ function rootSiblingProjectName(coreProjectName) {
2723
+ return `${coreProjectName}-app`;
2724
+ }
2725
+ function bucketRegionalDomain(bucketName, region) {
2726
+ return region === "us-east-1" ? `${bucketName}.s3.amazonaws.com` : `${bucketName}.s3.${region}.amazonaws.com`;
2727
+ }
2728
+ function siteBucketName(projectName, environment, accountId) {
2729
+ return `${projectName}-${environment}-site-${accountId}`;
2730
+ }
2731
+ function upsertSiblingOrigin(existing, entry) {
2732
+ return [...existing.filter((s) => s.name !== entry.name), entry];
2733
+ }
2734
+ function serializeRegistry(origins) {
2735
+ return JSON.stringify({ sibling_origins: origins }, null, 2) + "\n";
2736
+ }
2737
+
2738
+ // src/lib/sibling-teardown.ts
2739
+ var SiblingResolutionError = class extends Error {
2740
+ constructor(message) {
2741
+ super(message);
2742
+ this.name = "SiblingResolutionError";
2743
+ }
2744
+ };
2745
+ var REGISTRY_ENVIRONMENTS = ["dev", "staging", "prod"];
2746
+ function registryPath(environment) {
2747
+ return `infra/environments/${environment}/siblings.auto.tfvars.json`;
2748
+ }
2749
+ var REGISTRATION_BRANCH_PREFIX = "biffo/register-sibling-";
2750
+ function parseSiblingBucketDomain(domain) {
2751
+ const host = /^(?<bucket>[a-z0-9][a-z0-9.-]*)\.s3(?:\.[a-z0-9-]+)?\.amazonaws\.com$/.exec(domain);
2752
+ const bucket = host?.groups?.["bucket"];
2753
+ if (!bucket) return null;
2754
+ const parts = /^(?<project>.+)-(?<env>dev|staging|prod)-site-(?<account>\d{12})$/.exec(bucket);
2755
+ const project = parts?.groups?.["project"];
2756
+ const env = parts?.groups?.["env"];
2757
+ const account = parts?.groups?.["account"];
2758
+ if (!project || !env || !account) return null;
2759
+ return { projectName: project, environment: env, accountId: account };
2760
+ }
2761
+ function parseRegistry(contents) {
2762
+ if (contents === void 0 || contents.trim() === "") return [];
2763
+ let parsed;
2764
+ try {
2765
+ parsed = JSON.parse(contents);
2766
+ } catch {
2767
+ throw new SiblingResolutionError(
2768
+ "A siblings.auto.tfvars.json in the core repo is not valid JSON. Refusing to tear down while the sibling registry cannot be read \u2014 fix or remove the file and re-run."
2769
+ );
2770
+ }
2771
+ const origins = parsed.sibling_origins;
2772
+ if (origins === void 0) return [];
2773
+ if (!Array.isArray(origins)) {
2774
+ throw new SiblingResolutionError(
2775
+ "sibling_origins in the core repo is not a list. Refusing to tear down while the sibling registry cannot be read."
2776
+ );
2777
+ }
2778
+ return origins;
2779
+ }
2780
+ function collectSiblings(sources) {
2781
+ const byPrefix = /* @__PURE__ */ new Map();
2782
+ for (const source of sources) {
2783
+ for (const entry of source.entries) {
2784
+ if (!entry?.name || !entry.bucket_regional_domain) {
2785
+ throw new SiblingResolutionError(
2786
+ `A sibling entry in ${registryPath(source.environment)} is missing name or bucket_regional_domain. Refusing to tear down while a registered sibling cannot be identified \u2014 it would be left behind, still billing.`
2787
+ );
2788
+ }
2789
+ const parsed = parseSiblingBucketDomain(entry.bucket_regional_domain);
2790
+ if (!parsed) {
2791
+ throw new SiblingResolutionError(
2792
+ `Could not work out which project owns the sibling "${entry.name}" from its bucket "${entry.bucket_regional_domain}" (in ${registryPath(source.environment)}).
2793
+ Refusing to guess \u2014 tear that sibling down by hand, remove its entry, then re-run.`
2794
+ );
2795
+ }
2796
+ const existing = byPrefix.get(entry.name);
2797
+ if (existing) {
2798
+ if (existing.projectName !== parsed.projectName) {
2799
+ throw new SiblingResolutionError(
2800
+ `The sibling "${entry.name}" maps to two different projects across environments ("${existing.projectName}" and "${parsed.projectName}"). Refusing to guess which repo to delete \u2014 resolve the registry by hand and re-run.`
2801
+ );
2802
+ }
2803
+ if (!existing.environments.includes(parsed.environment)) {
2804
+ existing.environments.push(parsed.environment);
2805
+ }
2806
+ if (source.pendingRegistrationPr === void 0) {
2807
+ existing.registered = true;
2808
+ delete existing.pendingRegistrationPr;
2809
+ }
2810
+ continue;
2811
+ }
2812
+ byPrefix.set(entry.name, {
2813
+ pathPrefix: entry.name,
2814
+ projectName: parsed.projectName,
2815
+ environments: [parsed.environment],
2816
+ accountId: parsed.accountId,
2817
+ registered: source.pendingRegistrationPr === void 0,
2818
+ ...source.pendingRegistrationPr !== void 0 ? { pendingRegistrationPr: source.pendingRegistrationPr } : {}
2819
+ });
2820
+ }
2821
+ }
2822
+ return [...byPrefix.values()].sort((a, b) => a.pathPrefix.localeCompare(b.pathPrefix));
2823
+ }
2824
+ var SIBLING_MARKER_FILE = "biffo.sibling.json";
2825
+ function markerMatches(marker, coreProjectName, sibling) {
2826
+ if (!marker) return false;
2827
+ if (marker.core_project !== coreProjectName) return false;
2828
+ if (marker.path_prefix === sibling.pathPrefix) return true;
2829
+ if (marker.path_prefix === "" && sibling.pathPrefix === ROOT_SIBLING_NAME) return true;
2830
+ return marker.name === sibling.projectName;
2831
+ }
2832
+ async function resolveSiblingRepos(github, coreOrg, coreProjectName, siblings) {
2833
+ const resolved = [];
2834
+ for (const sibling of siblings) {
2835
+ const repo = sibling.projectName;
2836
+ if (!await github.repoExists(coreOrg, repo)) {
2837
+ resolved.push({ ...sibling, org: coreOrg, repo, repoState: "gone" });
2838
+ continue;
2839
+ }
2840
+ const raw = await github.getFileContent(coreOrg, repo, SIBLING_MARKER_FILE);
2841
+ let marker = null;
2842
+ if (raw !== void 0) {
2843
+ try {
2844
+ marker = JSON.parse(raw);
2845
+ } catch {
2846
+ marker = null;
2847
+ }
2848
+ }
2849
+ if (!markerMatches(marker, coreProjectName, sibling)) {
2850
+ throw new SiblingResolutionError(
2851
+ `Refusing to tear down: ${coreOrg}/${repo} is registered as the sibling "${sibling.pathPrefix}" of "${coreProjectName}", but that repo does not carry a matching ${SIBLING_MARKER_FILE}.
2852
+ It may be an unrelated repo that happens to share the name. Deleting a repo cannot be undone, so nothing has been deleted.
2853
+ Delete ${coreOrg}/${repo} by hand if it really is the sibling, remove its entry from the core repo's siblings.auto.tfvars.json, then re-run biffo teardown.`
2854
+ );
2855
+ }
2856
+ resolved.push({ ...sibling, org: coreOrg, repo, repoState: "present" });
2857
+ }
2858
+ return resolved;
2859
+ }
2860
+ async function discoverSiblings(github, coreOrg, coreRepo, coreProjectName) {
2861
+ const sources = [];
2862
+ for (const env of REGISTRY_ENVIRONMENTS) {
2863
+ const contents = await github.getFileContent(coreOrg, coreRepo, registryPath(env));
2864
+ sources.push({ environment: env, entries: parseRegistry(contents) });
2865
+ }
2866
+ const openPrs = await github.listOpenPullRequests(coreOrg, coreRepo);
2867
+ for (const pr of openPrs) {
2868
+ if (!pr.headRef.startsWith(REGISTRATION_BRANCH_PREFIX)) continue;
2869
+ for (const env of REGISTRY_ENVIRONMENTS) {
2870
+ const contents = await github.getFileContent(coreOrg, coreRepo, registryPath(env), pr.headRef);
2871
+ sources.push({
2872
+ environment: env,
2873
+ entries: parseRegistry(contents),
2874
+ pendingRegistrationPr: pr.number
2875
+ });
2876
+ }
2877
+ }
2878
+ const discovered = collectSiblings(sources);
2879
+ return resolveSiblingRepos(github, coreOrg, coreProjectName, discovered);
2880
+ }
2881
+
2882
+ // src/lib/sibling-wiring.ts
2883
+ function cloudfrontDistributionArn(accountId, distributionId) {
2884
+ return `arn:aws:cloudfront::${accountId}:distribution/${distributionId}`;
2885
+ }
2886
+ var REQUIRED_CORE_OUTPUTS = [
2887
+ "cognito_user_pool_id",
2888
+ "cognito_client_id",
2889
+ "api_gateway_url",
2890
+ "portal_url",
2891
+ "cloudfront_distribution_id"
2892
+ ];
2893
+ function coreWiringFromOutputs(outputs, coreAccountId, environment) {
2894
+ const missing = REQUIRED_CORE_OUTPUTS.filter((k) => !outputs[k]?.trim());
2895
+ if (missing.length > 0) {
2896
+ throw new Error(
2897
+ `Cannot wire siblings for ${environment}: the core deploy did not expose ${missing.join(", ")} in its Terraform outputs. A sibling wired without these would build a frontend pointing at no API/Cognito and skip its S3 bucket policy (so / returns 403). Confirm the core actually deployed ${environment} before wiring.`
2898
+ );
2899
+ }
2900
+ const distributionId = outputs["cloudfront_distribution_id"];
2901
+ return {
2902
+ identity: {
2903
+ cognitoUserPoolId: outputs["cognito_user_pool_id"],
2904
+ cognitoClientId: outputs["cognito_client_id"],
2905
+ apiUrl: outputs["api_gateway_url"],
2906
+ portalUrl: outputs["portal_url"]
2907
+ },
2908
+ cdn: {
2909
+ distributionId,
2910
+ distributionArn: cloudfrontDistributionArn(coreAccountId, distributionId)
2911
+ }
2912
+ };
2913
+ }
2914
+ async function setSiblingCoreIdentity(github, org, repo, env, identity) {
2915
+ await github.setEnvVariable(
2916
+ org,
2917
+ repo,
2918
+ env,
2919
+ "CORE_COGNITO_USER_POOL_ID",
2920
+ identity.cognitoUserPoolId
2921
+ );
2922
+ await github.setEnvVariable(org, repo, env, "CORE_COGNITO_CLIENT_ID", identity.cognitoClientId);
2923
+ await github.setEnvVariable(org, repo, env, "CORE_API_URL", identity.apiUrl);
2924
+ await github.setEnvVariable(org, repo, env, "CORE_PORTAL_URL", identity.portalUrl);
2925
+ await github.setEnvVariable(
2926
+ org,
2927
+ repo,
2928
+ env,
2929
+ "CORS_ORIGINS_JSON",
2930
+ JSON.stringify([identity.portalUrl])
2931
+ );
2932
+ }
2933
+ async function wireSiblingEnvironment(github, org, repo, env, wiring) {
2934
+ await setSiblingCoreIdentity(github, org, repo, env, wiring.identity);
2935
+ await github.setEnvVariable(
2936
+ org,
2937
+ repo,
2938
+ env,
2939
+ "PARENT_CLOUDFRONT_DISTRIBUTION_ARN",
2940
+ wiring.cdn.distributionArn
2941
+ );
2942
+ await github.setEnvVariable(
2943
+ org,
2944
+ repo,
2945
+ env,
2946
+ "PARENT_CLOUDFRONT_DISTRIBUTION_ID",
2947
+ wiring.cdn.distributionId
2948
+ );
2949
+ }
2950
+ async function wireSiblingsAfterCoreDeploy(github, coreOrg, coreRepo, coreProjectName, environment, wiring, siblingGithubToken) {
2951
+ const siblings = await discoverSiblings(github, coreOrg, coreRepo, coreProjectName);
2952
+ const result = { wired: [], gone: [], skippedEnv: [] };
2953
+ for (const sibling of siblings) {
2954
+ const slug = `${sibling.org}/${sibling.repo}`;
2955
+ if (sibling.repoState === "gone") {
2956
+ result.gone.push(slug);
2957
+ continue;
2958
+ }
2959
+ await github.setRepoSecret(
2960
+ sibling.org,
2961
+ sibling.repo,
2962
+ "SIBLING_GITHUB_TOKEN",
2963
+ siblingGithubToken
2964
+ );
2965
+ if (!sibling.environments.includes(environment)) {
2966
+ result.skippedEnv.push(slug);
2967
+ continue;
2968
+ }
2969
+ await wireSiblingEnvironment(github, sibling.org, sibling.repo, environment, wiring);
2970
+ result.wired.push(slug);
2971
+ }
2972
+ return result;
2973
+ }
2974
+ function formatWiringResult(environment, result) {
2975
+ const lines = [];
2976
+ for (const slug of result.wired) {
2977
+ lines.push(` Wired sibling ${slug} (${environment}): CORE_*, CORS, PARENT_CLOUDFRONT_*, token`);
2978
+ }
2979
+ for (const slug of result.skippedEnv) {
2980
+ lines.push(` Sibling ${slug}: token set; not registered for ${environment}, env vars skipped`);
2981
+ }
2982
+ for (const slug of result.gone) {
2983
+ lines.push(` Sibling ${slug}: registered but repo not found \u2014 skipped`);
2984
+ }
2985
+ return lines;
2986
+ }
2987
+
2704
2988
  // src/commands/deploy.ts
2705
2989
  var deployCommand = new Command9("deploy").description("Deploy infrastructure and application to an environment").argument("<environment>", "Target environment: dev | staging | prod").option("--infra-only", "Deploy infrastructure only, skip application build").option("--app-only", "Deploy application only, skip Terraform").option("-p, --project <name>", "Project name (overrides biffo.config.json in current directory)").option("-c, --config <path>", "Path to biffo.config.json").option("-y, --yes", "Skip deployment target confirmation").action(
2706
2990
  async (environment, options) => {
@@ -2968,8 +3252,9 @@ async function runDeploy(github, aws, config, environment, options = {}) {
2968
3252
  }
2969
3253
  const reportStep = totalSteps;
2970
3254
  log.step(reportStep, totalSteps, "Reading deployment outputs...");
3255
+ let outputs;
2971
3256
  try {
2972
- const outputs = await aws.readTerraformOutputs(stateBucket, stateKey);
3257
+ outputs = await aws.readTerraformOutputs(stateBucket, stateKey);
2973
3258
  console.log(chalk8.bold("\n Deploy complete!\n"));
2974
3259
  if (outputs.portal_url) console.log(` Portal: ${chalk8.cyan(outputs.portal_url)}`);
2975
3260
  if (outputs.api_gateway_url)
@@ -3006,6 +3291,50 @@ async function runDeploy(github, aws, config, environment, options = {}) {
3006
3291
  console.log(` Actions: ${actionsUrl}
3007
3292
  `);
3008
3293
  }
3294
+ await wireSiblingsIfAny(github, config, environment, awsConfig2.account_id, outputs, options.token);
3295
+ }
3296
+ async function wireSiblingsIfAny(github, config, environment, coreAccountId, outputs, token) {
3297
+ const { org, repo } = config.source_control.config;
3298
+ if (!outputs) {
3299
+ log.warn(
3300
+ `Skipped wiring siblings: this run produced no readable Terraform outputs (e.g. --app-only without a prior infra apply). Re-run \`biffo deploy ${environment}\` once the core infrastructure is applied so its siblings can be wired.`
3301
+ );
3302
+ return;
3303
+ }
3304
+ if (!token) {
3305
+ log.warn(
3306
+ "Skipped wiring siblings: no GitHub token available to set the SIBLING_GITHUB_TOKEN secret."
3307
+ );
3308
+ return;
3309
+ }
3310
+ try {
3311
+ const wiring = coreWiringFromOutputs(outputs, coreAccountId, environment);
3312
+ const result = await wireSiblingsAfterCoreDeploy(
3313
+ github,
3314
+ org,
3315
+ repo,
3316
+ config.project.name,
3317
+ environment,
3318
+ wiring,
3319
+ token
3320
+ );
3321
+ const lines = formatWiringResult(environment, result);
3322
+ if (lines.length > 0) {
3323
+ console.log(chalk8.dim("\n Siblings:"));
3324
+ for (const line of lines) console.log(chalk8.dim(line));
3325
+ console.log();
3326
+ }
3327
+ } catch (err) {
3328
+ if (err instanceof SiblingResolutionError) {
3329
+ log.error(`Could not wire siblings: ${err.message}`);
3330
+ } else {
3331
+ log.error(`Failed to wire siblings to the deployed core: ${err.message}`);
3332
+ }
3333
+ log.error(
3334
+ ` The core deployed, but at least one sibling was not wired. Fix the cause and re-run \`biffo deploy ${environment}\` \u2014 wiring is idempotent.`
3335
+ );
3336
+ process.exit(1);
3337
+ }
3009
3338
  }
3010
3339
  function resolveGithubToken() {
3011
3340
  if (process.env["GITHUB_TOKEN"]) return process.env["GITHUB_TOKEN"];
@@ -3507,37 +3836,6 @@ async function resolveRepoIds(github, config) {
3507
3836
  }
3508
3837
  }
3509
3838
 
3510
- // src/lib/root-sibling.ts
3511
- var ROOT_SIBLING_NAME = "app";
3512
- var RESERVED_SIBLING_NAMES = ["admin", "login", ROOT_SIBLING_NAME];
3513
- function isRootPathPrefix(pathPrefix) {
3514
- return pathPrefix === "";
3515
- }
3516
- function registryNameFor(pathPrefix) {
3517
- return isRootPathPrefix(pathPrefix) ? ROOT_SIBLING_NAME : pathPrefix;
3518
- }
3519
- function displayPath(pathPrefix) {
3520
- return isRootPathPrefix(pathPrefix) ? "/" : `/${pathPrefix}`;
3521
- }
3522
- function basePathFor(pathPrefix) {
3523
- return isRootPathPrefix(pathPrefix) ? "" : `/${pathPrefix}`;
3524
- }
3525
- function rootSiblingProjectName(coreProjectName) {
3526
- return `${coreProjectName}-app`;
3527
- }
3528
- function bucketRegionalDomain(bucketName, region) {
3529
- return region === "us-east-1" ? `${bucketName}.s3.amazonaws.com` : `${bucketName}.s3.${region}.amazonaws.com`;
3530
- }
3531
- function siteBucketName(projectName, environment, accountId) {
3532
- return `${projectName}-${environment}-site-${accountId}`;
3533
- }
3534
- function upsertSiblingOrigin(existing, entry) {
3535
- return [...existing.filter((s) => s.name !== entry.name), entry];
3536
- }
3537
- function serializeRegistry(origins) {
3538
- return JSON.stringify({ sibling_origins: origins }, null, 2) + "\n";
3539
- }
3540
-
3541
3839
  // src/config/sibling-schema.ts
3542
3840
  import { z as z4 } from "zod";
3543
3841
  var SiblingConfigSchema = z4.object({
@@ -3774,12 +4072,11 @@ async function runSiblingCreateCommand(name, options) {
3774
4072
  `
3775
4073
  Next steps:
3776
4074
  1. Merge the registration PR above \u2014 until it merges, baseurl.com${displayPath(pathPrefix)} won't route anywhere.
3777
- 2. Add a SIBLING_GITHUB_TOKEN secret to this new repo (a PAT with repo scope) \u2014 needed by its
3778
- deploy workflow to export Terraform outputs as environment variables, same as the core project.
4075
+ 2. Run \`biffo deploy <env>\` on the CORE project (or re-run it if already deployed). That wires
4076
+ this sibling automatically: the SIBLING_GITHUB_TOKEN secret, the CORE_* identity variables,
4077
+ and \u2014 once the registration PR has merged and the core has redeployed \u2014
4078
+ PARENT_CLOUDFRONT_DISTRIBUTION_ARN. No manual variable/secret setup is needed (issue #337).
3779
4079
  3. Push to \`dev\` (or run the Deploy workflow manually) to provision this sibling's own AWS resources.
3780
- 4. Once the registration PR has ALSO merged and the core project has redeployed, set
3781
- PARENT_CLOUDFRONT_DISTRIBUTION_ARN on this repo and re-run its Deploy workflow \u2014 see this
3782
- repo's README, "The two-phase CDN registration".
3783
4080
  `
3784
4081
  );
3785
4082
  }
@@ -3801,7 +4098,6 @@ async function runSiblingCreate(github, aws, coreAws, git, config, session, opti
3801
4098
  "Core project isn't deployed yet \u2014 deferring its identity (CORE_COGNITO_*, CORE_API_URL)"
3802
4099
  );
3803
4100
  session.outputs.coreIdentity = {};
3804
- markSiblingStepComplete(session, "resolve_core_identity");
3805
4101
  } else if (!session.completedSteps.includes("resolve_core_identity")) {
3806
4102
  log.step(2, totalSteps, "Resolving core project's identity...");
3807
4103
  session.outputs.coreIdentity = await resolveCoreIdentity(
@@ -4060,23 +4356,7 @@ async function configureSiblingGithub(github, config, coreConfig, session, coreI
4060
4356
  for (const env of config.environments) {
4061
4357
  const identity = coreIdentity[env];
4062
4358
  if (!identity) continue;
4063
- await github.setEnvVariable(
4064
- org,
4065
- repo,
4066
- env,
4067
- "CORE_COGNITO_USER_POOL_ID",
4068
- identity.cognitoUserPoolId
4069
- );
4070
- await github.setEnvVariable(org, repo, env, "CORE_COGNITO_CLIENT_ID", identity.cognitoClientId);
4071
- await github.setEnvVariable(org, repo, env, "CORE_API_URL", identity.apiUrl);
4072
- await github.setEnvVariable(org, repo, env, "CORE_PORTAL_URL", identity.portalUrl);
4073
- await github.setEnvVariable(
4074
- org,
4075
- repo,
4076
- env,
4077
- "CORS_ORIGINS_JSON",
4078
- JSON.stringify([identity.portalUrl])
4079
- );
4359
+ await setSiblingCoreIdentity(github, org, repo, env, identity);
4080
4360
  }
4081
4361
  if (session.outputs.oidcRoleArn) {
4082
4362
  await github.setRepoSecret(org, repo, "SIBLING_OIDC_ROLE_ARN", session.outputs.oidcRoleArn);
@@ -4320,12 +4600,7 @@ var initCommand = new Command12("init").description("Scaffold a new project from
4320
4600
  Platform: https://github.com/${org}/${repo}`);
4321
4601
  console.log(` Application: https://github.com/${org}/${appRepo} (serves /)`);
4322
4602
  console.log(
4323
- `
4324
- Next:
4325
- 1. Clone the platform repo and run its first deploy \u2014 /admin and /login come up with it.
4326
- 2. Then deploy the application repo. Until it deploys, / has no content and 404s;
4327
- that window is expected.
4328
- `
4603
+ "\n Next:\n 1. Clone the platform repo and run `biffo deploy dev` \u2014 /admin and /login come up, and\n the same deploy wires the application repo (its CORE_* identity, the parent CloudFront\n ARN, and the SIBLING_GITHUB_TOKEN secret) automatically.\n 2. Then run the application repo's Deploy workflow \u2014 no manual variable/secret setup is\n needed. Until it deploys, / has no content and 404s; that window is expected.\n"
4329
4604
  );
4330
4605
  }
4331
4606
  );
@@ -6160,131 +6435,6 @@ import { GetCallerIdentityCommand as GetCallerIdentityCommand3, STSClient as STS
6160
6435
  import chalk20 from "chalk";
6161
6436
  import { Command as Command22 } from "commander";
6162
6437
  import inquirer8 from "inquirer";
6163
-
6164
- // src/lib/sibling-teardown.ts
6165
- var SiblingResolutionError = class extends Error {
6166
- constructor(message) {
6167
- super(message);
6168
- this.name = "SiblingResolutionError";
6169
- }
6170
- };
6171
- var REGISTRY_ENVIRONMENTS = ["dev", "staging", "prod"];
6172
- function registryPath(environment) {
6173
- return `infra/environments/${environment}/siblings.auto.tfvars.json`;
6174
- }
6175
- var REGISTRATION_BRANCH_PREFIX = "biffo/register-sibling-";
6176
- function parseSiblingBucketDomain(domain) {
6177
- const host = /^(?<bucket>[a-z0-9][a-z0-9.-]*)\.s3(?:\.[a-z0-9-]+)?\.amazonaws\.com$/.exec(domain);
6178
- const bucket = host?.groups?.["bucket"];
6179
- if (!bucket) return null;
6180
- const parts = /^(?<project>.+)-(?<env>dev|staging|prod)-site-(?<account>\d{12})$/.exec(bucket);
6181
- const project = parts?.groups?.["project"];
6182
- const env = parts?.groups?.["env"];
6183
- const account = parts?.groups?.["account"];
6184
- if (!project || !env || !account) return null;
6185
- return { projectName: project, environment: env, accountId: account };
6186
- }
6187
- function parseRegistry(contents) {
6188
- if (contents === void 0 || contents.trim() === "") return [];
6189
- let parsed;
6190
- try {
6191
- parsed = JSON.parse(contents);
6192
- } catch {
6193
- throw new SiblingResolutionError(
6194
- "A siblings.auto.tfvars.json in the core repo is not valid JSON. Refusing to tear down while the sibling registry cannot be read \u2014 fix or remove the file and re-run."
6195
- );
6196
- }
6197
- const origins = parsed.sibling_origins;
6198
- if (origins === void 0) return [];
6199
- if (!Array.isArray(origins)) {
6200
- throw new SiblingResolutionError(
6201
- "sibling_origins in the core repo is not a list. Refusing to tear down while the sibling registry cannot be read."
6202
- );
6203
- }
6204
- return origins;
6205
- }
6206
- function collectSiblings(sources) {
6207
- const byPrefix = /* @__PURE__ */ new Map();
6208
- for (const source of sources) {
6209
- for (const entry of source.entries) {
6210
- if (!entry?.name || !entry.bucket_regional_domain) {
6211
- throw new SiblingResolutionError(
6212
- `A sibling entry in ${registryPath(source.environment)} is missing name or bucket_regional_domain. Refusing to tear down while a registered sibling cannot be identified \u2014 it would be left behind, still billing.`
6213
- );
6214
- }
6215
- const parsed = parseSiblingBucketDomain(entry.bucket_regional_domain);
6216
- if (!parsed) {
6217
- throw new SiblingResolutionError(
6218
- `Could not work out which project owns the sibling "${entry.name}" from its bucket "${entry.bucket_regional_domain}" (in ${registryPath(source.environment)}).
6219
- Refusing to guess \u2014 tear that sibling down by hand, remove its entry, then re-run.`
6220
- );
6221
- }
6222
- const existing = byPrefix.get(entry.name);
6223
- if (existing) {
6224
- if (existing.projectName !== parsed.projectName) {
6225
- throw new SiblingResolutionError(
6226
- `The sibling "${entry.name}" maps to two different projects across environments ("${existing.projectName}" and "${parsed.projectName}"). Refusing to guess which repo to delete \u2014 resolve the registry by hand and re-run.`
6227
- );
6228
- }
6229
- if (!existing.environments.includes(parsed.environment)) {
6230
- existing.environments.push(parsed.environment);
6231
- }
6232
- if (source.pendingRegistrationPr === void 0) {
6233
- existing.registered = true;
6234
- delete existing.pendingRegistrationPr;
6235
- }
6236
- continue;
6237
- }
6238
- byPrefix.set(entry.name, {
6239
- pathPrefix: entry.name,
6240
- projectName: parsed.projectName,
6241
- environments: [parsed.environment],
6242
- accountId: parsed.accountId,
6243
- registered: source.pendingRegistrationPr === void 0,
6244
- ...source.pendingRegistrationPr !== void 0 ? { pendingRegistrationPr: source.pendingRegistrationPr } : {}
6245
- });
6246
- }
6247
- }
6248
- return [...byPrefix.values()].sort((a, b) => a.pathPrefix.localeCompare(b.pathPrefix));
6249
- }
6250
- var SIBLING_MARKER_FILE = "biffo.sibling.json";
6251
- function markerMatches(marker, coreProjectName, sibling) {
6252
- if (!marker) return false;
6253
- if (marker.core_project !== coreProjectName) return false;
6254
- if (marker.path_prefix === sibling.pathPrefix) return true;
6255
- if (marker.path_prefix === "" && sibling.pathPrefix === ROOT_SIBLING_NAME) return true;
6256
- return marker.name === sibling.projectName;
6257
- }
6258
- async function resolveSiblingRepos(github, coreOrg, coreProjectName, siblings) {
6259
- const resolved = [];
6260
- for (const sibling of siblings) {
6261
- const repo = sibling.projectName;
6262
- if (!await github.repoExists(coreOrg, repo)) {
6263
- resolved.push({ ...sibling, org: coreOrg, repo, repoState: "gone" });
6264
- continue;
6265
- }
6266
- const raw = await github.getFileContent(coreOrg, repo, SIBLING_MARKER_FILE);
6267
- let marker = null;
6268
- if (raw !== void 0) {
6269
- try {
6270
- marker = JSON.parse(raw);
6271
- } catch {
6272
- marker = null;
6273
- }
6274
- }
6275
- if (!markerMatches(marker, coreProjectName, sibling)) {
6276
- throw new SiblingResolutionError(
6277
- `Refusing to tear down: ${coreOrg}/${repo} is registered as the sibling "${sibling.pathPrefix}" of "${coreProjectName}", but that repo does not carry a matching ${SIBLING_MARKER_FILE}.
6278
- It may be an unrelated repo that happens to share the name. Deleting a repo cannot be undone, so nothing has been deleted.
6279
- Delete ${coreOrg}/${repo} by hand if it really is the sibling, remove its entry from the core repo's siblings.auto.tfvars.json, then re-run biffo teardown.`
6280
- );
6281
- }
6282
- resolved.push({ ...sibling, org: coreOrg, repo, repoState: "present" });
6283
- }
6284
- return resolved;
6285
- }
6286
-
6287
- // src/commands/teardown.ts
6288
6438
  var teardownCommand = new Command22("teardown").description(
6289
6439
  "Destroy all infrastructure then remove the repo, IAM role, and state bucket \u2014 single command"
6290
6440
  ).option("--project <name>", "Project name to tear down (reads session if omitted)").option("--skip-destroy", "Skip terraform destroy (only use if infrastructure is already gone)").option(
@@ -6594,27 +6744,6 @@ Destroying sibling ${sibling.org}/${sibling.repo} (${env})...`);
6594
6744
  }
6595
6745
  var SIBLING_DESTROY_WORKFLOW = "destroy-infra.yml";
6596
6746
  var SIBLING_DESTROY_WORKFLOW_PATH = `.github/workflows/${SIBLING_DESTROY_WORKFLOW}`;
6597
- async function discoverSiblings(github, coreOrg, coreRepo, coreProjectName) {
6598
- const sources = [];
6599
- for (const env of REGISTRY_ENVIRONMENTS) {
6600
- const contents = await github.getFileContent(coreOrg, coreRepo, registryPath(env));
6601
- sources.push({ environment: env, entries: parseRegistry(contents) });
6602
- }
6603
- const openPrs = await github.listOpenPullRequests(coreOrg, coreRepo);
6604
- for (const pr of openPrs) {
6605
- if (!pr.headRef.startsWith(REGISTRATION_BRANCH_PREFIX)) continue;
6606
- for (const env of REGISTRY_ENVIRONMENTS) {
6607
- const contents = await github.getFileContent(coreOrg, coreRepo, registryPath(env), pr.headRef);
6608
- sources.push({
6609
- environment: env,
6610
- entries: parseRegistry(contents),
6611
- pendingRegistrationPr: pr.number
6612
- });
6613
- }
6614
- }
6615
- const discovered = collectSiblings(sources);
6616
- return resolveSiblingRepos(github, coreOrg, coreProjectName, discovered);
6617
- }
6618
6747
  async function assertSiblingsAreDestroyable(github, siblings) {
6619
6748
  const missing = [];
6620
6749
  for (const sibling of siblings) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.41.8",
3
+ "version": "0.41.10",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",