@reddoorla/maintenance 0.94.0 → 0.95.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.
Files changed (106) hide show
  1. package/README.md +31 -0
  2. package/dist/announce-PGT4SZZB.js +1 -1
  3. package/dist/{blux-NXQLMS4W.js → blux-4KNXUP2K.js} +1 -1
  4. package/dist/{bump-deps-6OR7LL4E.js → bump-deps-C5G2EFBB.js} +5 -5
  5. package/dist/{chunk-LS6QPANO.js → chunk-2LYOAB2E.js} +2 -2
  6. package/dist/chunk-6VKYQATC.js +1708 -0
  7. package/dist/chunk-6VKYQATC.js.map +1 -0
  8. package/dist/{chunk-XTK5VIQB.js → chunk-AVEQUR44.js} +48 -1
  9. package/dist/chunk-AVEQUR44.js.map +1 -0
  10. package/dist/{chunk-B6NUZ2ZX.js → chunk-BCKEP5S6.js} +2 -2
  11. package/dist/{chunk-5FCBYK6L.js → chunk-BVEDKMNG.js} +10 -10
  12. package/dist/{chunk-PPIZHDJL.js → chunk-CJ2RMTT7.js} +2 -2
  13. package/dist/{chunk-7CJ3DVZW.js → chunk-CWXTNPNZ.js} +2 -2
  14. package/dist/{chunk-TGUESYGF.js → chunk-DFKBTBO3.js} +2 -2
  15. package/dist/{chunk-WHWVYL7B.js → chunk-FSGICFXB.js} +7 -32
  16. package/dist/chunk-FSGICFXB.js.map +1 -0
  17. package/dist/{chunk-3HS6LPFX.js → chunk-FUWFLAWQ.js} +4 -4
  18. package/dist/{chunk-TCF6S7AD.js → chunk-LM2NHRCJ.js} +2 -2
  19. package/dist/{chunk-NTLSMJHW.js → chunk-S7RYFUIK.js} +2 -2
  20. package/dist/{chunk-MLT4QY5X.js → chunk-SLFG7Y76.js} +1 -1
  21. package/dist/chunk-SLFG7Y76.js.map +1 -0
  22. package/dist/{chunk-DHVJ7H2F.js → chunk-TR3HFH6J.js} +5 -4
  23. package/dist/chunk-TR3HFH6J.js.map +1 -0
  24. package/dist/{chunk-VELWHWOW.js → chunk-WJMPCVOR.js} +34 -6
  25. package/dist/chunk-WJMPCVOR.js.map +1 -0
  26. package/dist/{chunk-4R3YAQNV.js → chunk-WSJ7RO54.js} +2 -2
  27. package/dist/{chunk-ZTPPCHDB.js → chunk-ZQV7UQNE.js} +2 -2
  28. package/dist/cli/bin.js +31 -18
  29. package/dist/cli/bin.js.map +1 -1
  30. package/dist/cli/commands/audit.d.ts +1 -1
  31. package/dist/cli/commands/audit.js +2 -2
  32. package/dist/{convert-to-pnpm-3RVGUBBR.js → convert-to-pnpm-YTFREUJK.js} +5 -5
  33. package/dist/{db-FL7ZBF6H.js → db-BBEM7CBX.js} +1 -1
  34. package/dist/{digest-TZ3QQOSF.js → digest-65BOZ7AT.js} +2 -2
  35. package/dist/digest-collectors-EHFY2OQC.js +1 -1
  36. package/dist/{health-endpoint-XLNBWVHK.js → health-endpoint-KVCJYWET.js} +5 -5
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.js +22 -20
  39. package/dist/index.js.map +1 -1
  40. package/dist/{init-F5OBBWA5.js → init-FFE4FCO2.js} +12 -12
  41. package/dist/{launch-SUTMI4NG.js → launch-4RVWPZGI.js} +218 -4
  42. package/dist/launch-4RVWPZGI.js.map +1 -0
  43. package/dist/match-harness-2Q6ISEES.js +70 -0
  44. package/dist/match-harness-2Q6ISEES.js.map +1 -0
  45. package/dist/{onboard-NKE6GZKL.js → onboard-TM2KESGF.js} +5 -5
  46. package/dist/orchestrate-MWXENQOS.js +1 -1
  47. package/dist/{pipeline-PRMD4EJT.js → pipeline-YROAY6DT.js} +3 -3
  48. package/dist/preflight-4VQP7JJL.js +1 -1
  49. package/dist/{prismic-ci-QIO3I4KB.js → prismic-ci-6GKKZGB5.js} +12 -10
  50. package/dist/prismic-ci-6GKKZGB5.js.map +1 -0
  51. package/dist/{prismic-models-RWDVKZ2W.js → prismic-models-W6CNLTWH.js} +3 -3
  52. package/dist/prospect/types.d.ts +1 -1
  53. package/dist/prospect/types.js +1 -1
  54. package/dist/{prospect-audit-CIL3NCPM.js → prospect-audit-X4Z67LWW.js} +3 -3
  55. package/dist/recipes/sync-configs.d.ts +1 -1
  56. package/dist/recipes/sync-configs.js +4 -4
  57. package/dist/{render-ZUQBJYJR.js → render-YGNGTX3W.js} +3 -3
  58. package/dist/render-from-row-IFSBHWDH.js +1 -1
  59. package/dist/{report-6FFHGUUS.js → report-4GFD4MW2.js} +3 -3
  60. package/dist/{self-updating-5DVDIEHB.js → self-updating-MWXIZLL7.js} +5 -5
  61. package/dist/{selftest-LJYXBIO5.js → selftest-MAB2PBXQ.js} +5 -5
  62. package/dist/{smoke-suite-NUTC7IUK.js → smoke-suite-7DGITCTZ.js} +5 -5
  63. package/dist/{svelte-codemods-U4NIT6CV.js → svelte-codemods-WSV2FGNG.js} +5 -5
  64. package/dist/{sync-configs-S2JYY6QU.js → sync-configs-RRIHNLIJ.js} +6 -6
  65. package/dist/{types-OIQZ5hWN.d.ts → types-D_bpy89i.d.ts} +1 -1
  66. package/dist/{upgrade-YFZCMUPE.js → upgrade-VTD6V4HS.js} +5 -5
  67. package/dist/util/git.d.ts +46 -1
  68. package/dist/util/git.js +7 -1
  69. package/package.json +1 -1
  70. package/dist/chunk-DHVJ7H2F.js.map +0 -1
  71. package/dist/chunk-MLT4QY5X.js.map +0 -1
  72. package/dist/chunk-VELWHWOW.js.map +0 -1
  73. package/dist/chunk-WHWVYL7B.js.map +0 -1
  74. package/dist/chunk-XTK5VIQB.js.map +0 -1
  75. package/dist/launch-SUTMI4NG.js.map +0 -1
  76. package/dist/prismic-ci-QIO3I4KB.js.map +0 -1
  77. /package/dist/{blux-NXQLMS4W.js.map → blux-4KNXUP2K.js.map} +0 -0
  78. /package/dist/{bump-deps-6OR7LL4E.js.map → bump-deps-C5G2EFBB.js.map} +0 -0
  79. /package/dist/{chunk-LS6QPANO.js.map → chunk-2LYOAB2E.js.map} +0 -0
  80. /package/dist/{chunk-B6NUZ2ZX.js.map → chunk-BCKEP5S6.js.map} +0 -0
  81. /package/dist/{chunk-5FCBYK6L.js.map → chunk-BVEDKMNG.js.map} +0 -0
  82. /package/dist/{chunk-PPIZHDJL.js.map → chunk-CJ2RMTT7.js.map} +0 -0
  83. /package/dist/{chunk-7CJ3DVZW.js.map → chunk-CWXTNPNZ.js.map} +0 -0
  84. /package/dist/{chunk-TGUESYGF.js.map → chunk-DFKBTBO3.js.map} +0 -0
  85. /package/dist/{chunk-3HS6LPFX.js.map → chunk-FUWFLAWQ.js.map} +0 -0
  86. /package/dist/{chunk-TCF6S7AD.js.map → chunk-LM2NHRCJ.js.map} +0 -0
  87. /package/dist/{chunk-NTLSMJHW.js.map → chunk-S7RYFUIK.js.map} +0 -0
  88. /package/dist/{chunk-4R3YAQNV.js.map → chunk-WSJ7RO54.js.map} +0 -0
  89. /package/dist/{chunk-ZTPPCHDB.js.map → chunk-ZQV7UQNE.js.map} +0 -0
  90. /package/dist/{convert-to-pnpm-3RVGUBBR.js.map → convert-to-pnpm-YTFREUJK.js.map} +0 -0
  91. /package/dist/{db-FL7ZBF6H.js.map → db-BBEM7CBX.js.map} +0 -0
  92. /package/dist/{digest-TZ3QQOSF.js.map → digest-65BOZ7AT.js.map} +0 -0
  93. /package/dist/{health-endpoint-XLNBWVHK.js.map → health-endpoint-KVCJYWET.js.map} +0 -0
  94. /package/dist/{init-F5OBBWA5.js.map → init-FFE4FCO2.js.map} +0 -0
  95. /package/dist/{onboard-NKE6GZKL.js.map → onboard-TM2KESGF.js.map} +0 -0
  96. /package/dist/{pipeline-PRMD4EJT.js.map → pipeline-YROAY6DT.js.map} +0 -0
  97. /package/dist/{prismic-models-RWDVKZ2W.js.map → prismic-models-W6CNLTWH.js.map} +0 -0
  98. /package/dist/{prospect-audit-CIL3NCPM.js.map → prospect-audit-X4Z67LWW.js.map} +0 -0
  99. /package/dist/{render-ZUQBJYJR.js.map → render-YGNGTX3W.js.map} +0 -0
  100. /package/dist/{report-6FFHGUUS.js.map → report-4GFD4MW2.js.map} +0 -0
  101. /package/dist/{self-updating-5DVDIEHB.js.map → self-updating-MWXIZLL7.js.map} +0 -0
  102. /package/dist/{selftest-LJYXBIO5.js.map → selftest-MAB2PBXQ.js.map} +0 -0
  103. /package/dist/{smoke-suite-NUTC7IUK.js.map → smoke-suite-7DGITCTZ.js.map} +0 -0
  104. /package/dist/{svelte-codemods-U4NIT6CV.js.map → svelte-codemods-WSV2FGNG.js.map} +0 -0
  105. /package/dist/{sync-configs-S2JYY6QU.js.map → sync-configs-RRIHNLIJ.js.map} +0 -0
  106. /package/dist/{upgrade-YFZCMUPE.js.map → upgrade-VTD6V4HS.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/recipes/self-updating/index.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { templatesByName } from \"../sync-configs/templates.js\";\nimport {\n renovateActionGaps,\n withRenovatePinsFrom,\n RENOVATE_ACTION_CONFIG,\n} from \"../sync-configs/renovate-action.js\";\nimport {\n getRemoteUrl,\n parseOwnerRepo,\n isOwnerRepo,\n push as gitPush,\n branchName,\n checkoutBranch,\n createBranch,\n currentBranch,\n commit as gitCommit,\n isWorkingTreeClean,\n} from \"../../util/git.js\";\nimport { siteLabel } from \"../../util/site.js\";\nimport { readGitHubConfig } from \"../../github/config.js\";\nimport { makeGitHub, type GitHub } from \"../../github/gh.js\";\nimport {\n FLEET_RULESET_NAME,\n desiredRuleset,\n healRuleset,\n rulesetGaps,\n} from \"../../github/rulesets.js\";\n\n// Deliberately NOT ci.yml: that file is per-site parameterized (netlify-site,\n// node-version, permissions), so an exact-match heal against a static template\n// would strip those values in a green auto-mergeable PR — the armed clobber\n// the 2026-08-02 architecture review found. The starter clone owns ci.yml's\n// shape; Renovate bumps its pinned reusable-workflow ref per repo (proven:\n// its github-actions manager already updates action pins on fleet sites, and\n// reddoorla/.github publishes the tags it tracks).\nconst SELF_UPDATING_CONFIGS = [\"renovate-action\", \"renovate-config\"] as const;\n\n// Reusable-workflow jobs report their check as \"<caller-job> / <reusable-job>\".\n// The thin `ci` caller (job `ci`) calls reddoorla/.github's reusable workflow (job `ci`),\n// so the required context is \"ci / ci\", NOT \"ci\". (Provisional — verified empirically on the\n// starter during M7.1 rollout; correct here if the live checks API reports a different string.)\nconst REQUIRED_CHECK = \"ci / ci\";\n\nexport type SelfUpdatingDeps = {\n github?: GitHub;\n pushBranch?: (cwd: string, branch: string) => Promise<void>;\n};\n\nfunction resultOf(\n site: Site,\n status: RecipeResult[\"status\"],\n notes: string,\n commits: string[] = [],\n): RecipeResult {\n return { recipe: \"self-updating\", site: siteLabel(site), status, commits, notes };\n}\n\n/** True when a file's current content matches the canonical template for drift\n * purposes. Normalizes line endings + trailing whitespace so a stray trailing\n * newline (or CRLF) can't read as drift and open a needless PR every nightly run;\n * any real content change still differs. */\nfunction sameConfigContents(current: string, canonical: string): boolean {\n const norm = (s: string) => s.replace(/\\r\\n/g, \"\\n\").replace(/\\s+$/, \"\");\n return norm(current) === norm(canonical);\n}\n\n/**\n * Resolve the `owner/repo` this recipe will mutate. An explicit `site.gitRepo`\n * (from Airtable) wins; otherwise derive it from the checkout's `origin`.\n *\n * Returns `null` when there is no repo to act on (no `gitRepo`, no origin) — a\n * benign \"nothing wired\" state. THROWS when a repo value IS present but doesn't\n * match the strict `owner/repo` shape: this recipe writes the broad GitHub\n * token as a repo secret (plus branch protection / auto-merge) at this\n * identity, so an attacker/typo-controlled value must be rejected here, before\n * the first `gh` call, rather than passed through to `gh`.\n */\nasync function resolveRepo(site: Site): Promise<string | null> {\n if (site.gitRepo) {\n if (!isOwnerRepo(site.gitRepo)) {\n throw new Error(\n `refusing to act on malformed repo identity: expected \"owner/repo\", got ${JSON.stringify(site.gitRepo)}`,\n );\n }\n return site.gitRepo;\n }\n let fromOrigin: string | null;\n try {\n fromOrigin = parseOwnerRepo(await getRemoteUrl(site.path));\n } catch {\n return null;\n }\n if (fromOrigin === null) return null;\n if (!isOwnerRepo(fromOrigin)) {\n throw new Error(\n `refusing to act on malformed repo identity from origin: ${JSON.stringify(fromOrigin)}`,\n );\n }\n return fromOrigin;\n}\n\nexport async function selfUpdating(site: Site, deps: SelfUpdatingDeps = {}): Promise<RecipeResult> {\n const templates = templatesByName([...SELF_UPDATING_CONFIGS]);\n\n let repo: string | null;\n try {\n repo = await resolveRepo(site);\n } catch (err) {\n // A malformed repo identity must abort before any `gh` write — surface it\n // as a recipe failure rather than letting the token reach an unintended repo.\n return resultOf(site, \"failed\", err instanceof Error ? err.message : String(err));\n }\n if (!repo) {\n return resultOf(\n site,\n \"failed\",\n \"no Git repo (set Airtable 'Git repo' or add an origin remote)\",\n );\n }\n\n const cfg = readGitHubConfig();\n if (!deps.github && !cfg) return resultOf(site, \"failed\", \"GITHUB_TOKEN not set\");\n const github = deps.github ?? makeGitHub({ token: cfg!.token });\n\n const base = await github.defaultBranch(repo).catch(() => \"main\");\n const actions: string[] = [];\n const commits: string[] = [];\n // Hoisted so the `finally` can restore the operator's branch even when a\n // failure (push/PR error) aborts AFTER we created + checked out the maint\n // branch. `maintBranch` stays null until createBranch succeeds, so the\n // restore is a no-op on every path that never left the operator's branch.\n let original: string | null = null;\n let maintBranch: string | null = null;\n\n try {\n // A. Renovate config on the default branch — compare CONTENT, not just\n // existence, so a present-but-STALE config (a pre-App workflow, a drifted\n // Renovate schedule window) is corrected rather than left silently\n // forever. The prior existence-only gate reported \"already self-updating\" for any\n // repo that merely HAD the files, however out of date — the very drift this\n // recipe exists to repair (e.g. the Renovate schedule-window regression).\n //\n // renovate.yml specifically is compliance-checked rather than byte-matched\n // (see renovateActionGaps / withRenovatePinsFrom in\n // ../sync-configs/renovate-action.js): Renovate legitimately bumps its own\n // digest pins forward, and a site's prettier may legitimately quote the\n // cron / RENOVATE_* scalars differently — neither is real drift (issue\n // #651; a byte-match recipe opened reddoor-starter-blux#1, a PR that\n // DOWNGRADED Renovate's own pin).\n const drifted: string[] = [];\n // Captured so the write loop below can carry a drifted renovate.yml's own\n // pins forward instead of re-fetching (and so a compliant-but-non-byte-\n // identical file's contents are available for that carry-forward too,\n // though it won't need healing).\n const currentContents = new Map<string, string | null>();\n for (const t of templates) {\n const current = await github.fileContentsOnBranch(repo, base, t.path);\n currentContents.set(t.path, current);\n if (current === null) {\n drifted.push(t.path);\n continue;\n }\n if (sameConfigContents(current, t.contents)) continue;\n if (t.config === RENOVATE_ACTION_CONFIG && renovateActionGaps(current).length === 0) {\n continue;\n }\n drifted.push(t.path);\n }\n if (drifted.length > 0) {\n const existingPR = await github.findOpenSelfUpdatingPR(repo);\n if (existingPR) {\n actions.push(`self-updating PR already open: ${existingPR}`);\n } else {\n if (!(await isWorkingTreeClean(site.path))) {\n return resultOf(site, \"failed\", \"working tree not clean — commit or stash first\");\n }\n // Capture the operator's branch BEFORE creating the maint branch so the\n // `finally` can return them to it (#2). Best-effort capture: if it fails\n // we just skip the restore rather than guess.\n try {\n original = await currentBranch(site.path);\n } catch {\n original = null;\n }\n maintBranch = branchName(\"self-updating\");\n await createBranch(site.path, maintBranch);\n // Write only the paths that actually drifted — a repo whose\n // renovate.json is stale but whose renovate.yml already passes\n // `renovateActionGaps` must not have that compliant renovate.yml\n // silently rewritten (e.g. un-quoting its scalars) in the same PR.\n // On a fresh repo every template path is missing, so `drifted`\n // already contains all of them and bootstrap is unaffected.\n for (const t of templates) {\n if (!drifted.includes(t.path)) continue;\n const dest = join(site.path, t.path);\n await mkdir(dirname(dest), { recursive: true });\n // For a genuinely non-compliant renovate.yml, carry the site's own\n // (still-digest-pinned) action refs forward onto the template before\n // writing it — writing the template verbatim would put its OLDER\n // pins over the site's newer ones, re-introducing the downgrade\n // this compliance check exists to prevent (issue #651).\n const contents =\n t.config === RENOVATE_ACTION_CONFIG\n ? withRenovatePinsFrom(t.contents, currentContents.get(t.path) ?? null)\n : t.contents;\n await writeFile(dest, contents, \"utf-8\");\n }\n const sha = await gitCommit(site.path, \"ci: enable self-updating (Renovate auto-merge)\");\n if (sha) commits.push(sha);\n await (deps.pushBranch ?? gitPush)(site.path, maintBranch);\n const pr = await github.openPullRequest(repo, {\n head: maintBranch,\n base,\n title: \"Enable self-updating (Renovate)\",\n body: \"Adds scheduled Renovate and Renovate-owned merges for patch/minor updates. (ci.yml is deliberately not managed here — its shape comes from the starter and its reusable-workflow pin from Renovate.)\",\n });\n actions.push(`opened PR ${pr.url}`);\n // (Branch restore happens in the `finally` below — on success AND on a\n // failure that aborts after createBranch.)\n }\n }\n\n // B. Repo settings — check-then-ensure, each independent (self-healing).\n //\n // Platform auto-merge is DISABLED fleet-wide, and this block self-heals toward\n // OFF (it used to drive repos ON — do not \"fix\" it back).\n //\n // Why: GitHub's platform auto-merge is a per-PR flag that anyone with write\n // access can arm; the PR then merges itself later, unattended, once checks go\n // green. The org Renovate preset forbids auto-merging majors and was working\n // correctly — Renovate never armed them — but the preset has no authority over\n // a flag someone else set. On 2026-07-26 a bulk `gh pr merge --auto` sweep\n // armed it across 8 fleet repos, and the next morning two `actions/checkout`\n // MAJOR bumps merged with zero reviews (reddoor-starter#77,\n // reddoor-website#111) as soon as Renovate's own rebase cleared their\n // conflicts. Realized damage, not a hypothetical.\n //\n // The fix is to keep merges INSIDE Renovate's own run, where its `packageRules`\n // actually govern the decision. Renovate can only merge while it is running, so\n // the workflow cron (see sync-configs templates) is the merge cadence.\n //\n // Leaving the self-heal pointed at OFF makes this a drift ALARM: if someone\n // re-enables platform auto-merge, the next `self-updating` / `launch` run turns\n // it back off and records the action. `enableRepoAutoMerge` is retained in\n // `src/github/gh.ts` as the documented rollback path.\n if (await github.autoMergeEnabled(repo)) {\n await github.disableRepoAutoMerge(repo);\n actions.push(\"disabled auto-merge (platform auto-merge is not permitted fleet-wide)\");\n }\n const existingContexts = await github.branchProtectionContexts(repo, base);\n if (!existingContexts.includes(REQUIRED_CHECK)) {\n // protectBranch issues a full PUT that REPLACES required-status-check contexts.\n // Send the UNION of the branch's existing required contexts + our REQUIRED_CHECK\n // so we ADD the CI gate without silently dropping any other checks the repo\n // already requires. (When the branch has no protection yet, existingContexts is\n // [] and this is just [REQUIRED_CHECK] — the original behavior.) Dedupe defends\n // against REQUIRED_CHECK already appearing among the existing contexts.\n const contexts = [...new Set([...existingContexts, REQUIRED_CHECK])];\n await github.protectBranch(repo, base, contexts);\n actions.push(`required \"${REQUIRED_CHECK}\" check on ${base}`);\n }\n // Ruleset ensure — the layer that binds ADMINS. Classic protection above\n // keeps `enforce_admins: false`, which is exactly the hole the 2026-07-26\n // bulk `gh pr merge --auto` sweep walked through (two unreviewed MAJORS\n // merged). The 2026-08-01 audit then found the six NEWEST repos with no\n // ruleset at all — every hand-applied sweep regrows its gap at the next\n // onboard, so the recipe owns the invariant now. Spec + invariants:\n // docs/superpowers/specs/2026-08-02-ruleset-self-healing-design.md.\n //\n // Private repos are skipped BEFORE any attempt: rulesets there need a paid\n // plan, so trying would either fail every run or spam a phantom action\n // that flips this recipe to \"applied\" forever. Classic protection remains\n // their floor.\n if ((await github.repoVisibility(repo)) === \"public\") {\n // Evidence gate: only require \"ci / ci\" once that check-run has actually\n // been OBSERVED on the default branch. With an empty bypass list, a\n // required context that never fires would make the repo permanently\n // unmergeable by EVERYONE — e.g. a workflow_call-only ci.yml emits no\n // check runs. Unobserved ⇒ refs rules only; the check upgrades on a\n // later pass once real evidence exists.\n //\n // Residual risk, accepted: this proves the check fires on default-branch\n // PUSHES, not on pull_request events — a hand-edited push-only ci.yml\n // would pass the gate and then block every PR. ci.yml is deliberately\n // NOT healed by block A (per-site parameterized; see\n // SELF_UPDATING_CONFIGS), so this recipe cannot correct that drift —\n // the exposure is one bricked-PR repo, immediately visible on its next\n // PR. Un-brick path: edit the ruleset via API (ruleset ADMIN is not\n // ref-gated), then fix the ci.yml triggers by push.\n const check = (await github.checkContextObserved(repo, base, REQUIRED_CHECK))\n ? REQUIRED_CHECK\n : null;\n const existingRuleset = (await github.listRepoRulesets(repo)).find(\n (r) => r.name === FLEET_RULESET_NAME,\n );\n if (!existingRuleset) {\n await github.createRuleset(repo, desiredRuleset(check));\n actions.push(\n check === null\n ? `created ruleset \"${FLEET_RULESET_NAME}\" (refs rules only — \"${REQUIRED_CHECK}\" not yet observed on ${base})`\n : `created ruleset \"${FLEET_RULESET_NAME}\"`,\n );\n } else {\n const full = await github.getRuleset(repo, existingRuleset.id);\n const gaps = rulesetGaps(full, check);\n if (gaps.length > 0) {\n // PUT the healed payload (merge TOWARD the floor, never away — see\n // healRuleset). Recording the specific gaps makes this the drift\n // ALARM, same as the auto-merge-off action above.\n await github.updateRuleset(repo, existingRuleset.id, healRuleset(full, check));\n actions.push(`healed ruleset drift: ${gaps.join(\"; \")}`);\n }\n }\n }\n // (No per-repo RENOVATE_TOKEN planting anymore: Renovate authenticates as\n // the reddoor-renovate GitHub App, whose RENOVATE_APP_ID / _PRIVATE_KEY are\n // org-level with all-repos visibility — a new repo needs NOTHING set. The\n // old step was already redundant for new sites (the PAT was an org secret\n // too); existing per-repo copies are inert and left to rot harmlessly.\n // docs/runbooks/renovate-app-identity.md covers the migration + rollback.)\n } catch (err) {\n const done = actions.length ? ` (completed: ${actions.join(\"; \")})` : \"\";\n const message = err instanceof Error ? err.message : String(err);\n return resultOf(site, \"failed\", `${message}${done}`, commits);\n } finally {\n // Restore the operator's branch whenever we created + checked out a maint\n // branch — on SUCCESS and, critically, on FAILURE. Without this, a push/PR\n // error after createBranch strands the checkout on the maint branch with an\n // unpushed commit, and the retry then fails at createBranch (\"branch already\n // exists\"). Best-effort: a restore failure must not mask the real outcome.\n if (maintBranch !== null && original !== null && original !== maintBranch) {\n try {\n await checkoutBranch(site.path, original);\n } catch (err) {\n console.warn(\n `warning: could not restore branch ${original} after self-updating: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n }\n }\n\n return actions.length\n ? resultOf(site, \"applied\", actions.join(\"; \"), commits)\n : resultOf(site, \"noop\", \"already self-updating\", commits);\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/util/git.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst exec = promisify(execFile);\n\nasync function git(cwd: string, args: string[]): Promise<{ stdout: string; stderr: string }> {\n return exec(\"git\", args, { cwd, env: process.env });\n}\n\nexport function branchName(recipe: string, when: Date = new Date()): string {\n // ISO with millisecond precision: 2026-05-20T10:30:00.123Z → 20260520T103000123Z.\n // Millis (vs. second-precision) shrinks the collision window for parallel runs.\n const compact = when.toISOString().replace(/[-:.]/g, \"\");\n return `maint/${recipe}-${compact}`;\n}\n\nexport async function currentBranch(cwd: string): Promise<string> {\n const { stdout } = await git(cwd, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n return stdout.trim();\n}\n\nexport async function isWorkingTreeClean(cwd: string): Promise<boolean> {\n const { stdout } = await git(cwd, [\"status\", \"--porcelain\"]);\n return stdout.trim().length === 0;\n}\n\nexport async function createBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", \"-b\", name]);\n}\n\n/** Check out an existing branch. Throws (via git) if the branch is missing or\n * the checkout is blocked (e.g. uncommitted changes that would be overwritten). */\nexport async function checkoutBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", name]);\n}\n\n/**\n * Force-check-out an existing branch, DISCARDING any uncommitted changes on the\n * current branch. Used by the recipe failure path to return the operator to\n * their original branch even when a recipe left the work-in-progress branch\n * dirty. Only ever called with the operator's ORIGINAL branch as `name`. Does\n * not run `git clean`, so untracked operator files are left untouched.\n */\nexport async function forceCheckoutBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", \"-f\", name]);\n}\n\n/**\n * Delete a local branch with `-D` (force). Used by the recipe failure path to\n * remove the branch the recipe itself created so a re-run starts clean. Callers\n * MUST only ever pass the recipe-created branch here, never the operator's\n * original branch.\n */\nexport async function deleteBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"branch\", \"-D\", name]);\n}\n\nexport async function stageAll(cwd: string): Promise<void> {\n await git(cwd, [\"add\", \"-A\"]);\n}\n\nexport async function listTrackedFiles(cwd: string): Promise<string[]> {\n const { stdout } = await git(cwd, [\"ls-files\"]);\n return stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n}\n\nexport async function removeFromIndex(cwd: string, paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n await git(cwd, [\"rm\", \"-r\", \"--cached\", \"--\", ...paths]);\n}\n\n/**\n * Stages all current changes and commits with `message`. Returns the commit SHA,\n * or `null` if there was nothing to commit.\n */\nexport async function commit(cwd: string, message: string): Promise<string | null> {\n await stageAll(cwd);\n const { stdout: status } = await git(cwd, [\"status\", \"--porcelain\"]);\n if (status.trim().length === 0) return null;\n await git(cwd, [\"commit\", \"-m\", message]);\n const { stdout: sha } = await git(cwd, [\"rev-parse\", \"HEAD\"]);\n return sha.trim();\n}\n\n/**\n * Strict GitHub repo identity: exactly two `[A-Za-z0-9._-]` segments separated\n * by a single slash. Rejects a scheme, host, extra path segment, traversal\n * (`..`), whitespace, or an argv flag — anything that could retarget a `gh`\n * write at an unintended (attacker/typo-controlled) repo.\n */\nexport const OWNER_REPO_RE = /^[A-Za-z0-9._-]+\\/[A-Za-z0-9._-]+$/;\n\n/** True when `repo` is a clean `owner/repo` (see {@link OWNER_REPO_RE}). The\n * explicit `..` reject covers traversal segments the char-class would otherwise\n * admit (`.` is a legal repo char, so `../evil` matches the shape regex). */\nexport function isOwnerRepo(repo: string): boolean {\n if (repo.includes(\"..\")) return false;\n return OWNER_REPO_RE.test(repo);\n}\n\n/**\n * True when two repo references name the same `owner/repo`. Each side may be a\n * full remote URL (https or scp-style), or already an `owner/repo`. Used to\n * verify an existing checkout's `origin` matches the site's expected repo\n * before reusing it. Returns false if either side is unparseable.\n */\nexport function sameOwnerRepo(a: string, b: string): boolean {\n const na = isOwnerRepo(a.trim()) ? a.trim() : parseOwnerRepo(a);\n const nb = isOwnerRepo(b.trim()) ? b.trim() : parseOwnerRepo(b);\n if (na === null || nb === null) return false;\n return na.toLowerCase() === nb.toLowerCase();\n}\n\n/** Derive `owner/repo` from a git remote URL (https or scp-style). Null if unparseable. */\nexport function parseOwnerRepo(remoteUrl: string): string | null {\n const trimmed = remoteUrl\n .trim()\n .replace(/\\.git$/, \"\")\n .replace(/\\/$/, \"\");\n // scp-style: git@github.com:owner/repo\n const scp = trimmed.match(/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:(.+)$/);\n const path = scp ? scp[1]! : trimmed.replace(/^https?:\\/\\/[^/]+\\//, \"\");\n const segments = path.split(\"/\").filter(Boolean);\n if (segments.length < 2) return null;\n return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;\n}\n\n/** `origin` remote URL for a checkout, trimmed. Throws (via git) if there's no origin. */\nexport async function getRemoteUrl(cwd: string): Promise<string> {\n const { stdout } = await git(cwd, [\"remote\", \"get-url\", \"origin\"]);\n return stdout.trim();\n}\n\n/** Push a branch to origin, setting upstream. Throws on non-zero (execFile rejects). */\nexport async function push(cwd: string, branch: string): Promise<void> {\n await git(cwd, [\"push\", \"-u\", \"origin\", branch]);\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli/commands/launch.ts","../src/recipes/launch.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { launch, type LaunchResult, type LaunchStepResult } from \"../../recipes/launch.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\n\nexport type LaunchCommandOptions = {\n cwd?: string;\n};\n\nfunction formatStep(name: string, r: LaunchStepResult): string {\n if (r.kind === \"error\") return `${name.padEnd(20)} error: ${r.message}`;\n if (r.kind === \"audit\") {\n const s = r.scores;\n return `${name.padEnd(20)} audited (P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo})`;\n }\n if (r.kind === \"draft\") {\n return `${name.padEnd(20)} drafted ${r.report.reportId}`;\n }\n const rec = r.result;\n if (rec.status === \"noop\") return `${name.padEnd(20)} noop${rec.notes ? ` — ${rec.notes}` : \"\"}`;\n if (rec.status === \"failed\")\n return `${name.padEnd(20)} failed${rec.notes ? ` — ${rec.notes}` : \"\"}`;\n return `${name.padEnd(20)} applied (${rec.commits.length} commit${rec.commits.length === 1 ? \"\" : \"s\"})${rec.notes ? ` — ${rec.notes}` : \"\"}`;\n}\n\nfunction formatResult(r: LaunchResult): string {\n const header = `[${r.site}] launch — ${r.complete ? \"drafted (awaiting approval)\" : \"STOPPED\"}`;\n const body = r.steps.map((s) => formatStep(s.name, s.result)).join(\"\\n\");\n return `${header}\\n${body}`;\n}\n\n/**\n * `launch <site>` — single-site only. Bootstrap (Renovate + protection), first-audit\n * the site, and DRAFT its launch email into the M3 approve queue. Never sends;\n * the operator approves the draft and the next send run delivers the go-live\n * email (flipping Status → maintenance + stamping Launched at).\n */\nexport async function runLaunchCommand(\n site: string,\n opts: LaunchCommandOptions,\n): Promise<{ output: string; code: number }> {\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n const sites = await resolveSites({ site, cwd });\n const target = sites[0];\n if (!target) {\n return { output: `No site resolved for \"${site}\".`, code: 1 };\n }\n\n // #539 Phase 5: create-side Turso dual-write, wired at the composition root\n // (see runAnnounceCommand for why it is not defaulted inside the recipe).\n const { makeReportMirror } = await import(\"../../reports/report-mirror.js\");\n const { makeSiteMirror } = await import(\"../../db/site-mirror.js\");\n const result = await launch(target, {\n reportMirror: await makeReportMirror(),\n siteMirror: await makeSiteMirror(),\n });\n return { output: formatResult(result), code: result.complete ? 0 : 1 };\n}\n","import type { AuditResult, RecipeResult, Site } from \"../types.js\";\nimport { siteLabel } from \"../util/site.js\";\nimport { selfUpdating } from \"./self-updating/index.js\";\nimport { runAudits } from \"../audits/index.js\";\nimport { hasRealScores, lighthouseScoresFromResult } from \"../audits/lighthouse-airtable.js\";\nimport { writeAuditsToAirtable } from \"../audits/write-audits-to-airtable.js\";\nimport { openBase, readAirtableConfig } from \"../reports/airtable/client.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport { listWebsites, siteSlug } from \"../reports/airtable/websites.js\";\nimport type { WebsiteRow } from \"../reports/airtable/websites.js\";\nimport {\n createDraft,\n findReportByPeriod,\n updateReportScores,\n} from \"../reports/airtable/reports.js\";\nimport type { ReportRow } from \"../reports/airtable/reports.js\";\nimport type { ReportMirror } from \"../reports/report-mirror.js\";\nimport type { SiteMirror } from \"../db/site-mirror.js\";\nimport { queueDraft } from \"../reports/queue.js\";\nimport { uploadAttachment } from \"../reports/airtable/attachments.js\";\nimport { renderReportHtml } from \"../reports/render.js\";\nimport { resolveCopy } from \"../reports/copy.js\";\nimport type { LighthouseScores } from \"../reports/types.js\";\n\nexport type LaunchStepResult =\n | { kind: \"recipe\"; result: RecipeResult }\n | { kind: \"audit\"; results: AuditResult[]; scores: LighthouseScores }\n | { kind: \"draft\"; report: ReportRow }\n | { kind: \"error\"; message: string };\n\nexport type LaunchResult = {\n site: string;\n steps: Array<{ name: string; result: LaunchStepResult }>;\n /** True if every step ran (bootstrap + audit + draft); false if a step\n * errored or a recipe `failed` short-circuited the chain. */\n complete: boolean;\n};\n\nexport type LaunchDeps = {\n /** Bootstrap step (Renovate + protection; ci.yml comes from the starter). Defaults to the real `selfUpdating`. */\n bootstrap?: (site: Site) => Promise<RecipeResult>;\n /** Audit step. Defaults to the real `runAudits`. */\n audit?: (site: Site) => Promise<AuditResult[]>;\n /** Airtable handle. Defaults to opening the live base from credentials. */\n base?: AirtableBase;\n /** #539 Phase 5: Turso write-through for everything this recipe writes — the\n * Launch row (or a re-run's refreshed scores), the rendered body, and the\n * queue flag. Wired at the CLI composition root, never defaulted here — the\n * unit suite calls `launch` with a fake base and must not open a real\n * libSQL handle. */\n reportMirror?: ReportMirror;\n /** #539 Phase 5: the Websites-row twin — launch writes the site's FIRST audit\n * results and (on send) its launched status. Injected at the CLI root. */\n siteMirror?: SiteMirror;\n};\n\n/**\n * Launch a site: bootstrap → first-audit → DRAFT a launch email. The M3\n * approve loop is what actually sends; `launch` never sends — it stops at a\n * dashboard-queued draft (`reportType: \"Launch\"`) carrying the just-audited\n * Lighthouse scores, so `sendOne`'s `report.lighthouse` guard passes.\n *\n * Step-chain (mirrors `init`), stopping on the first error or `failed` recipe:\n * 1. selfUpdating — Renovate + protection (platform auto-merge OFF; ci.yml is the starter's).\n * 2. runAudits + write the scores to the site's Websites row (reuses the\n * `audit --write-airtable` writer); the Lighthouse scores feed the draft.\n * 3. createDraft — reportType \"Launch\", today's period, the audited scores.\n */\nexport async function launch(site: Site, deps: LaunchDeps = {}): Promise<LaunchResult> {\n const label = siteLabel(site);\n const bootstrap = deps.bootstrap ?? selfUpdating;\n const audit = deps.audit ?? runAudits;\n const base = deps.base ?? openBase(readAirtableConfig());\n\n const steps: Array<{ name: string; result: LaunchStepResult }> = [];\n const stop = (): LaunchResult => ({ site: label, steps, complete: false });\n\n // 1. Bootstrap.\n let recipe: RecipeResult;\n try {\n recipe = await bootstrap(site);\n } catch (err) {\n steps.push({ name: \"self-updating\", result: errorOf(err) });\n return stop();\n }\n steps.push({ name: \"self-updating\", result: { kind: \"recipe\", result: recipe } });\n if (recipe.status === \"failed\") return stop();\n\n // 2. Audit + write scores back to Airtable.\n let results: AuditResult[];\n try {\n results = await audit(site);\n } catch (err) {\n steps.push({ name: \"audit\", result: errorOf(err) });\n return stop();\n }\n const lhResult = results.find((r) => r.audit === \"lighthouse\");\n if (!lhResult || !hasRealScores(lhResult)) {\n steps.push({\n name: \"audit\",\n result: { kind: \"error\", message: \"lighthouse audit produced no real scores\" },\n });\n return stop();\n }\n // The launch announcement renders a numeric score per category; a metric that\n // errored this run (now null from lighthouseScoresFromResult) keeps the prior\n // 0 behavior here rather than propagating null into the launch-email path. The\n // Airtable write path (write-audits-to-airtable) keeps the null → shows \"—\".\n const rawScores = lighthouseScoresFromResult(lhResult);\n const scores: LighthouseScores = {\n performance: rawScores.performance ?? 0,\n accessibility: rawScores.accessibility ?? 0,\n bestPractices: rawScores.bestPractices ?? 0,\n seo: rawScores.seo ?? 0,\n };\n\n const websites = await listWebsites(base);\n const target = websites.find((w) => siteSlug(w.name) === siteSlug(label));\n if (!target) {\n steps.push({\n name: \"audit\",\n result: { kind: \"error\", message: `no Websites row matched site \"${label}\"` },\n });\n return stop();\n }\n try {\n const auditWrite = await writeAuditsToAirtable({\n base,\n websites,\n slug: siteSlug(target.name),\n results,\n });\n // #539 Phase 5: mirror the first-audit write-back. Launch is the ONE path\n // that writes a brand-new site's health, so without this its row reads empty\n // in the console until the next hourly sync.\n if (auditWrite.siteId && auditWrite.fields) {\n await deps.siteMirror?.health(auditWrite.siteId, auditWrite.fields);\n }\n } catch (err) {\n steps.push({ name: \"audit\", result: errorOf(err) });\n return stop();\n }\n steps.push({ name: \"audit\", result: { kind: \"audit\", results, scores } });\n\n // 3. Draft the launch email (reuses draft.ts's reportId/period scheme). DRAFTS\n // ONLY — the M3 approve loop sends it and flips Status on send.\n const today = new Date();\n const period = today.toISOString().slice(0, 7);\n const slug = siteSlug(target.name);\n\n let report: ReportRow;\n try {\n // Re-run dedupe: reuse an existing Launch row for this (site, period) instead\n // of stacking a second draft. findReportByPeriod is the same idempotency\n // lookup draft.ts documents (dashboard/digest point lookup).\n const existing = await findReportByPeriod(base, target.id, \"Launch\", period);\n if (existing) {\n // Reuse path: the row was created on a prior run with THAT run's scores. This\n // re-run just produced fresh audit scores AND will re-render the preview from\n // them — so refresh the row's Lighthouse cells (+ Completed on) to match,\n // otherwise the sent email (which reads the row) ships stale scores. The\n // create path already writes fresh scores via createDraft.\n await updateReportScores(base, existing.id, scores, today);\n // Mirror the same refresh — see the announce reuse path for why.\n await deps.reportMirror?.patch(existing.id, {\n lighthouse_performance: scores.performance,\n lighthouse_accessibility: scores.accessibility,\n lighthouse_best_practices: scores.bestPractices,\n lighthouse_seo: scores.seo,\n completed_on: today.toISOString().slice(0, 10),\n });\n report = existing;\n } else {\n report = await createDraft(\n base,\n draftInputFor(target, scores, today, period),\n deps.reportMirror?.created,\n );\n }\n } catch (err) {\n steps.push({ name: \"draft\", result: errorOf(err) });\n return stop();\n }\n\n // Mirror draft.ts:135-154 — render → upload \"Rendered HTML\" preview → flip\n // Draft ready. Without setDraftReady the draft never enters the approve queue\n // (every pending-approval gate requires draftReady true), so it can never be\n // approved or sent. The upload is a review convenience; the ready flag is the\n // critical step.\n try {\n const { html } = await renderReportHtml({\n siteName: target.name,\n siteUrl: target.url,\n reportType: \"Launch\",\n completedOn: today,\n lighthouse: scores,\n lastTestedDate: null,\n commentary: null,\n copy: resolveCopy(target),\n headerImageCid: `${slug}-header`,\n });\n // A preview-upload hiccup must NOT fail the launch — log and continue.\n try {\n await uploadAttachment(\n report.id,\n \"Rendered HTML\",\n html,\n `${slug}-${today.toISOString().slice(0, 10)}.html`,\n \"text/html\",\n );\n // The console preview reads the body from Turso, not the attachment.\n await deps.reportMirror?.body(report.id, html);\n } catch (uploadErr) {\n console.warn(\n `⚠ Launch preview upload skipped for ${target.name}: ${\n uploadErr instanceof Error ? uploadErr.message : String(uploadErr)\n }`,\n );\n }\n // Critical: NOT wrapped — a failure here must surface as a failed launch. queueDraft\n // supersedes any lower-tier (Maintenance/Testing) drafts queued for this site; a Launch is\n // top tier so it only stands down if another Launch/Announcement is already queued.\n await queueDraft(\n base,\n { id: report.id, siteId: target.id, reportType: \"Launch\" },\n deps.reportMirror,\n );\n } catch (err) {\n steps.push({ name: \"draft\", result: errorOf(err) });\n return stop();\n }\n\n steps.push({ name: \"draft\", result: { kind: \"draft\", report } });\n\n return { site: label, steps, complete: true };\n}\n\n/** Build the Launch `DraftInput`. reportId/period mirror `draftReportForSite`\n * (draft.ts) — do not invent a new id scheme. `today`/`period` are threaded in\n * from `launch()` so a single timestamp drives the render, the draft, the\n * dedupe lookup, and the preview filename. Launch reports have no period window\n * and no prior maintenance test, so periodStart/periodEnd/completedOn all\n * collapse to \"today\" and `lastTestedDate` is null. */\nfunction draftInputFor(\n target: WebsiteRow,\n scores: LighthouseScores,\n today: Date,\n period: string,\n): Parameters<typeof createDraft>[1] {\n const reportType = \"Launch\" as const;\n const reportId = `${target.name} — ${reportType} — ${today.toISOString().slice(0, 10)}`;\n return {\n reportId,\n siteId: target.id,\n reportType,\n period,\n periodStart: today,\n periodEnd: today,\n completedOn: today,\n lighthouse: scores,\n lastTestedDate: null,\n };\n}\n\nfunction errorOf(err: unknown): LaunchStepResult {\n return { kind: \"error\", message: err instanceof Error ? err.message : String(err) };\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli/commands/prismic-ci.ts","../src/recipes/prismic-ci/index.ts","../src/recipes/prismic-ci/template.ts","../src/recipes/prismic-ci/cli-version.ts"],"sourcesContent":["// The fleet face of the `prismic-ci` recipe: land the Prismic model delivery\n// workflow across the fleet, as ONE PULL REQUEST PER REPOSITORY.\n//\n// The blast radius is fifteen live client repositories, and the failure this\n// file is written against is not a bad PR — the recipe's gates handle that, and\n// nothing here can push to a client's main. It is the QUIET one: a run that\n// touched nothing and exited 0, read as a finished rollout. Three facts only the\n// fleet layer can get wrong produce it, and each has a refusal below:\n//\n// - an inventory that resolved NOBODY prints \"0 applied, 0 noop, 0 failed.\"\n// and exits 0, which is indistinguishable from a fleet with nothing left to\n// do. The Airtable inventory is view-filtered; one filter change empties it\n// with no error anywhere.\n// - a site that could not be PREPARED is isolated into `skipped` by\n// `prepareFleetSites` (correctly — one bad row must not abort the fleet) and\n// then disappears from a summary built out of `prepared` alone. It gets a\n// `failed` ROW, because the count is the only thing a machine reads.\n// - a checkout holding `.git` and nothing else answers \"not a Prismic site\" to\n// every read inside it, so the site noops out of the rollout for good — a\n// fleet workdir reuses any non-empty directory forever. Guarded BEFORE the\n// recipe runs: a tree nobody established is not a tree to branch from.\n//\n// SEQUENTIAL, one site at a time, like every other fleet recipe command — see\n// `runRecipeOverSites`. Each site does git work in its own checkout plus a\n// handful of GitHub calls; there is no Airtable write on this path (so the\n// fleet's ≤4.5 req/s throttle does not apply) and nothing parallelism would buy\n// except a burst of writes against fifteen repositories at once.\nimport { readdir } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { prismicCi } from \"../../recipes/prismic-ci/index.js\";\nimport {\n REUSABLE_WORKFLOW_PIN,\n isPinResolved,\n type ReusableWorkflowPin,\n} from \"../../recipes/prismic-ci/template.js\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\nimport { siteLabel } from \"../../util/site.js\";\nimport { noWorkingTreeFailure } from \"../../util/working-tree.js\";\nimport { appendSkipNotice, prepareFleetSites, type SkippedSite } from \"../fleet/prepare-sites.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport { runRecipeOverSites } from \"../fleet/run-recipe-over-sites.js\";\n\nexport type PrismicCiCommandOptions = {\n fleet?: string;\n workdir?: string;\n dry?: boolean;\n cwd?: string;\n};\n\n/** Injected only by tests, so the fleet layer's aggregation — counts, exit code,\n * refusals — is exercised without git, GitHub or a network. Production runs the\n * real recipe. */\nexport type PrismicCiCommandDeps = {\n runRecipe?: (site: Site) => Promise<RecipeResult>;\n /**\n * Which `reddoorla/.github` commit this run would pin. Injected only by tests;\n * production uses the shipped {@link REUSABLE_WORKFLOW_PIN}.\n *\n * It is injectable because the unresolved-pin refusal is a SAFETY behaviour\n * that must be tested in both directions at all times, and a test that reads\n * the shipped constant can only ever exercise whichever direction the constant\n * currently happens to be in. Two tests here did exactly that, and the release\n * that resolved the pin turned them from \"the refusal works\" into a red suite\n * with the refusal itself no longer covered by anything.\n */\n pin?: ReusableWorkflowPin;\n};\n\nconst messageOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));\n\nconst failedRow = (site: string, notes: string): RecipeResult => ({\n recipe: \"prismic-ci\",\n site,\n status: \"failed\",\n commits: [],\n notes,\n});\n\n/**\n * One line per site plus a counts summary — the shape every fleet rollout\n * prints, so a sweep over fifteen repos is scannable.\n *\n * The banner above the counts is not decoration. A rollout in which every site\n * failed prints fifteen `[site] failed:` lines and then an arithmetic exercise;\n * the one fact an operator needs from that run — NOT ONE repository got the\n * workflow — has to be stated. It is keyed on \"sites were attempted and none was\n * applied\", not on \"nothing was applied\": a fleet that is already fully\n * delivered applies nothing every time it runs, and a banner that shouts at that\n * run is a banner nobody reads at the run above.\n */\nexport function formatPrismicCiResults(results: RecipeResult[]): string {\n const lines = results.map((r) => `[${r.site}] ${r.status}: ${r.notes ?? \"\"}`.trimEnd());\n const n = (s: RecipeResult[\"status\"]) => results.filter((r) => r.status === s).length;\n const failed = n(\"failed\");\n lines.push(\"\");\n if (failed > 0 && n(\"applied\") === 0) {\n lines.push(\n `⛔ NO SITE GOT THE DELIVERY WORKFLOW. ${failed} of ${results.length} site(s) failed and` +\n ` not one pull request was opened, so no site's model changes reach Prismic on merge.` +\n ` Do NOT read this run as a rollout.`,\n );\n lines.push(\"\");\n }\n lines.push(`${n(\"applied\")} applied, ${n(\"noop\")} noop, ${failed} failed.`);\n return lines.join(\"\\n\");\n}\n\n/**\n * Is there a checkout here at all, or the named reason there is not?\n *\n * Runs BEFORE the recipe, for both the reason in {@link noWorkingTreeFailure}\n * and one this command adds: the recipe's next steps create a branch, write a\n * file and commit it. A directory nobody managed to check out is not a tree to\n * do that in, and the answer the recipe would otherwise reach — \"not a Prismic\n * site\" — is the one answer that quietly removes the site from the rollout.\n *\n * A `readdir` that THROWS is its own failure and never the skip: a path that\n * does not exist, or one this process cannot read, is not a repo without\n * Prismic in it.\n */\nasync function checkoutFailure(repoRoot: string): Promise<string | null> {\n let entries: string[];\n try {\n entries = await readdir(repoRoot);\n } catch (e) {\n return `cannot read this checkout at ${repoRoot}: ${messageOf(e)}`;\n }\n return noWorkingTreeFailure(\n repoRoot,\n entries,\n \"No delivery workflow was proposed for it and no pull request was opened\",\n );\n}\n\n/**\n * Roll the delivery workflow out to one site or to the fleet.\n *\n * `resolveSites` is allowed to THROW (a positional site alongside `--fleet`, an\n * unsupported inventory extension, an Airtable read that failed). Those are \"the\n * fleet itself could not be established\", which has no per-site row to live in\n * and must not be reported as a rollout across zero sites; `bin.ts` prints the\n * message and exits with the error's own `exitCode`.\n */\nexport async function runPrismicCiCommand(\n site: string | undefined,\n opts: PrismicCiCommandOptions,\n deps: PrismicCiCommandDeps = {},\n): Promise<{ output: string; code: number }> {\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n // Passed through because `--fleet airtable` derives each site's path from\n // it; without it the keyword inventory would resolve paths under a different\n // workdir than the one this run then prepares and commits in.\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n cwd,\n });\n\n // NOTHING RESOLVED IS NOT A DELIVERED FLEET — the refusal `runFleetSweep` and\n // the token doctor both open with, for the same reason: the summary below\n // would print \"0 applied, 0 noop, 0 failed.\" over no rows at all and exit 0,\n // which is exactly what a finished rollout looks like.\n if (sites.length === 0) {\n return {\n output:\n `the inventory resolved NO SITES, so no site was offered the delivery workflow.` +\n ` This is not a delivered fleet — check the inventory (an Airtable view filter, an` +\n ` empty JSON file, a dynamic inventory returning []). Do NOT read this exit as a` +\n ` rollout.`,\n code: 1,\n };\n }\n\n let skipped: SkippedSite[] = [];\n if (opts.fleet) {\n const prep = await prepareFleetSites(sites, { workdir: opts.workdir ?? fleetWorkdir() });\n sites = prep.prepared;\n skipped = prep.skipped;\n }\n\n // THE PIN, checked here as well as in the recipe — not a second copy of the\n // gate but the same exported predicate on the same pin, asked at the one point\n // the recipe cannot answer for: a `--dry` run never reaches the recipe at all,\n // and a preview that promised fifteen pull requests the real run would refuse\n // is a preview of something that cannot happen.\n //\n // ONE pin object serves both this preview and the recipe below, so the preview\n // can never be answering about a different pin than the run it previews.\n const pin = deps.pin ?? REUSABLE_WORKFLOW_PIN;\n const pinUnresolved = !isPinResolved(pin)\n ? `⛔ the reusable-workflow pin is unresolved (${pin.sha}) — every site` +\n ` below would be REFUSED, not delivered. Publish and tag the workflow in` +\n ` reddoorla/.github, then set REUSABLE_WORKFLOW_PIN.`\n : \"\";\n\n if (opts.dry) {\n const lines = sites.map(\n (s) =>\n `[${siteLabel(s)}] would be offered the delivery workflow (no gate was evaluated:` +\n ` the secret, default-branch and already-delivered checks run only on a real run)`,\n );\n return {\n output: appendSkipNotice(\n [pinUnresolved, lines.join(\"\\n\")].filter((s) => s !== \"\").join(\"\\n\\n\"),\n skipped,\n ),\n code: pinUnresolved === \"\" ? 0 : 1,\n };\n }\n\n const run = deps.runRecipe ?? ((s: Site) => prismicCi(s, { pin }));\n const results = await runRecipeOverSites(\"prismic-ci\", sites, async (s) => {\n const failure = await checkoutFailure(s.path);\n return failure ? failedRow(siteLabel(s), failure) : run(s);\n });\n\n // A SITE THAT COULD NOT BE PREPARED IS A SITE THAT DID NOT GET THE WORKFLOW,\n // and it gets a row. `prepareFleetSites` isolates the clone failure into\n // `skipped`, which is right and is also exactly how a site disappears: a\n // summary walking `prepared` alone counts a fleet-wide clone outage — an\n // expired App token, GitHub down, a full disk — as \"0 applied, 0 failed\",\n // exit 0. The notice below stays; the row is what makes the site countable.\n for (const s of skipped) {\n results.push(\n failedRow(\n s.site,\n `could not prepare this checkout: ${s.reason}. No delivery workflow was proposed` +\n ` for it — this is NOT \"this site already has one\".`,\n ),\n );\n }\n\n return {\n output: appendSkipNotice(formatPrismicCiResults(results), skipped),\n code: results.some((r) => r.status === \"failed\") ? 1 : 0,\n };\n}\n","import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { defaultSpawn, type SpawnFn } from \"../../audits/util/spawn.js\";\nimport { readGitHubConfig } from \"../../github/config.js\";\nimport { makeGitHub, type GitHub } from \"../../github/gh.js\";\nimport { readPrismicConfig } from \"../../prismic/models/index.js\";\nimport type { RecipeResult, Site } from \"../../types.js\";\nimport {\n branchName,\n checkoutBranch,\n commit as gitCommit,\n createBranch,\n currentBranch,\n deleteBranch,\n isOwnerRepo,\n isWorkingTreeClean,\n push as gitPush,\n} from \"../../util/git.js\";\nimport { siteLabel } from \"../../util/site.js\";\nimport { formatWithPrettier, resolveTargetPrettier, PRETTIER_FLAG_NOTE } from \"../_prettier.js\";\nimport {\n APPLY_BRANCH,\n PRISMIC_CI_WORKFLOW,\n REUSABLE_WORKFLOW_PIN,\n SECRET,\n WORKFLOW_PATH,\n isPinResolved,\n prismicCiWorkflow,\n type ReusableWorkflowPin,\n} from \"./template.js\";\nimport { MIN_CLI_VERSION, atLeast, readLockedCliVersion } from \"./cli-version.js\";\n\n/** Head-branch prefix of every PR this recipe opens. `branchName(\"prismic-ci\")`\n * produces `maint/prismic-ci-<timestamp>`, and the re-run guard matches on this\n * prefix — the same idiom as `findOpenSelfUpdatingPR`. */\nconst BRANCH_PREFIX = \"maint/prismic-ci-\";\n\n/** How long the target's prettier gets. Also what makes the fleet's default\n * spawn detach the child, so the kill reaches prettier and not just a wrapper.\n * Matches the pull-down path's budget in `src/prismic/models/write.ts`. */\nconst PRETTIER_TIMEOUT_MS = 60_000;\n\n/** Just the GitHub surface this recipe touches, so a test fake is five methods\n * rather than thirty. `makeGitHub()` satisfies it structurally. */\nexport type PrismicCiGitHub = Pick<\n GitHub,\n \"defaultBranch\" | \"secretExists\" | \"fileContentsOnBranch\" | \"openPullRequest\" | \"openPullRequests\"\n>;\n\nexport type PrismicCiDeps = {\n github?: PrismicCiGitHub;\n pushBranch?: (cwd: string, branch: string) => Promise<void>;\n spawn?: SpawnFn;\n /** Resolve the TARGET repo's own prettier. Injected so a test can assert the\n * absolute-path spawn without a populated `node_modules`. */\n resolvePrettier?: (repoRoot: string) => Promise<string | null>;\n /** Which `reddoorla/.github` commit the written workflow pins. Injected only\n * by tests; production uses the shipped pin. */\n pin?: ReusableWorkflowPin;\n};\n\nconst resultOf = (\n site: Site,\n status: RecipeResult[\"status\"],\n notes: string,\n commits: string[] = [],\n): RecipeResult => ({\n recipe: \"prismic-ci\",\n site: siteLabel(site),\n status,\n commits,\n notes,\n});\n\nconst messageOf = (err: unknown): string => (err instanceof Error ? err.message : String(err));\n\n/** Same normalization as `self-updating`'s config comparison: CRLF and a stray\n * trailing newline are not drift, and treating them as drift opens a needless\n * PR on every run. Any real content difference still differs. */\nfunction sameWorkflow(current: string, canonical: string): boolean {\n const norm = (s: string) => s.replace(/\\r\\n/g, \"\\n\").replace(/\\s+$/, \"\");\n return norm(current) === norm(canonical);\n}\n\n/**\n * Land the Prismic model delivery workflow in one repo, as a pull request.\n *\n * Everything before the first mutation is a gate, and the gates exist because\n * this recipe's output is the only path in the project that writes to a live\n * client's Prismic repository. Two of them are worth naming:\n *\n * - THE SECRET. A workflow whose `PRISMIC_WRITE_TOKEN` does not exist yet goes\n * red on the repo's first model PR. Landing 15 of those turns a rollout into\n * 15 red repos, so a definitively-absent secret FAILS the site rather than\n * nooping it: \"this site was skipped and a human must act\" is not the same\n * report as \"this site was already done\", and a rollout summary where those\n * two look alike is how a site silently never gets model delivery. A secret\n * whose existence could not be DETERMINED fails too, with different wording —\n * they are different facts and the operator's next move differs.\n *\n * - THE DEFAULT BRANCH. The reusable workflow's apply job hard-codes\n * `refs/heads/main`. On a repo whose default branch is anything else, every\n * model PR would merge green and never reach Prismic. Refuse instead.\n *\n * Nothing here can delete a model: the workflow it installs calls a module that\n * exports no delete path.\n */\nexport async function prismicCi(site: Site, deps: PrismicCiDeps = {}): Promise<RecipeResult> {\n // 1. Repo identity. `failed`, not `noop`: a rollout that silently skips a site\n // is the failure this whole recipe is guarding against one level up. The\n // strict shape check runs BEFORE the first `gh` call, so a typo'd or\n // attacker-controlled value can never be interpolated into an API path.\n const repo = site.gitRepo;\n if (!repo) {\n return resultOf(site, \"failed\", \"no Git repo on this site (set Airtable 'Git repo')\");\n }\n if (!isOwnerRepo(repo)) {\n return resultOf(\n site,\n \"failed\",\n `refusing to act on malformed repo identity: expected \"owner/repo\", got ${JSON.stringify(repo)}`,\n );\n }\n\n // 2. Is this a Prismic site? `readPrismicConfig` returns null for \"no Prismic\n // here\" and THROWS for a present-but-unreadable config — the distinction is\n // the point, so the throw becomes `failed` rather than being caught into\n // the skip branch.\n let isPrismic: boolean;\n try {\n isPrismic = (await readPrismicConfig(site.path)) !== null;\n } catch (err) {\n return resultOf(site, \"failed\", `could not read the Prismic config: ${messageOf(err)}`);\n }\n if (!isPrismic) return resultOf(site, \"noop\", \"not a Prismic site (no repositoryName) — skipped\");\n\n // 3. The pin. Checked before any network call: while it is unresolved NOTHING\n // can roll out, and 15 pointless API round-trips are not worth spending to\n // find that out per site.\n const pin = deps.pin ?? REUSABLE_WORKFLOW_PIN;\n if (!isPinResolved(pin)) {\n return resultOf(\n site,\n \"failed\",\n `reusable-workflow pin is unresolved (${pin.sha}) — publish + tag the workflow in ` +\n `reddoorla/.github and set REUSABLE_WORKFLOW_PIN before rolling out`,\n );\n }\n const workflow = pin === REUSABLE_WORKFLOW_PIN ? PRISMIC_CI_WORKFLOW : prismicCiWorkflow(pin);\n\n // 3b. THE BINARY THAT WILL ACTUALLY RUN. The reusable workflow does not\n // install this CLI — it runs `pnpm install --frozen-lockfile` and then the\n // site's OWN installed bin, so the version that executes is whatever this\n // repo's lockfile pins. Installing the caller next to a binary with no\n // `prismic-models` command yields a workflow that fails on the first model\n // PR, in a client repo, with an error naming an unknown command rather\n // than a rollout that ran too early.\n //\n // Local and cheap, so it sits with the pin gate ahead of the first network\n // call. And it is a REFUSAL on \"could not establish\", not a pass: the\n // asymmetry is deliberate, because the cost of waiting is a re-run and the\n // cost of being wrong is broken CI on someone else's repository.\n const locked = await readLockedCliVersion(site.path);\n if (!locked.ok) {\n return resultOf(\n site,\n \"failed\",\n `cannot establish which @reddoorla/maintenance version this repo's CI would run ` +\n `(${locked.reason}) — refusing to install a workflow that may not run`,\n );\n }\n if (!atLeast(locked.version, MIN_CLI_VERSION)) {\n return resultOf(\n site,\n \"failed\",\n `this repo's lockfile pins @reddoorla/maintenance ${locked.version}, which has no ` +\n `\\`prismic-models\\` command (first shipped in ${MIN_CLI_VERSION}) — bump the dependency ` +\n `and commit the lockfile first, or the workflow fails on its first model PR`,\n );\n }\n\n const ghConfig = readGitHubConfig();\n if (!deps.github && !ghConfig) return resultOf(site, \"failed\", \"GITHUB_TOKEN not set\");\n const gh = deps.github ?? makeGitHub({ token: ghConfig!.token });\n const spawn = deps.spawn ?? defaultSpawn;\n\n // 4. The secret. Absent and unreadable are separate answers with separate\n // wording; neither proceeds.\n let hasSecret: boolean;\n try {\n hasSecret = await gh.secretExists(repo, SECRET);\n } catch (err) {\n return resultOf(\n site,\n \"failed\",\n `could not determine whether ${repo} has the ${SECRET} secret (${messageOf(err)}) — ` +\n `refusing to install a workflow that may have no token`,\n );\n }\n if (!hasSecret) {\n return resultOf(\n site,\n \"failed\",\n `${repo} has no ${SECRET} Actions secret — the workflow reds the repo on its first ` +\n `model PR without it. Mint one, then: gh secret set ${SECRET} --repo ${repo}`,\n );\n }\n\n // 5. The default branch. NOT `.catch(() => \"main\")`: \"I could not read the\n // default branch\" and \"the default branch is main\" are opposite facts, and\n // guessing the second targets a PR (and an apply gate) at a branch nobody\n // confirmed.\n let base: string;\n try {\n base = await gh.defaultBranch(repo);\n } catch (err) {\n return resultOf(\n site,\n \"failed\",\n `could not read the default branch of ${repo}: ${messageOf(err)}`,\n );\n }\n if (base !== APPLY_BRANCH) {\n return resultOf(\n site,\n \"failed\",\n `default branch of ${repo} is ${base}, not ${APPLY_BRANCH} — the reusable workflow's ` +\n `apply job guards refs/heads/${APPLY_BRANCH}, so merged model changes would never reach Prismic`,\n );\n }\n\n // 6. Already delivered? Content-compared, not existence-compared, so a\n // present-but-STALE workflow (an older pin) is corrected rather than left\n // forever.\n const existing = await gh.fileContentsOnBranch(repo, base, WORKFLOW_PATH);\n if (existing !== null && sameWorkflow(existing, workflow)) {\n return resultOf(site, \"noop\", `delivery workflow already current on ${base}`);\n }\n\n // 7. Already proposed? Without this, every run before the PR merges opens\n // another one. `fileContentsOnBranch` also answers null for a read it could\n // not perform (gh exits non-zero on both a 404 and an auth failure), which\n // makes this the backstop for that collapse as well.\n const open = (await gh.openPullRequests(repo)).find((pr) => pr.headRef.startsWith(BRANCH_PREFIX));\n if (open) {\n return resultOf(site, \"noop\", `delivery workflow PR already open: ${open.url}`);\n }\n\n if (!(await isWorkingTreeClean(site.path))) {\n return resultOf(site, \"failed\", \"working tree not clean — commit or stash first\");\n }\n\n // Capture the operator's branch BEFORE creating ours so the `finally` can put\n // them back. Best-effort: if we cannot read it we skip the restore rather than\n // guess at a branch to check out.\n let original: string | null = null;\n try {\n original = await currentBranch(site.path);\n } catch {\n // Stays null (detached HEAD, git error) — the restore below is then skipped\n // rather than aiming a checkout at a branch name we had to invent.\n }\n const branch = branchName(\"prismic-ci\");\n const commits: string[] = [];\n let pushedOrOpened = false;\n\n try {\n await createBranch(site.path, branch);\n const dest = join(site.path, WORKFLOW_PATH);\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, workflow, \"utf-8\");\n\n // Format with the SITE's own prettier, resolved POSITIVELY and run by\n // absolute path. `pnpm exec prettier` here would run a full `pnpm install`\n // in the client's repo first (an unrequested mutation of a live client repo\n // by a rollout that is supposed to add one file), and in a repo without\n // prettier would then format with the CALLING repo's binary and exit 0.\n const notes: string[] = [];\n const bin = await (deps.resolvePrettier ?? resolveTargetPrettier)(site.path);\n if (bin === null) {\n notes.push(PRETTIER_FLAG_NOTE);\n } else if (\n !(await formatWithPrettier(spawn, site.path, [WORKFLOW_PATH], {\n bin,\n timeoutMs: PRETTIER_TIMEOUT_MS,\n }))\n ) {\n notes.push(PRETTIER_FLAG_NOTE);\n } else if (!sameWorkflow(await readFile(dest, \"utf-8\"), workflow)) {\n // The site's prettier config disagrees with the template's shape (single\n // quotes, a different tab width). Harmless for the workflow — but the\n // step-6 comparison is byte-based, so this site will not recognise its own\n // delivered workflow as current and will propose it again after each\n // merge. Say so rather than let it be discovered as a mystery PR.\n notes.push(\n \"this site's prettier reformatted the workflow — re-runs will not match it as current\",\n );\n }\n\n const sha = await gitCommit(site.path, \"ci: deliver Prismic model changes from merged PRs\");\n if (!sha) {\n // Git saw no change: the checkout already holds this exact workflow even\n // though the default branch does not. Nothing to push and nothing to\n // review — and pushing a commit-less branch would fail at PR creation.\n return resultOf(\n site,\n \"noop\",\n `${WORKFLOW_PATH} already present and identical in the checkout`,\n );\n }\n commits.push(sha);\n pushedOrOpened = true;\n await (deps.pushBranch ?? gitPush)(site.path, branch);\n const pr = await gh.openPullRequest(repo, {\n head: branch,\n base,\n title: \"Deliver Prismic model changes from merged PRs\",\n body:\n \"Adds the `prismic-models` workflow. On a PR touching `customtypes/**` or \" +\n \"`src/lib/slices/**/model.json` it comments the model delta and writes nothing; \" +\n \"on merge to main it pushes those models to Prismic. It can create and update \" +\n \"models but never delete — a model present only in Prismic is reported, not touched.\",\n });\n notes.unshift(`opened PR ${pr.url}`);\n return resultOf(site, \"applied\", notes.join(\"; \"), commits);\n } catch (err) {\n return resultOf(site, \"failed\", messageOf(err), commits);\n } finally {\n // Restore the operator's branch on success AND on failure — otherwise a push\n // error strands the checkout on the recipe branch with an unpushed commit,\n // and the retry dies at createBranch. Best-effort; never masks the result.\n if (original !== null && original !== branch) {\n try {\n await checkoutBranch(site.path, original);\n // Nothing was pushed and no PR exists, so the local branch is litter and\n // a re-run would leave another. Only ever the branch WE created.\n if (!pushedOrOpened) await deleteBranch(site.path, branch);\n } catch (err) {\n console.warn(\n `warning: could not restore branch ${original} after prismic-ci: ${messageOf(err)}`,\n );\n }\n }\n }\n}\n","/**\n * The caller workflow every Prismic site gets.\n *\n * Fully generic — no per-site values — because the token is the site's own\n * `PRISMIC_WRITE_TOKEN` secret, the name every site's code already reads.\n * Contrast `ci.yml`, which is deliberately NOT templated anywhere in this\n * package because it carries per-site `netlify-site:` / `node-version:` values\n * a byte template would strip fleet-wide.\n */\n\n/** Where the file lands in a site repo. Also the `gh api .../contents/<path>`\n * segment, so it must stay traversal-free (`assertUrlSegment` re-checks). */\nexport const WORKFLOW_PATH = \".github/workflows/prismic-models.yml\";\n\n/** The Actions secret the reusable workflow declares as `required: true`, and\n * the environment variable the CLI reads. Spelled ONCE, here, because the\n * failure mode of a second spelling is silent: a caller passing a name the\n * callee never declared supplies nothing, and the callee's required secret\n * arrives empty at the one step that writes to a live client's Prismic repo. */\nexport const SECRET = \"PRISMIC_WRITE_TOKEN\";\n\n/** The reusable workflow, without a ref. */\nexport const REUSABLE_WORKFLOW = \"reddoorla/.github/.github/workflows/prismic-models.yml\";\n\n/**\n * The ONE branch this delivery path can run its apply job on.\n *\n * Not a preference — the reusable workflow's apply job hard-codes\n * `github.ref == 'refs/heads/main'`. On a repo whose default branch is anything\n * else, a merged model PR would produce a green check and never reach Prismic,\n * which is why the recipe refuses such a repo instead of installing a workflow\n * that silently cannot fire.\n */\nexport const APPLY_BRANCH = \"main\";\n\n/** A commit in `reddoorla/.github`, plus the release tag that commit carries.\n * Both are written into the workflow: the SHA is what Actions resolves, the tag\n * is what a human (and Renovate's github-actions manager) reads. */\nexport type ReusableWorkflowPin = { sha: string; tag: string };\n\n/**\n * What the shipped pin says while the reusable workflow has not been published\n * to `reddoorla/.github` and tagged yet (plan Task 25).\n *\n * DELIBERATELY NOT 40 HEX CHARACTERS. A placeholder shaped like a real SHA is\n * indistinguishable from a real one in review, and would install a workflow\n * referencing a commit that does not exist into 15 client repositories — where\n * it fails at workflow-load time, on every model PR, with an error that names\n * neither this file nor the reason. Unresolved must be unmistakable, and\n * {@link isPinResolved} is what the recipe gates on.\n */\nexport const UNRESOLVED_PIN_SHA = \"UNRESOLVED-publish-and-tag-reddoorla-dot-github-first\";\n\n/**\n * The pin the fleet rolls out.\n *\n * RESOLVED 2026-08-14. `reddoorla/.github#30` merged as\n * 558395431ddcb481ecba3dd84b78b38c338cfa03 and that commit was tagged v1.4.0;\n * `gh api repos/reddoorla/.github/commits/v1.4.0 --jq .sha` returns the SHA\n * below. `validate` (the digest-pinning + Renovate-preset check that is the\n * only CI in that repo) passed on the merge commit.\n *\n * TO RE-RESOLVE after a future `reddoorla/.github` release: set `sha` to\n * `gh api repos/reddoorla/.github/commits/<tag> --jq .sha` and `tag` to that\n * tag. Nothing else changes. Renovate's github-actions manager bumps the\n * already-installed `uses:` line per site repo; this constant is what NEW\n * rollouts install, so a stale value here is not a broken fleet, only a fleet\n * whose newest members start one version behind.\n */\nexport const REUSABLE_WORKFLOW_PIN: ReusableWorkflowPin = {\n sha: \"558395431ddcb481ecba3dd84b78b38c338cfa03\",\n tag: \"v1.4.0\",\n};\n\n/**\n * Is this pin a real commit reference?\n *\n * A full 40-hex SHA and nothing else. Not a tag, not a branch: this repo pins\n * every `uses:` to a commit (see `src/recipes/sync-configs/templates.ts`, whose\n * renovate template spells out why — a retagged `@v3` runs attacker code with\n * whatever token the workflow holds), and Renovate's github-actions manager\n * bumps the pin per repo when `reddoorla/.github` tags a new version.\n */\nexport function isPinResolved(pin: ReusableWorkflowPin): boolean {\n return /^[0-9a-f]{40}$/.test(pin.sha);\n}\n\n/**\n * Render the caller workflow for a pin.\n *\n * Path-filtered on both triggers: this must not run on every commit, only when\n * a model changes.\n */\nexport function prismicCiWorkflow(pin: ReusableWorkflowPin): string {\n return `# Delivers this site's Prismic model changes. Managed by @reddoorla/maintenance\n# (\\`reddoor-maint prismic-ci\\`) — change it there and re-run, not here.\n#\n# On a PR touching a model: comment the delta, write nothing.\n# On merge to ${APPLY_BRANCH}: push those models to Prismic. Never delete.\n#\n# THE BRANCH FILTER ON \\`push:\\` IS LOAD-BEARING, not tidiness. The reusable\n# workflow's apply job also guards \\`github.ref == 'refs/heads/${APPLY_BRANCH}'\\`,\n# and its comment calls this the other half of that gate: an unfiltered\n# \\`on: push:\\` fires on every branch AND every tag, so a feature branch's models\n# would reach production with no pull request and therefore no review anywhere in\n# the sequence. Keep both halves.\nname: prismic-models\n\non:\n # No branch filter here, deliberately: a model PR into any base deserves its\n # delta comment, and the dry job is INCAPABLE of writing — it never passes\n # \\`--apply\\`, and it holds no job that does.\n pull_request:\n paths:\n - \"customtypes/**\"\n - \"src/lib/slices/**/model.json\"\n push:\n branches: [${APPLY_BRANCH}]\n paths:\n - \"customtypes/**\"\n - \"src/lib/slices/**/model.json\"\n\njobs:\n prismic-models:\n # A called workflow's jobs can only NARROW what the caller granted, so the\n # PR-comment permission has to be granted here as well as there. Nothing in\n # either job pushes code, so \\`contents\\` stays read.\n permissions:\n contents: read\n pull-requests: write\n uses: ${REUSABLE_WORKFLOW}@${pin.sha} # ${pin.tag}\n secrets:\n # Spelled exactly as the reusable workflow declares it. A workflow-local\n # alias would not be a rename — it would be an undeclared secret, and the\n # required one would arrive empty.\n ${SECRET}: \\${{ secrets.${SECRET} }}\n`;\n}\n\n/** The workflow as currently shipped. Unusable — and refused by the recipe —\n * until {@link REUSABLE_WORKFLOW_PIN} is resolved. */\nexport const PRISMIC_CI_WORKFLOW = prismicCiWorkflow(REUSABLE_WORKFLOW_PIN);\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\n/**\n * The first published version whose `reddoor-maint` bin carries `prismic-models`.\n *\n * VERIFIED BY INSTALLING IT, not by reading a changelog: `npm i\n * @reddoorla/maintenance@0.83.0` in an empty directory, then `reddoor-maint\n * prismic-models --help`, prints the command's usage. Every earlier published\n * version — 0.82.0 included — exits with \"unknown command\".\n *\n * This matters because the reusable workflow does NOT install the CLI. It runs\n * `pnpm install --frozen-lockfile` and then the site's own installed bin, so the\n * version that executes in a client repo is whatever that repo's lockfile pins.\n * Installing the caller workflow next to an older binary produces a workflow\n * that fails at the first model PR, in a client repo, with an error about an\n * unknown command rather than about a rollout that ran too early.\n */\nexport const MIN_CLI_VERSION = \"0.83.0\";\n\n/** The dependency whose version decides whether the workflow can run at all. */\nconst PACKAGE = \"@reddoorla/maintenance\";\n\n/**\n * `version >= min`, compared NUMERICALLY, position by position.\n *\n * Not a string comparison, and not `localeCompare`: as strings `\"0.9.0\" >\n * \"0.83.0\"`, so a lexicographic gate waves through a site pinned seventy-four\n * releases before the command existed. That is the single most likely way for\n * this gate to be wrong while looking right.\n *\n * A prerelease (`0.83.0-beta.1`) sorts BELOW the release it precedes, per\n * semver. The fleet publishes no prereleases today; the rule is here so that if\n * one ever appears it is not silently treated as the release.\n */\nexport function atLeast(version: string, min: string): boolean {\n const parse = (v: string): { nums: number[]; pre: boolean } => {\n const [core = \"\", ...rest] = v.split(\"-\");\n return {\n nums: core.split(\".\").map((n) => Number.parseInt(n, 10) || 0),\n pre: rest.length > 0,\n };\n };\n const a = parse(version);\n const b = parse(min);\n for (let i = 0; i < 3; i++) {\n const av = a.nums[i] ?? 0;\n const bv = b.nums[i] ?? 0;\n if (av !== bv) return av > bv;\n }\n // Cores equal: a prerelease of the minimum is below it; anything else is equal.\n if (a.pre && !b.pre) return false;\n return true;\n}\n\n/** Either the version pnpm resolved, or WHY that could not be established.\n * The two are deliberately different shapes: a caller cannot read \"I could not\n * tell\" as a version by forgetting to check a flag. */\nexport type LockedCliVersion = { ok: true; version: string } | { ok: false; reason: string };\n\n/**\n * The version of {@link PACKAGE} the repo's committed pnpm lockfile resolves.\n *\n * Reads the LOCKFILE, never `package.json`. The declared range is not what\n * executes: espada declares `^0.81.0` and its lockfile resolves 0.69.0, and CI\n * installs `--frozen-lockfile`. A gate reading the range would pass a site whose\n * install is two dozen releases behind it.\n *\n * Parsed with a targeted scan rather than a YAML dependency — this needs one\n * field out of one block, the repo carries no YAML parser, and adding one to\n * read a single `version:` line is not a trade worth making. The scan is\n * deliberately narrow: it reads only `importers:` entries (what is installed),\n * never the `packages:`/`snapshots:` sections, which list every transitively\n * resolvable version and would happily report one nobody installed.\n *\n * Every failure is a NAMED reason, never a version and never a silent default.\n * A gate that cannot tell \"too old\" from \"could not read\" reports one as the\n * other, which is the absent-vs-unreadable collapse this whole feature exists to\n * prevent — arriving here at the last step before writing to a client repo.\n */\nexport async function readLockedCliVersion(repoRoot: string): Promise<LockedCliVersion> {\n let raw: string;\n try {\n raw = await readFile(join(repoRoot, \"pnpm-lock.yaml\"), \"utf-8\");\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n // ENOENT is the only error meaning \"this repo has no lockfile\". EACCES and\n // friends mean it is there and unreadable — a different answer, and one that\n // must not read as \"not a pnpm site\".\n if (code === \"ENOENT\") {\n return {\n ok: false,\n reason: `no pnpm-lock.yaml in the checkout — cannot tell which ${PACKAGE} version CI would run`,\n };\n }\n return { ok: false, reason: `pnpm-lock.yaml is present but unreadable (${String(err)})` };\n }\n\n // Only inside `importers:` — see the note above on packages:/snapshots:.\n const lines = raw.split(\"\\n\");\n const importersAt = lines.findIndex((l) => /^importers:\\s*$/.test(l));\n if (importersAt === -1) {\n return {\n ok: false,\n reason: \"pnpm-lock.yaml has no `importers:` section — unrecognised lockfile shape\",\n };\n }\n const end = lines.findIndex((l, i) => i > importersAt && /^[a-zA-Z]/.test(l));\n const importers = lines.slice(importersAt + 1, end === -1 ? lines.length : end);\n\n const found = new Set<string>();\n for (let i = 0; i < importers.length; i++) {\n if (!new RegExp(`^\\\\s+'?${PACKAGE.replace(\"/\", \"\\\\/\")}'?:\\\\s*$`).test(importers[i]!)) continue;\n // The entry's own `version:` sits within the next few lines, alongside\n // `specifier:`. Bounded so a malformed block cannot walk into the next\n // dependency and attribute its version to this one.\n for (let j = i + 1; j < Math.min(i + 4, importers.length); j++) {\n const m = /^\\s+version:\\s*(\\S+)\\s*$/.exec(importers[j]!);\n if (!m) continue;\n // Strip the peer-dependency suffix: real fleet lockfiles run it past 400\n // characters after the semver.\n found.add(m[1]!.split(\"(\")[0]!);\n break;\n }\n }\n\n if (found.size === 0) {\n return { ok: false, reason: `${PACKAGE} is not a dependency in this repo's lockfile` };\n }\n if (found.size > 1) {\n // Picking one would be a coin flip deciding whether a client repo gets a\n // workflow its binary cannot run.\n return {\n ok: false,\n reason: `lockfile resolves ${PACKAGE} to more than one version (${[...found].sort().join(\", \")}) — refusing to guess which one CI would run`,\n };\n }\n return { ok: true, version: [...found][0]! };\n}\n"],"mappings":"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","names":["readFile","join","join","readFile","messageOf"]}
File without changes