@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.
- package/core.version +1 -1
- package/dist/index.js +341 -212
- package/package.json +1 -1
package/core.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.41.
|
|
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(
|
|
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}.
|
|
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
|
-
|
|
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.
|
|
3778
|
-
|
|
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
|
|
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) {
|