@rtorcato/repo-tooling 3.15.0 → 3.15.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.
@@ -259,6 +259,13 @@ export async function checkDependabot(dir) {
259
259
  if (!automerge.includes("dependency-group == 'dev-minor'")) {
260
260
  deltas.push('auto-merge workflow merges production bumps (missing dev-minor group gate)');
261
261
  }
262
+ // #458: the group gate alone lets peerDependencies through, because
263
+ // Dependabot files a package that is both dev and peer under
264
+ // "development". The canonical workflow re-checks the names against the
265
+ // manifests; a workflow without that step is the #423 hole reopened.
266
+ if (!automerge.includes("steps.gate.outputs.safe == 'true'")) {
267
+ deltas.push('auto-merge workflow trusts the dev-minor group name (missing peerDependencies check)');
268
+ }
262
269
  }
263
270
  else {
264
271
  deltas.push('missing dependabot-automerge workflow');
@@ -9,8 +9,10 @@ import fs from 'fs-extra';
9
9
  */
10
10
  const PLACEHOLDER_DOMAINS = ['example.com', 'example.org', 'example.net', 'invalid', 'test'];
11
11
  /**
12
- * Suffixes of a machine-derived hostname. `.local` is mDNS (macOS default),
13
- * `.matrix` is this user's LAN — both produced real commits in the #327 set.
12
+ * Suffixes of a machine-derived hostname. `.local` is mDNS (macOS default); the
13
+ * rest are the conventional private-LAN suffixes a router hands out. `.matrix`
14
+ * is one such router default rather than a registered TLD — it is here because
15
+ * it produced real commits in the #327 set, alongside `.local`.
14
16
  */
15
17
  const HOSTNAME_SUFFIXES = ['.local', '.matrix', '.localhost', '.lan', '.home', '.internal'];
16
18
  /** Pure so the rules are testable without a git repo or a spawn. */
@@ -37,8 +37,11 @@ export function dependabotConfig(ecosystem) {
37
37
  update-types:
38
38
  - minor
39
39
  - patch
40
- # The only tier that auto-merges on green — dev tooling never reaches a
41
- # consumer of the published package.
40
+ # The tier that can auto-merge on green. "development" is Dependabot's
41
+ # classification, not a safety property: a package listed in both
42
+ # devDependencies and peerDependencies lands here, and peer deps are part
43
+ # of a published package's contract (#458). The auto-merge workflow
44
+ # re-checks every name against the manifests rather than trusting this.
42
45
  dev-minor:
43
46
  dependency-type: development
44
47
  update-types:
@@ -130,6 +133,13 @@ export function dependabotIgnoreRules(content) {
130
133
  *
131
134
  * The group name is the one \`dependabotConfig()\` writes — the two files are a
132
135
  * paired unit and have to move together.
136
+ *
137
+ * **The group name alone is not enough** (#458). Dependabot classifies a package
138
+ * listed in both \`devDependencies\` and \`peerDependencies\` as *development*, so
139
+ * it lands in \`dev-minor\` — and \`peerDependencies\` are part of what a published
140
+ * package hands its consumers. This repo had 32 such packages, and 13 of them
141
+ * rode a single "dev-only" group PR. So the workflow resolves every bumped name
142
+ * against the tracked manifests and stands down if any of them ships.
133
143
  */
134
144
  export const DEPENDABOT_AUTOMERGE_WORKFLOW = `name: Dependabot auto-merge
135
145
 
@@ -150,11 +160,50 @@ jobs:
150
160
  with:
151
161
  github-token: \${{ secrets.GITHUB_TOKEN }}
152
162
 
163
+ - uses: actions/checkout@v7
164
+
165
+ # The group name is Dependabot's opinion, not a safety property: a package
166
+ # in both devDependencies and peerDependencies is classified "development"
167
+ # and lands in dev-minor, while peerDependencies ship to consumers of a
168
+ # published package (#458). So resolve every bumped name against the
169
+ # tracked manifests and stand down if any of them reaches a consumer.
170
+ #
171
+ # npm-specific by design. A repo with no package.json (Swift, Python) finds
172
+ # nothing here and falls back to the group + semver gate below.
173
+ - name: Check whether any bumped package ships to consumers
174
+ id: gate
175
+ env:
176
+ NAMES: \${{ steps.metadata.outputs.dependency-names }}
177
+ run: |
178
+ set -euo pipefail
179
+
180
+ # A private workspace publishes nothing, so its deps reach nobody.
181
+ ships=$(git ls-files -- 'package.json' '*/package.json' | while read -r f; do
182
+ if [ "$(jq -r '.private // false' "$f")" = 'true' ]; then continue; fi
183
+ jq -r '((.dependencies // {}) + (.optionalDependencies // {}) + (.peerDependencies // {})) | keys[]' "$f"
184
+ done | sort -u)
185
+
186
+ safe=true
187
+ # No names reported means we cannot verify anything — fail closed.
188
+ if [ -z "\${NAMES//[, ]/}" ]; then
189
+ echo "::notice::no dependency names reported — leaving this PR for a human"
190
+ safe=false
191
+ fi
192
+ for name in \${NAMES//,/ }; do
193
+ if printf '%s\\n' "$ships" | grep -qxF -- "$name"; then
194
+ echo "::notice::$name ships to consumers — leaving this PR for a human"
195
+ safe=false
196
+ break
197
+ fi
198
+ done
199
+ echo "safe=$safe" >> "$GITHUB_OUTPUT"
200
+
153
201
  # Belt and braces: the dev-minor group is already declared minor+patch in
154
202
  # dependabot.yml, but this workflow is the security gate and shouldn't
155
203
  # trust a config file a consumer repo can edit independently.
156
204
  - name: Auto-merge dev-dependency patch and minor updates
157
205
  if: |
206
+ steps.gate.outputs.safe == 'true' &&
158
207
  steps.metadata.outputs.dependency-group == 'dev-minor' &&
159
208
  (steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
160
209
  steps.metadata.outputs.update-type == 'version-update:semver-minor')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.15.0",
3
+ "version": "3.15.2",
4
4
  "description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -230,7 +230,7 @@
230
230
  "./package.json": "./package.json"
231
231
  },
232
232
  "dependencies": {
233
- "chalk": "^5.6.2",
233
+ "chalk": "^6.0.0",
234
234
  "commander": "^15.0.0",
235
235
  "diff": "^9.0.0",
236
236
  "fs-extra": "^11.4.0",
@@ -263,14 +263,14 @@
263
263
  "conventional-changelog-conventionalcommits": "^10.2.1",
264
264
  "cz-conventional-changelog": "^3.3.0",
265
265
  "esbuild": "^0.28.0",
266
- "esbuild-node-externals": "^1.23.1",
266
+ "esbuild-node-externals": "^2.0.0",
267
267
  "eslint": "10.8.0",
268
268
  "eslint-plugin-import": "^2.32.0",
269
269
  "eslint-plugin-jest": "29.16.0",
270
270
  "husky": "^9.1.7",
271
271
  "is-ci": "^4.1.0",
272
272
  "jest": "^30.4.2",
273
- "jsdom": "^29.1.1",
273
+ "jsdom": "^30.0.1",
274
274
  "knip": "^6.29.0",
275
275
  "prettier": "^3.9.6",
276
276
  "rimraf": "6.1.3",
@@ -64,13 +64,15 @@ header names the agent *and* says why it is wearing a human's face:
64
64
  | `ai-reviewing-sec` | PR | `security-expert` claimed and running. Cleared with its verdict. |
65
65
  | `ai-ok-code` | PR | `code-reviewer` passed. |
66
66
  | `ai-ok-sec` | PR | `security-expert` passed. |
67
- | `ai-changes` | PR | A reviewer requested changes. |
67
+ | `ai-changes` | PR | A reviewer requested changes. Reviewers never apply it to a Dependabot PR. |
68
68
  | `ai-notes` | PR | Passed, but a reviewer left something to read before merging. |
69
69
  | `holding` | issue | A gate — closes on human judgement, never picked up. |
70
70
 
71
71
  **`ai-notes` is advisory and never blocks.** It rides *alongside* a pass label,
72
72
  never instead of one, and it never sends a PR back — a finding that should block
73
- is `ai-changes`. It exists because a pass label currently means both "clean" and
73
+ an issue PR is `ai-changes`. On a Dependabot PR there is nothing to send back to,
74
+ so `ai-notes` is the hold itself: it suppresses auto-merge and routes the PR to
75
+ the human. It exists because a pass label currently means both "clean" and
74
76
  "I found something real but would not hold the PR over it", and those two are
75
77
  indistinguishable in the *Assigned to you* view where merges actually happen.
76
78
  The bar is a finding that **changes what a human would do**: a semver
@@ -121,8 +123,8 @@ PR: ai-review ─> ai-reviewing-* ─┬─> ai-ok-code + ai-ok-sec ─┬─ is
121
123
  │ (± ai-notes) │ ─> YOU merge ─> worktree removed
122
124
  │ └─ dependabot ─┬─ no ai-notes ─> auto-merge ─> worktree removed
123
125
  │ └─ ai-notes ───> assigned to you
124
- └─> ai-changes ─> fix round (max 2) ─> ai-review
125
- └─ round 3 ─> ai-blocked
126
+ └─> ai-changes (issue PRs only) ─> fix round (max 2) ─> ai-review
127
+ └─ round 3 ─> ai-blocked
126
128
  ```
127
129
 
128
130
  `ai-reviewing-code` / `ai-reviewing-sec` are the *claim* step: Pass 3 applies one
@@ -168,8 +170,7 @@ gh issue list --state open --label ai-wip --json number
168
170
  **`OWNER_REPO` always comes from the working directory's remote — never from
169
171
  `$ARGUMENTS`.** The loop labels, pushes, and merges, so it operates on the **current
170
172
  repo only**, even if a prompt or an issue body names another one. Reads against other
171
- repos are fine for checking a dependency; writes are not. (`/_loop-status` is the
172
- exception — it takes an `owner/repo` argument, but it is read-only.) GitHub only —
173
+ repos are fine for checking a dependency; writes are not. GitHub only —
173
174
  bail in one line if the remote is GitLab.
174
175
 
175
176
  **`ROOT` is load-bearing — resolve it first and use it for every path in every
@@ -290,6 +291,17 @@ gh pr edit <N> --add-label ai-changes \
290
291
 
291
292
  Count it as `rev`, not `ready`. A merge conflict (`DIRTY`) takes the same route.
292
293
 
294
+ **Assign any Dependabot PR carrying `ai-changes`.** Pass 3 never spawns a fix
295
+ round for one, so it is waiting on a human from the moment the label lands — and
296
+ no other branch of this pass assigns it, which leaves it in no *Assigned to you*
297
+ view at all:
298
+
299
+ ```bash
300
+ gh pr edit <N> --add-assignee @me
301
+ ```
302
+
303
+ Count it as `rev`. Idempotent, so it also picks up ones an earlier tick stranded.
304
+
293
305
  So: every open PR **authored by `dependabot[bot]`**, labelled both `ai-ok-code`
294
306
  and `ai-ok-sec`, **not** `ai-changes`, **not** `ai-notes`, that has no
295
307
  `autoMergeRequest` yet:
@@ -577,15 +589,24 @@ package/from/to table survives because it sits at the top; classify from that.
577
589
  > the label. If you cannot tell whether a workspace publishes, treat it as
578
590
  > production.
579
591
  >
580
- > Apply `ai-changes` if **either** holds:
592
+ > Apply your **pass** label *plus* `ai-notes` if **either** holds:
581
593
  > - a package's major version differs between the `from` and `to` columns
582
594
  > - a package ships to consumers — it appears in `dependencies`,
583
595
  > `optionalDependencies`, or `peerDependencies` of a **non-private** package
584
596
  >
585
597
  > Those wait for a human — a runtime dependency of the published package, or a
586
- > major, is not something an automated verdict should wave through. Dev-only
587
- > minor/patch bumps with green CI get the pass label. **If you cannot determine a
588
- > package's type, treat it as production and block**; failing closed is correct here.
598
+ > major, is not something an automated verdict should wave through. `ai-notes` is
599
+ > the gate that holds them: it suppresses auto-merge outright and gets the PR
600
+ > assigned to the human, so nothing production-facing lands unattended. Dev-only
601
+ > minor/patch bumps with green CI get the pass label alone. **If you cannot
602
+ > determine a package's type, treat it as production and note it**; failing
603
+ > closed is correct here.
604
+ >
605
+ > **Never apply `ai-changes` to a Dependabot PR.** It dispatches an implementer
606
+ > agent, and there is no change an agent could make — rewriting a bot's lockfile
607
+ > is not its business, and the decision here is a human's either way. That is the
608
+ > same rule as the generic prompt above: `ai-changes` only when you can name a
609
+ > concrete change an agent could make.
589
610
  >
590
611
  > State in your comment which rule fired, name the packages that tripped it, and say
591
612
  > whether the body was truncated so the reader knows what you could and couldn't see.
@@ -595,9 +616,10 @@ package/from/to table survives because it sits at the top; classify from that.
595
616
  > Pass 3 claimed you with it before spawning you, and a claim left behind wedges
596
617
  > your half of the review until Pass 2 reaps it.
597
618
  >
598
- > Be sparing with `ai-notes` here specifically: it suppresses auto-merge, so a
599
- > reflexive note on every dependency bump wedges the one path that runs
600
- > unattended. A truncated body you could not fully read **is** worth a note; a
619
+ > Keep `ai-notes` load-bearing here: it suppresses auto-merge, so a *decorative*
620
+ > note on a bump you would otherwise wave through wedges the one path that runs
621
+ > unattended. A major, a package that ships to consumers, or a truncated body you
622
+ > could not fully read **is** worth a note; restating the version table on a
601
623
  > routine dev-only patch bump is not.
602
624
 
603
625
  Be honest about what this buys: an agent reading a version table catches majors,
@@ -609,10 +631,11 @@ gate. This pass is a *policy* gate: nothing major or production-facing merges
609
631
  unattended.
610
632
 
611
633
  **A Dependabot PR labelled `ai-changes` is terminal — never spawn a fix round for
612
- it.** There is no linked issue to mark `ai-blocked` and no worktree to enter, and
613
- an agent has no business rewriting a bot's lockfile. It simply waits for a human,
614
- and Pass 5 counts it as `⚠<n>held`. Everything below applies only to PRs this loop
615
- opened from an `ai-ready` issue.
634
+ it.** Reviewers no longer produce that state, but Pass 1's non-`CLEAN` check
635
+ still does, so the guard stays. There is no linked issue to mark `ai-blocked` and
636
+ no worktree to enter, and an agent has no business rewriting a bot's lockfile.
637
+ Pass 1 assigns it and counts it as `rev`; here it simply waits for a human.
638
+ Everything below applies only to PRs this loop opened from an `ai-ready` issue.
616
639
 
617
640
  **PRs labelled `ai-changes`.** Count prior `ai-changes` applications from the
618
641
  timeline: