@biffo/cli 0.41.10 → 0.41.12

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 CHANGED
@@ -1 +1 @@
1
- 0.41.10
1
+ 0.41.12
package/dist/index.js CHANGED
@@ -835,6 +835,39 @@ var GitHubAdapter = class {
835
835
  log.info(`Committed ${files.map((f) => f.path).join(", ")} to ${branch}`);
836
836
  return commit.sha;
837
837
  }
838
+ /** The commit SHA at the head of `branch`. */
839
+ async getBranchSha(org, repo, branch) {
840
+ const ref = await this.waitForRef(org, repo, `heads/${branch}`, 12e4, 3e3);
841
+ return ref.object.sha;
842
+ }
843
+ /**
844
+ * Point `branch` at `sha` via a fast-forward-only ref update (issue #329).
845
+ *
846
+ * `sha` must be an ancestor-descendant of the branch's current head:
847
+ * GitHub's `updateRef` rejects a non-fast-forward move unless `force` is set,
848
+ * and it is deliberately left unset here. The only caller is `writeInstanceFiles`,
849
+ * moving a freshly-created branch onto the instance commit built on that
850
+ * branch's own shared base — always a fast-forward. If it ever isn't, failing
851
+ * loudly is the correct outcome, not clobbering history.
852
+ *
853
+ * Idempotent: a no-op (no API write) when `branch` is already at `sha`, so a
854
+ * resumed init neither errors nor rewrites anything.
855
+ */
856
+ async fastForwardBranch(org, repo, branch, sha) {
857
+ const current = await this.getBranchSha(org, repo, branch);
858
+ if (current === sha) {
859
+ log.info(`${branch} already at ${sha.slice(0, 7)} \u2014 skipping`);
860
+ return;
861
+ }
862
+ await this.octokit.git.updateRef({
863
+ owner: org,
864
+ repo,
865
+ ref: `heads/${branch}`,
866
+ sha,
867
+ force: false
868
+ });
869
+ log.info(`Fast-forwarded ${branch} to ${sha.slice(0, 7)}`);
870
+ }
838
871
  async setDefaultBranch(org, repo, branch) {
839
872
  await this.octokit.repos.update({ owner: org, repo, default_branch: branch });
840
873
  log.info(`Default branch set to ${branch}`);
@@ -1509,6 +1542,12 @@ function materializeTemplateAtTag(repo, version, git = defaultGit) {
1509
1542
  return { dir, cleanup: () => rmSync3(dir, { recursive: true, force: true }) };
1510
1543
  }
1511
1544
 
1545
+ // src/lib/global-workflows.ts
1546
+ var GLOBAL_DISPATCH_REF = "main";
1547
+ var GLOBAL_DISPATCH_WORKFLOW_PATHS = [
1548
+ ".github/workflows/deploy-global.yml"
1549
+ ];
1550
+
1512
1551
  // src/commands/core-upgrade.ts
1513
1552
  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`.";
1514
1553
  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(
@@ -1685,7 +1724,7 @@ async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVe
1685
1724
  head: branch,
1686
1725
  base,
1687
1726
  title: `Upgrade Biffo core ${fromVersion} \u2192 ${toVersion}`,
1688
- body: buildPrBody(fromVersion, toVersion, plan, migrations)
1727
+ body: buildPrBody(fromVersion, toVersion, plan, migrations, base)
1689
1728
  });
1690
1729
  if (plan.conflicts.length > 0) {
1691
1730
  log.warn(`PR opened with ${plan.conflicts.length} conflict(s) to resolve: ${pr.url}`);
@@ -1693,7 +1732,7 @@ async function applyAndOpenPr(options, deps, plan, migrations, fromVersion, toVe
1693
1732
  log.success(`Opened PR #${pr.number}: ${pr.url}`);
1694
1733
  }
1695
1734
  }
1696
- function buildPrBody(from, to, plan, migrations) {
1735
+ function buildPrBody(from, to, plan, migrations, base = GLOBAL_DISPATCH_REF) {
1697
1736
  const lines = [
1698
1737
  "Automated core upgrade generated by `biffo core upgrade` (ADR-0006).",
1699
1738
  "",
@@ -1720,6 +1759,21 @@ function buildPrBody(from, to, plan, migrations) {
1720
1759
  "Review the DDL before merging: merging this PR runs these migrations against the database on the next deploy."
1721
1760
  );
1722
1761
  }
1762
+ const globalWorkflowChanges = plan.changes.filter(
1763
+ (c) => GLOBAL_DISPATCH_WORKFLOW_PATHS.includes(c.path)
1764
+ );
1765
+ if (base !== GLOBAL_DISPATCH_REF && globalWorkflowChanges.length > 0) {
1766
+ lines.push(
1767
+ "",
1768
+ `## \u26A0 Promotion required \u2014 global workflow change (issue #328)`,
1769
+ "",
1770
+ `This upgrade changes ${globalWorkflowChanges.length} workflow(s) that \`biffo deploy\` dispatches from \`${GLOBAL_DISPATCH_REF}\`, but this PR targets \`${base}\`:`,
1771
+ "",
1772
+ ...globalWorkflowChanges.map((c) => `- \`${c.path}\``),
1773
+ "",
1774
+ `Merging here lands the fix on \`${base}\` \u2014 **but the deploy runs it from \`${GLOBAL_DISPATCH_REF}\`.** Until you promote \`${base}\` \u2192 \`${GLOBAL_DISPATCH_REF}\` (open a PR from \`${base}\` into \`${GLOBAL_DISPATCH_REF}\` and merge it), the old workflow keeps running and this fix will not take effect.`
1775
+ );
1776
+ }
1723
1777
  if (plan.conflicts.length > 0) {
1724
1778
  lines.push(
1725
1779
  "",
@@ -3124,6 +3178,61 @@ async function reportOidcHint(github, org, repo, runId) {
3124
3178
  log.error(" Re-run `biffo init` to rewrite the trust policy with both patterns.");
3125
3179
  log.error("");
3126
3180
  }
3181
+ async function readRemoteCoreVersion(github, org, repo, ref) {
3182
+ const record = await github.getFileContent(org, repo, INSTANCE_CORE_FILE, ref);
3183
+ if (record) {
3184
+ try {
3185
+ const parsed = JSON.parse(record);
3186
+ if (typeof parsed.version === "string") {
3187
+ parseCoreVersion(parsed.version);
3188
+ return parsed.version;
3189
+ }
3190
+ } catch {
3191
+ }
3192
+ }
3193
+ const inherited = await github.getFileContent(org, repo, CORE_VERSION_FILE, ref);
3194
+ if (inherited) {
3195
+ try {
3196
+ const version = inherited.trim();
3197
+ parseCoreVersion(version);
3198
+ return version;
3199
+ } catch {
3200
+ return null;
3201
+ }
3202
+ }
3203
+ return null;
3204
+ }
3205
+ async function warnIfDispatchRefStale(github, org, repo, workflowFile, dispatchRef, deployBranch) {
3206
+ if (dispatchRef === deployBranch) return;
3207
+ let refVersion;
3208
+ let branchVersion;
3209
+ try {
3210
+ ;
3211
+ [refVersion, branchVersion] = await Promise.all([
3212
+ readRemoteCoreVersion(github, org, repo, dispatchRef),
3213
+ readRemoteCoreVersion(github, org, repo, deployBranch)
3214
+ ]);
3215
+ } catch {
3216
+ return;
3217
+ }
3218
+ if (!refVersion || !branchVersion) return;
3219
+ if (compareCoreVersions(refVersion, branchVersion) >= 0) return;
3220
+ log.warn("");
3221
+ log.warn(
3222
+ ` ${workflowFile} is dispatched from '${dispatchRef}', but '${dispatchRef}' is on core ${refVersion} while '${deployBranch}' (deploying now) is on core ${branchVersion}.`
3223
+ );
3224
+ log.warn(
3225
+ ` A core upgrade landed on '${deployBranch}' that has not reached '${dispatchRef}'. This deploy`
3226
+ );
3227
+ log.warn(
3228
+ ` will run the OLDER ${workflowFile} from '${dispatchRef}', so template fixes to it will NOT`
3229
+ );
3230
+ log.warn(
3231
+ ` take effect. Promote '${deployBranch}' \u2192 '${dispatchRef}' (open a PR from '${deployBranch}'`
3232
+ );
3233
+ log.warn(` into '${dispatchRef}' and merge it), then re-run this deploy. (issue #328)`);
3234
+ log.warn("");
3235
+ }
3127
3236
  async function runDeploy(github, aws, config, environment, options = {}) {
3128
3237
  const { org, repo } = config.source_control.config;
3129
3238
  const awsConfig2 = config.cloud.config;
@@ -3165,8 +3274,16 @@ async function runDeploy(github, aws, config, environment, options = {}) {
3165
3274
  if (!skipInfra && hasGlobalInfra) {
3166
3275
  const globalLabel = dns.mode === "external" ? "Requesting SSL certificate for external DNS..." : "Deploying global infrastructure (DNS + SSL certificate)...";
3167
3276
  log.step(2, totalSteps, globalLabel);
3277
+ await warnIfDispatchRefStale(
3278
+ github,
3279
+ org,
3280
+ repo,
3281
+ "deploy-global.yml",
3282
+ GLOBAL_DISPATCH_REF,
3283
+ branch
3284
+ );
3168
3285
  const globalBaselineId = await github.getLatestWorkflowRunId(org, repo, "deploy-global.yml");
3169
- await github.triggerWorkflow(org, repo, "deploy-global.yml", {}, "main");
3286
+ await github.triggerWorkflow(org, repo, "deploy-global.yml", {}, GLOBAL_DISPATCH_REF);
3170
3287
  if (dns.mode === "external") {
3171
3288
  log.info(" DNS will not be changed automatically.");
3172
3289
  log.info(" The workflow summary will list ACM validation CNAMEs to add manually.");
@@ -3181,7 +3298,7 @@ async function runDeploy(github, aws, config, environment, options = {}) {
3181
3298
  globalBaselineId,
3182
3299
  36e5,
3183
3300
  3e4,
3184
- "main"
3301
+ GLOBAL_DISPATCH_REF
3185
3302
  );
3186
3303
  if (globalResult.conclusion !== "success") {
3187
3304
  log.error(`Global infrastructure deploy ${globalResult.conclusion ?? "failed"}.`);
@@ -4814,7 +4931,12 @@ function appSiblingRegistryFiles(config) {
4814
4931
  }));
4815
4932
  }
4816
4933
  var INSTANCE_CONFIG_FILE = "biffo.config.json";
4817
- var INSTANCE_FILE_BRANCHES = ["main", "dev", "staging"];
4934
+ var INSTANCE_FILE_BASE_BRANCH = "main";
4935
+ var INSTANCE_FILE_FOLLOWER_BRANCHES = ["dev", "staging"];
4936
+ var INSTANCE_FILE_BRANCHES = [
4937
+ INSTANCE_FILE_BASE_BRANCH,
4938
+ ...INSTANCE_FILE_FOLLOWER_BRANCHES
4939
+ ];
4818
4940
  async function writeInstanceFiles(github, org, repo, config) {
4819
4941
  const files = [
4820
4942
  { path: INSTANCE_CORE_FILE, content: serializeInstanceCoreVersion(getLatestCoreVersion()) },
@@ -4822,8 +4944,10 @@ async function writeInstanceFiles(github, org, repo, config) {
4822
4944
  ...config ? appSiblingRegistryFiles(config) : []
4823
4945
  ];
4824
4946
  const message = config ? `chore: record core version, register the ${ROOT_SIBLING_NAME} sibling, and drop the template ${INSTANCE_CONFIG_FILE}` : `chore: record core version and drop the template ${INSTANCE_CONFIG_FILE}`;
4825
- for (const branch of INSTANCE_FILE_BRANCHES) {
4826
- await github.commitFiles(org, repo, branch, files, message);
4947
+ const committed = await github.commitFiles(org, repo, INSTANCE_FILE_BASE_BRANCH, files, message);
4948
+ const sharedSha = committed ?? await github.getBranchSha(org, repo, INSTANCE_FILE_BASE_BRANCH);
4949
+ for (const branch of INSTANCE_FILE_FOLLOWER_BRANCHES) {
4950
+ await github.fastForwardBranch(org, repo, branch, sharedSha);
4827
4951
  }
4828
4952
  }
4829
4953
  function printPlan2(config) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.41.10",
3
+ "version": "0.41.12",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",