@natjswenson/shipflow 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md CHANGED
@@ -51,30 +51,32 @@ user; the CLI is the only thing that *does*.
51
51
  - `"shipflow"` → tell the user no existing branch protection was found and shipflow will own it going forward.
52
52
  - `"ambiguous"` → **branch protection exists but no settings-as-code artifact was found** (e.g. hand-configured via the GitHub UI). Do NOT silently pick either value — this is exactly the false-positive failure mode a prior design iteration got wrong. Ask explicitly: *"Branch protection exists on this repo but isn't managed as code — should shipflow take ownership of it, or keep managing it externally even though no artifact was found?"* Record whichever the user picks.
53
53
 
54
- 5. **Present the interview summary and write `.github/shipflow.json`.** Before writing anything, show the user the resolved branch names, `requiredChecks`, and `protectionOwner` together in one place and wait for explicit confirmation this is the checkpoint called out at the top of this section. Then write the config in the target repo (never inside the skill package) using `config.example.json` as the template, with `release.mode: "manual-gate"` (the only implemented mode in this version see Auto mode, below). Tell the user `.github/shipflow.json` is committed policy and should be `git add`/committed ideally in the same commit as the rendered auto-merge workflow, once step 9 produces one.
54
+ 5. **Resolve `release.releaseCredential` never default it to `GITHUB_TOKEN`.** The rendered `dev-to-main-automerge.yml`'s `GH_TOKEN` comes from this secret name. A PR auto-merged under `secrets.GITHUB_TOKEN` completes (once checks pass) attributed to the `github-actions[bot]` identity, and GitHub's loop-prevention rule means that bot-attributed merge's `pull_request: closed` event **never triggers this or any other workflow** so `label-release-pending` silently never runs, and the entire manual-gate release-ask flow never has anything to find. This was confirmed empirically, not theoretically: an otherwise-identical PR merged by a real, PAT-authenticated actor fired the closed-event trigger within 2 seconds; one completed by `GITHUB_TOKEN`-enabled auto-merge fired no run at all, even after 100+ seconds. Ask the user to create a fine-grained PAT (or GitHub App installation token) scoped to this repo with `contents: write` + `pull-requests: write`, and to store it as a repo secret themselves (e.g. `gh secret set <NAME> --repo <owner>/<repo>`, run in *their own* shell so the token value never passes through the agent or the transcript). Record only the secret's *name* in `release.releaseCredential` — never its value.
55
55
 
56
- 6. **Show the plan.** Run:
56
+ 6. **Present the interview summary and write `.github/shipflow.json`.** Before writing anything, show the user the resolved branch names, `requiredChecks`, `protectionOwner`, and `release.releaseCredential` together in one place and wait for explicit confirmation — this is the checkpoint called out at the top of this section. Then write the config in the target repo (never inside the skill package) using `config.example.json` as the template, with `release.mode: "manual-gate"` (the only implemented mode in this version — see Auto mode, below). Tell the user `.github/shipflow.json` is committed policy and should be `git add`/committed — ideally in the same commit as the rendered auto-merge workflow, once step 10 produces one.
57
+
58
+ 7. **Show the plan.** Run:
57
59
  ```
58
60
  npx -y @natjswenson/shipflow plan --repo <path>
59
61
  ```
60
- This prints `{ plan, stateHash }`. Present `plan.creates`/`plan.updates`/`plan.noops` to the user in plain language — what will be created, what will change, what's already correct. **Wait for explicit confirmation before proceeding.** If any entry has `handEditDetected: true`, call it out specifically and ask whether to override (see step 8).
62
+ This prints `{ plan, stateHash }`. Present `plan.creates`/`plan.updates`/`plan.noops` to the user in plain language — what will be created, what will change, what's already correct. **Wait for explicit confirmation before proceeding.** If any entry has `handEditDetected: true`, call it out specifically and ask whether to override (see step 9).
61
63
 
62
- 7. **Dry-run apply** (optional sanity check, same output shape as the real apply but nothing is mutated):
64
+ 8. **Dry-run apply** (optional sanity check, same output shape as the real apply but nothing is mutated):
63
65
  ```
64
66
  npx -y @natjswenson/shipflow apply --repo <path> --dry-run
65
67
  ```
66
68
 
67
- 8. **Apply for real**, passing the `stateHash` from step 6's plan output as `--expect-state-hash` — this is the TOCTOU guard: if repo state drifted between the plan you showed the user and this call, `apply` refuses to mutate anything and tells you to re-plan.
69
+ 9. **Apply for real**, passing the `stateHash` from step 7's plan output as `--expect-state-hash` — this is the TOCTOU guard: if repo state drifted between the plan you showed the user and this call, `apply` refuses to mutate anything and tells you to re-plan.
68
70
  ```
69
- npx -y @natjswenson/shipflow apply --repo <path> --expect-state-hash <hash-from-step-6>
71
+ npx -y @natjswenson/shipflow apply --repo <path> --expect-state-hash <hash-from-step-7>
70
72
  ```
