diff --git a/.claude/skills/cms-platform-secrets/SKILL.md b/.claude/skills/cms-platform-secrets/SKILL.md index a2211d93..b1a43d6e 100644 --- a/.claude/skills/cms-platform-secrets/SKILL.md +++ b/.claude/skills/cms-platform-secrets/SKILL.md @@ -125,6 +125,69 @@ it's explicitly set. > A fine-grained PAT can't write repo variables for you — the setter uses your > `gh` auth, which needs admin/maintain on the consumer repo. +## Platform-repo secrets (set on `Adam-S-Daniel/cms-platform`, NOT on consumers) + +The platform repo itself holds a few Actions secrets for its own workflows — +they are not part of a consumer's setup checklist. + +### `REPO_SETTINGS_READ_ADAM_S_DANIEL` / `REPO_SETTINGS_READ_JODIDANIEL` — repo-settings drift audit (#109) + +Consumed by `.github/workflows/repo-settings-audit.yml`, the daily READ-ONLY +drift audit of `repo-settings.yml` (the settings-as-code manifest at the +platform root — see `docs/SYNC.md` "Repo settings as code") against the live +repo settings/rulesets. A fine-grained PAT can't span two owners, so there is +one secret per resource owner: + +| Secret | Resource owner | Repository access | Repository permissions | +|---|---|---|---| +| `REPO_SETTINGS_READ_ADAM_S_DANIEL` | `Adam-S-Daniel` | `cms-platform` + `adamdaniel.ai` | **Administration: Read-only** (+ implied Metadata: Read) | +| `REPO_SETTINGS_READ_JODIDANIEL` | org **`jodidaniel`** | `jodidaniel.com` | **Administration: Read-only** (+ implied Metadata: Read) | + +- **Read-only by design** — there is NO write credential in CI anywhere in + this mechanism; applying the manifest is a human running + `node scripts/audit-repo-settings.js --fix --yes` with their own `gh` auth + (admin on the target repo). An accidental mutation attempt with these + tokens 403s — a designed tripwire. +- Store: `gh secret set REPO_SETTINGS_READ_ADAM_S_DANIEL -R Adam-S-Daniel/cms-platform` + (and the same for `REPO_SETTINGS_READ_JODIDANIEL`). +- **Verify each PAT after minting/rotation** — the negative proof matters as + much as the positives: + + ```bash + GH_TOKEN= gh api repos///actions/permissions --jq .enabled # true/false = Administration:Read OK + GH_TOKEN= gh api repos///rulesets --jq length # a number + GH_TOKEN= gh api -X PATCH repos// -f has_wiki=false # MUST fail 403 + ``` + + > **Do NOT probe `delete_branch_on_merge` (or any `allow_*_merge` / + > `*_commit_*` merge-setting) as the positive check.** GitHub gates those + > merge-setting keys behind the **Contents** permission (read+write), so they + > are **entirely absent** from the repo object these correctly read-only + > **Administration: Read** PATs return — the field is missing, not `false`, and + > a `--jq .delete_branch_on_merge` probe would read empty even on a healthy + > token. `actions/permissions` needs Administration: Read with no public + > exemption, so its `.enabled` boolean is the reliable positive proof. Leave + > these PATs at **Administration: Read only** — do NOT add Contents (the audit + > skips the merge flags as informational; `--fix` reconciles them under the + > operator's own admin `gh` auth, which already has Contents). + +- **Expiry / rotation:** fine-grained PATs expire (a year at most). An + expired or missing token turns the daily audit run RED with a distinct + "read path is broken" error naming the env var — that red run is the + rotation reminder (the audit never mistakes an auth failure for drift). + Re-mint with the same permissions and `gh secret set` again. + +### `BUMP_DISPATCH_ADAMDANIEL_AI` / `BUMP_DISPATCH_JODIDANIEL_COM` — release fan-out (back-documented) + +Consumed by `release.yml`'s "Fan out bump dispatches to consumers" step: +right after `gh release create`, it dispatches each consumer's +`platform-bump.yml` (`gh workflow run platform-bump.yml -R `) so +bumps don't wait for the weekly Monday cron. Fine-grained PATs are minted per +RESOURCE OWNER, so each consumer owner supplies its own secret: **Actions: +Read and write** on that ONE consumer repo, nothing else. FAIL-OPEN: a +missing/expired token only degrades that consumer to its weekly +platform-bump cron (a `::warning`, never a job failure). + ## Quick checklist for a new consumer - [ ] `CMS_E2E_PAT` — fine-grained, this repo: Contents R/W + Pull requests R/W + **Actions R/W** (+ be a reviewer of the `regression-review` environment) diff --git a/.claude/skills/writing-adrs/SKILL.md b/.claude/skills/writing-adrs/SKILL.md deleted file mode 100644 index 0caa3282..00000000 --- a/.claude/skills/writing-adrs/SKILL.md +++ /dev/null @@ -1,116 +0,0 @@ ---- -name: writing-adrs -description: Write a lightweight Nygard-style Architecture Decision Record under `docs/decisions/` when a non-obvious decision needs context that won't fit in a code comment and would rot if left only in a PR description. Trigger when the user asks "should I document this", "add an ADR", "why did we do X" referring to a past choice with no comment trail, or when you find yourself drafting a multi-paragraph PR description justifying a one-line change. Also covers the bootstrap case (creating `docs/decisions/README.md` + the first ADR from scratch) if the folder doesn't exist yet. ---- - -# Writing Architecture Decision Records - -`docs/decisions/` in this repo captures **why** non-obvious decisions were made — context that's not in the code, that `git blame` won't surface, and that a contributor a year from now would re-derive (badly) without it. - -This skill tells you when to write one, how to format it, and how to keep the index honest. - -## When to invoke this skill - -Trigger any of these: - -- **The user explicitly asks for one.** "Should this be an ADR?", "add an ADR for X", "document this decision so we don't undo it", "write up why we did Y this way". -- **You're drafting a PR description that's growing into an explanation of *why* a small change isn't crazy.** That's the signal: the explanation belongs in an ADR, the PR description should link to it. Recognize this pattern when your PR body has paragraphs starting with "you might wonder why we didn't just…" or "this looks like a downgrade because…" or "the reason we picked X over Y is…". -- **The user asks "why did we…" about a past decision and there's no satisfying answer in code comments, AGENTS.md, or commit history.** Offer to write the ADR retroactively so the next person doesn't have to ask. -- **A code-review comment surfaces a non-obvious decision** — propose extracting the reasoning into an ADR and linking to it from the code comment. - -Do NOT invoke for: - -- One-character cosmetic preferences. -- Decisions already covered by an existing ADR (link to that one instead, or update its `Status` to `Superseded by NNNN` and write the replacement). -- Decisions that are fully obvious from the code or already documented in AGENTS.md / a CLAUDE.md / a focused doc under `docs/`. Adding an ADR for those is bureaucratic clutter. -- Decisions where the "why" fits comfortably in a 2-3 line code comment at the call site AND the call site is stable. Local context beats remote context. - -## Procedure - -### 0. Read the existing convention - -If `docs/decisions/README.md` exists, read it first — it has the canonical format, naming convention, and index for this repo. Follow it. If it doesn't exist, skip to "Bootstrap" below. - -### 1. Confirm the decision is ADR-worthy - -Ask yourself (or the user, if unclear) one question: **"In six months, when someone proposes reverting this change, what's the shortest answer that'll save them an afternoon of investigation?"** If the answer is a sentence, an inline comment is fine. If it's three paragraphs covering the alternatives you ruled out, write the ADR. - -The most reliable ADR-worthiness test: would a reasonable contributor scan the diff and think "this looks wrong, let me undo it"? If yes — ADR. - -### 2. Pick the next number - -Read `docs/decisions/README.md`'s index and pick `MAX(existing) + 1`. Zero-pad to 4 digits (`0001`, `0002`, …, `0011`, …). Files sort correctly past 9999 if and when we get there; we won't. - -### 3. Pick a title - -Imperative verb + object. Examples that work: "Use `widget: text` for the e2e canary collection body", "Pin GitHub Actions to commit SHAs not version tags", "Block direct pushes to `main` via ruleset, not branch protection". - -Examples that don't work: "Widget config" (no verb), "Decided to switch widgets" (past tense + vague), "Architecture for canary body" (noun pile). - -### 4. Use the template - -Copy the template from `docs/decisions/README.md` and fill in the sections in this order: - -1. **Context** first. What did we observe? What constraints applied? What forced the decision? Write this without referencing the decision — it should read as "here's the problem space." -2. **Decision** second, in one or two sentences. The shortest unambiguous statement of what we did. -3. **Consequences** third. Both positive AND negative — be honest. Future-you will trust an ADR more if it acknowledges trade-offs than if it reads like a press release. -4. **Alternatives considered** fourth. This is the section that earns its keep when a future contributor proposes one of them. For each alternative, one short paragraph: what it was, why we rejected it. If the rejection is "we didn't think of it at the time," say that — write the alternative anyway so we don't re-evaluate it without learning something. -5. **References** last. PRs, issues, commits, external docs. Anchor the ADR to artefacts that won't go stale. - -Optional sections worth considering for high-impact decisions: - -- **"Why this doesn't break X"** when the decision looks scarier than it is. Walk through the layers (storage, rendering, callers, tests) and explain why each one is fine. `docs/decisions/0001-canary-body-widget-text.md` has an example. -- **"How to verify"** when the decision can be locked in with a test or an invariant — describe the test and link to it. - -### 5. Update the index - -The README's index table at the bottom needs a new row. Add it in the same commit as the ADR — never in a follow-up. Format: - -```markdown -| [NNNN](NNNN-kebab-title.md) | Title (matching the H1) | Status | -``` - -### 6. Link to the ADR from the relevant code - -Wherever the decision shows up in the codebase, add a short comment pointing at the ADR. Two or three lines max — the ADR has the prose, the code comment is the signpost. Pattern: - -```yaml -# See docs/decisions/0001-canary-body-widget-text.md for why this is -# `widget: text`, not `widget: markdown`. -``` - -If the decision shows up in multiple files, only the call site needs a comment; the rest can rely on grep. Don't sprinkle the same comment everywhere. - -### 7. Update the PR description - -In the PR that introduces (or implements) the decision, link to the ADR in the body. The PR description should be short — "implements `docs/decisions/NNNN-…`" plus a one-line summary — and the ADR carries the prose. This keeps reviewers' attention on the diff, not on re-reading the rationale. - -## Bootstrap: the folder doesn't exist yet - -If `docs/decisions/` doesn't exist, set it up in the same change as the first ADR. The README and the first ADR land together — never bootstrap an empty folder. - -1. Create `docs/decisions/README.md` with the format spec, the when-to-write rules, the template, and an empty index. Use [the one in this repo](../../docs/decisions/README.md) as a starting point — it's tuned for the repo style. -2. Write the first ADR (`0001-…`). -3. Add the index row for the first ADR. -4. Add a pointer paragraph in `AGENTS.md` (or `README.md` if there's no AGENTS.md) under a new `### Architecture Decision Records` heading, immediately above or below the project docs / README index. The pointer is one or two sentences and a link. -5. Commit everything as a single `docs(decisions): start ADR folder; first ADR is …` commit. Same PR as the change the ADR is justifying — don't ship the ADR scaffolding in isolation, it'll feel like overhead. - -## Common mistakes to avoid - -- **Writing the ADR *after* the PR merges.** ADRs are most useful when reviewers can read them during review — they signal "I've thought about the alternatives, here they are." Write the ADR as part of the same PR. -- **Editing accepted ADRs to change the decision.** Don't. Write a new ADR that supersedes the old one and update the old one's `Status` line. The audit trail is the point. -- **Including transient state.** No "as of today the build is broken" or "currently we're using X but will move to Y next quarter." ADRs decay if they contain time-bound information that's not pinned to a specific PR or commit. -- **Skipping "Alternatives considered."** This is the most-skipped section and the most valuable. A future contributor proposing alternative A wants to know if A was already evaluated. If you didn't evaluate any alternatives, write "Alternatives considered: none; the decision was forced by an external constraint (link)" — make it explicit. -- **Linking to the ADR from too many places.** One comment at the call site is enough. The ADR is searchable; the codebase doesn't need ADR breadcrumbs on every adjacent line. -- **Numbering disputes.** If two PRs both want `NNNN` and one merges first, the second rebases and renumbers in their own PR. No retroactive renumbering of already-merged ADRs. - -## What's NOT an ADR - -To keep `docs/decisions/` from drifting into a general-purpose docs dump: - -- **Tutorials and how-tos** belong in `docs/.md` (see `docs/CONTENT_GUIDE.md`, `docs/TESTING.md`). -- **Project-wide conventions** that aren't the result of a *decision* belong in `AGENTS.md` (see "Branch hygiene", "Reading PR diffs", etc.). -- **Skill instructions for agents** (when to use a pattern, how to apply it) belong in `.agents/skills//SKILL.md` — like this file. -- **Test plans and runbooks** belong in `docs/TESTING.md` or a script's own header comment. - -The ADR test: "If someone proposes reverting this, the ADR is what they read." Anything that doesn't fit that purpose probably belongs elsewhere.