71
- If a `handEditDetected` entry was confirmed for override in step 6, pass `--force <entry-id>` (repeatable — one flag per confirmed entry id, never a blanket override).
73
+ If a `handEditDetected` entry was confirmed for override in step 7, pass `--force <entry-id>` (repeatable — one flag per confirmed entry id, never a blanket override).
72
74
 
73
- 9. **Report the result.** Read `applied`/`skipped`/`errors` from the response. A `skipped` entry can be a deliberate refusal (empty checks, hand-edit) or an environment limitation shipflow can't do anything about (e.g. a deletion-ruleset skipped because the repo is private and not on a paid GitHub tier) — read each `reason` and relay it plainly rather than treating every `skipped` entry the same. If `renderedTemplateHashes` is non-empty, update `.github/shipflow.json`'s `renderedTemplateHashes` field with those values and tell the user to commit the config change *and* the rendered workflow file **together, in the same commit** — a split commit is exactly what causes a false `handEditDetected` on a clean checkout later.
75
+ 10. **Report the result.** Read `applied`/`skipped`/`errors` from the response. A `skipped` entry can be a deliberate refusal (empty checks, hand-edit) or an environment limitation shipflow can't do anything about (e.g. a deletion-ruleset skipped because the repo is private and not on a paid GitHub tier) — read each `reason` and relay it plainly rather than treating every `skipped` entry the same. If `renderedTemplateHashes` is non-empty, update `.github/shipflow.json`'s `renderedTemplateHashes` field with those values and tell the user to commit the config change *and* the rendered workflow file **together, in the same commit** — a split commit is exactly what causes a false `handEditDetected` on a clean checkout later.
74
76
 
75
77
  ## Re-run / audit
76
78
 
77
- Same as steps 1, 6, 7, 8, 9 above, skipping the interview (branch names/checks/protectionOwner are already recorded in `.github/shipflow.json` — read it, don't re-ask, unless the user explicitly says they want to reconfigure). If `plan.creates`/`plan.updates` is non-empty, that's drift since the last apply — show it and confirm before applying, exactly as in first-run setup.
79
+ Same as steps 1, 7, 8, 9, 10 above, skipping the interview (branch names/checks/protectionOwner/releaseCredential are already recorded in `.github/shipflow.json` — read it, don't re-ask, unless the user explicitly says they want to reconfigure). If `plan.creates`/`plan.updates` is non-empty, that's drift since the last apply — show it and confirm before applying, exactly as in first-run setup.
78
80
 
79
81
  ## Check pending releases (`manual-gate` ask-flow)
80
82
 
@@ -106,6 +108,7 @@ This is a **separate, later invocation** from the one that ran the promotion's `
106
108
  - **`handEditDetected`:** a template file's on-disk content doesn't match what shipflow last rendered *or* what it would freshly render — someone hand-edited it. Never silently pass `--force` for this; always show the user what changed and get explicit confirmation per entry.
107
109
  - **TOCTOU abort:** if `apply` returns a `toctou` error, repo state changed between plan and apply — re-run the plan step, don't retry the same `--expect-state-hash`.
108
110
  - **`gh auth` failures:** surface these immediately; branch protection and rulesets need repo-admin scope. Don't proceed partway through a plan on missing auth.
111
+ - **`release.releaseCredential` left as (or defaulted to) `GITHUB_TOKEN`:** auto-merge and the required-check gate still work, but `label-release-pending` will silently never run — a `GITHUB_TOKEN`-attributed auto-merge's `pull_request: closed` event never triggers it, so no promotion will ever surface via `shipflow releases`. This fails silently, not loudly — there's no error to catch it — so it must be caught at setup time (step 5) rather than discovered later. If a user reports "releases never show up," check this first.
109
112
 
110
113
  ## Security rules
111
114
 
@@ -12,7 +12,7 @@
12
12
  "mode": "manual-gate",
13
13
  "tool": "release-please",
14
14
  "layout": "manifest",
15
- "releaseCredential": "GITHUB_TOKEN"
15
+ "releaseCredential": "SHIPFLOW_AUTOMERGE_PAT"
16
16
  },
17
17
  "branchCleanup": {
18
18
  "deleteOnMerge": true,
package/lib/plan.mjs CHANGED
@@ -79,6 +79,11 @@ function computeTemplatePlanEntry(repoState, config, templateSource) {
79
79
  devBranch: config.branches.dev,
80
80
  mainBranch: config.branches.main,
81
81
  mergeFlag: mergeMethodToFlag(config.mergeMethod?.devToMainMethod),
82
+ // Must be a real PAT/App-installation-token secret name, not
83
+ // "GITHUB_TOKEN" — see the template's header comment for why a
84
+ // GITHUB_TOKEN-attributed auto-merge never fires the closed-event
85
+ // label job at all.
86
+ releaseCredentialSecret: config.release?.releaseCredential ?? 'GITHUB_TOKEN',
82
87
  };
83
88
  const renderedContent = renderTemplate(templateSource, params);
84
89
  const freshHash = sha256(renderedContent);
package/lib/render.mjs CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  const TOKEN_RE = /\{\{(\w+)\}\}/g;
16
16
 
17
- // params: { devBranch, mainBranch, mergeFlag }
17
+ // params: { devBranch, mainBranch, mergeFlag, releaseCredentialSecret }
18
18
  // mergeFlag is one of "--merge" | "--squash" | "--rebase", derived from
19
19
  // config.mergeMethod.devToMainMethod by the caller (not this function —
20
20
  // mapping method name -> gh flag is a config-schema concern, kept out of
@@ -40,6 +40,7 @@ const TOKEN_TO_PARAM = Object.freeze({
40
40
  DEV_BRANCH: 'devBranch',
41
41
  MAIN_BRANCH: 'mainBranch',
42
42
  MERGE_FLAG: 'mergeFlag',
43
+ RELEASE_CREDENTIAL_SECRET: 'releaseCredentialSecret',
43
44
  });
44
45
 
45
46
  export function mergeMethodToFlag(devToMainMethod) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@natjswenson/shipflow",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Scaffold a configurable dev/main branching, auto-merge, branch-cleanup, and release-tagging workflow into any repo",
5
5
  "license": "MIT",
6
6
  "author": "Nate Swenson",
@@ -55,6 +55,11 @@
55
55
  "id": "ci-scaffold-never-overwrites",
56
56
  "pattern": "[Nn]ever silently overwrite an existing workflow file",
57
57
  "rationale": "Agent-driven CI scaffolding (added after 1.00s had no pull_request-triggered CI at all) must never clobber a workflow file that's already there — same confirm-before-write discipline as every other mutation in this skill, applied to a step the CLI itself never touches."
58
+ },
59
+ {
60
+ "id": "release-credential-never-github-token",
61
+ "pattern": "never default it to `GITHUB_TOKEN`",
62
+ "rationale": "Found by dogfooding on claude-skills itself (2026-07-15): a PR auto-merged under secrets.GITHUB_TOKEN completes attributed to github-actions[bot], and GitHub's loop-prevention rule means that bot-attributed merge's pull_request:closed event never triggers label-release-pending — the entire manual-gate release-ask flow silently never has anything to find unless releaseCredential names a real PAT."
58
63
  }
59
64
  ],
60
65
  "cli_commands_referenced": ["detect", "plan", "apply", "releases", "release-dispatch", "rename-default-branch"]
@@ -5,6 +5,19 @@ name: auto-merge {{DEV_BRANCH}} to {{MAIN_BRANCH}}
5
5
  # for this file, or shipflow's next apply will refuse to overwrite it
6
6
  # (handEditDetected) until an explicit --force is passed. Commit both files
7
7
  # together in the same commit.
8
+ #
9
+ # GH_TOKEN uses config.release.releaseCredential, NOT a hardcoded
10
+ # secrets.GITHUB_TOKEN, because of GitHub's loop-prevention rule: a PR
11
+ # auto-merged via `gh pr merge --auto` run under the default GITHUB_TOKEN
12
+ # completes (later, asynchronously, once checks pass) attributed to the
13
+ # github-actions[bot] identity — and a `pull_request: closed` event
14
+ # resulting from that bot-attributed merge does NOT trigger this or any
15
+ # other workflow's `on: pull_request` handlers. Confirmed empirically:
16
+ # an identical PR merged by a real, PAT-authenticated actor fired the
17
+ # closed-event trigger immediately; one completed by GITHUB_TOKEN-enabled
18
+ # auto-merge fired no run at all. releaseCredential must therefore name a
19
+ # real PAT/App-installation-token secret (not GITHUB_TOKEN) for
20
+ # label-release-pending to ever actually run.
8
21
 
9
22
  on:
10
23
  pull_request:
@@ -30,7 +43,7 @@ jobs:
30
43
  - name: Enable auto-merge
31
44
  run: gh pr merge --auto {{MERGE_FLAG}} "${{ github.event.pull_request.number }}" --repo "${{ github.repository }}"
32
45
  env:
33
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46
+ GH_TOKEN: ${{ secrets.{{RELEASE_CREDENTIAL_SECRET}} }}
34
47
 
35
48
  # Fires once, when the promotion PR actually merges (a separate event from
36
49
  # the job above, which only *enables* auto-merge). Applies a durable
@@ -49,4 +62,4 @@ jobs:
49
62
  - name: Apply release-pending label
50
63
  run: gh pr edit "${{ github.event.pull_request.number }}" --add-label release-pending --repo "${{ github.repository }}"
51
64
  env:
52
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
65
+ GH_TOKEN: ${{ secrets.{{RELEASE_CREDENTIAL_SECRET}} }}