You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .claude/skills/repomatic-ship/SKILL.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -119,6 +119,7 @@ The checks:
119
119
-**Shippable deps**: `<cmd> lint-deps`. Offline and instant, and it covers the one release failure nothing else in this gate can see: a `[tool.uv.sources]` override never reaches the published metadata, so tests, types, formatting and the build all pass on a tree whose wheel every user then fails to install. Run it even on a docs-only cycle, where a lockfile bump can still have moved a source. A blocker naming a git source paired with a `.dev` floor is the `sync-dep-sources` idiom mid-flight: the fix is to wait for that swap PR, not to edit `pyproject.toml` by hand. The release lane carries the same check as a hard gate, but it fires after the freeze commit is already on `main`, where the only recovery is to burn the version and ship the next one, so a red here is cheap and a red there is not.
120
120
- **Formatting**, reproduced with the **pinned** tools, never the dev-env `uv run ruff` (a newer local ruff once silently disagreed on a `PERF401` fix): `<cmd> run autopep8 --` over the cycle's changed Python files (it wraps long-line comments ruff leaves), fed the way the Types gate feeds mypy — `git diff --name-only HEAD -- '*.py' | xargs <cmd> run autopep8 --` — and never as a shell variable holding the space-separated list, which the runner takes as one literal path and rejects with `[Errno 2] No such file or directory: 'a.py b.py c.py '`, then `<cmd> run ruff -- check` and `<cmd> run ruff -- format` (all write in place; the runner injects `--fix`), then read `git diff`: write-mode output is the reliable signal, `--check` is not. An empty diff past your reconciliation edits is green; fold a legitimate style fix into the reconciliation. For any Markdown the reconciliation touched (`changelog.md`, `docs/`), verify with the pinned `<cmd> run mdformat -- <file>`, never a bare `mdformat`/`mdformat --with mdformat-myst`: the bare form rewrites MyST directive colon-options (a `{list-table}`'s `:header-rows:`/`:widths:`) to `---` frontmatter form, diverging from CI's autofix and injecting a spurious reflow you would then have to revert.
121
121
- Landmine: autopep8 relocates a trailing `# type: ignore[...]` off a >88-char line onto its own line, voiding the suppression (Lint red under `warn_unused_ignores`); ruff format usually reverts the relocation, so only wraps that survive the full pinned sequence are real formatting debt. Never commit the relocation: fix the line length at the source so the comment rides the opening line.
122
+
- **Workflow YAML**, whenever the cycle touched `.github/workflows/`: `<cmd> run actionlint --`, `<cmd> run zizmor -- .`, and `<cmd> run yamllint --` over every workflow. Nothing else in this gate reads workflow YAML, yet step 1's code pass is told to edit it, so a regression the sweep itself introduces stays invisible until CI — and on this repo a bundled workflow carries it to every downstream repo, which gets the workflow without the conformance tests guarding it here. The class worth knowing, because a reviewer's eye slides straight past it: GitHub Actions evaluates an `if:` as an expression only when the value is *entirely* one `${{ … }}`. A folded block scalar (`if: >`) wrapped around one appends a trailing newline, so the value interpolates to a non-empty string and the step is truthy forever, gating nothing. Write a multi-line condition **bare**, with no wrapper (a leading `!` rules out the bare single-line form, since YAML reads it as a tag indicator). Eleven `autofix.yaml` steps stopped gating this way and sat red on `main` across two commits before a release run caught them.
122
123
-**Autofix externals**: smoke-run `<cmd> run typos`, **every formatter that downloads a checksum-pinned binary** (`<cmd> run biome` and peers), and the vulnerable-deps scan `<cmd> audit --fix` (parses live `uv audit` output). An upstream re-publish flips a pinned SHA-256 and kills the step; pytest mocks these, so the drift (or a changed output schema) surfaces only here or in CI's `autofix` run. A pin living upstream in `repomatic` breaks every downstream repo and cannot be patched here: surface it for the step-8 upstream report. Invocation rules:
123
124
- Arg-needing tools take their CI-shaped args after the `--` separator (`<cmd> run pyproject-fmt -- pyproject.toml`, `<cmd> run zizmor -- .`): a bare `<cmd> run biome` still downloads and checksum-verifies the binary, but then exits non-zero on its own usage error, indistinguishable from real drift in a scripted gate (a bare `<cmd> run shfmt` likewise errors on stdin).
124
125
-**Never hand a write-mode formatter an unscoped path**, and do not mistake one for a CI-shaped invocation. `biome check --write .` and `shfmt -- .` resemble what `autofix.yaml` runs and are not it: that workflow scopes biome to the `json_files` metadata list and runs `format --write --no-errors-on-unmatched` with the JSONC parse flags, never `check` over `.`. The unscoped form also descends into gitignored build output, since biome ships no `.gitignore` awareness by default and this repo carries no `biome.jsonc` to switch it on: one `.` walked a `docs/_build` tree into 79,553 reported errors while retabbing two tracked files (`docs/_static/custom.css`, `repomatic/data/vt-trend-chart.js`) that no CI job formats. Smoke these with `--version` per the next rule instead: the pin is what this check exists to verify, not the formatting.
Copy file name to clipboardExpand all lines: claude.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -330,15 +330,17 @@ CLI commands, workflow job IDs, PR branch names, and PR body template names must
330
330
|`lint-X`| Check content without modifying it. | Linter rules | Yes |`lint-changelog`|
331
331
|`pack-X`| Assemble a distributable artifact set for release. | Repository tree | Yes |`pack-binaries`, `pack-plugin`|
332
332
|`scan-X`| Submit artifacts to an external analysis service. | External API | Yes |`scan-virustotal`|
333
+
|`{noun}`| Maintain a GitHub issue tracking a repo condition. | GitHub API, settings | Yes |`runner-images`, `setup-guide`|
333
334
334
335
**Rules:**
335
336
336
337
1.**Pick the verb that matches the data source.** External template/API/canonical reference: `sync`. Local project state (lockfiles, git history, source): `update`. Reformatting: `format`.
337
338
2.**Name the specific tool or file, not a generic category** (`sync-zizmor`, not `sync-linter-configs`). A second tool in a category gets its own operation.
338
-
3. **All four dimensions must agree.** A file-modifying operation uses one `verb-noun` for CLI command, workflow job ID, PR branch, and PR body template (`sync-gitignore` everywhere). Operations that write no repository file use only the CLI command and job ID: `lint-*`, which reads, and `pack-*`, which emits a build artifact. Two exceptions: release-lane recording (`scan-virustotal` and the `sync-binaries` catalog it commits) has no PR branch or template because it pushes directly to the default branch, disableable via `[tool.repomatic] binaries.sync`, see [§ Release-lane direct commits](https://kdeldycke.github.io/repomatic/operation-contracts.html#release-lane-direct-commits); and `fix-awesome-toc` runs as a step of `format-markdown` because it corrects what that job just wrote and the two would otherwise fight across separate PRs, see [§ Fix steps inside another job](https://kdeldycke.github.io/repomatic/operation-contracts.html#fix-steps-inside-another-job). A job local to this repository (one with no upstream template to name on `pr-body --template`) keeps the same identity: its template is `.github/pr-templates/{job-id}.md`, passed with `--template-file`, and `repomatic lint-repo` flags one placed elsewhere. See [§ Repository-local templates](https://kdeldycke.github.io/repomatic/operation-contracts.html#repository-local-templates).
339
+
3. **All four dimensions must agree.** A file-modifying operation uses one `verb-noun` for CLI command, workflow job ID, PR branch, and PR body template (`sync-gitignore` everywhere). Operations that write no repository file use only the CLI command and job ID: `lint-*`, which reads, `pack-*`, which emits a build artifact, and the bare-noun issue trackers of rule 7. Two exceptions: release-lane recording (`scan-virustotal` and the `sync-binaries` catalog it commits) has no PR branch or template because it pushes directly to the default branch, disableable via `[tool.repomatic] binaries.sync`, see [§ Release-lane direct commits](https://kdeldycke.github.io/repomatic/operation-contracts.html#release-lane-direct-commits); and `fix-awesome-toc` runs as a step of `format-markdown` because it corrects what that job just wrote and the two would otherwise fight across separate PRs, see [§ Fix steps inside another job](https://kdeldycke.github.io/repomatic/operation-contracts.html#fix-steps-inside-another-job). A job local to this repository (one with no upstream template to name on `pr-body --template`) keeps the same identity: its template is `.github/pr-templates/{job-id}.md`, passed with `--template-file`, and `repomatic lint-repo` flags one placed elsewhere. See [§ Repository-local templates](https://kdeldycke.github.io/repomatic/operation-contracts.html#repository-local-templates).
339
340
4.**Function names follow the CLI name** (`sync_gitignore` for `sync-gitignore`). On collision with an imported module, use the Click `name=` parameter (`@repomatic.command(name="update-dep-graph")` on `dep_graph`) or append `_cmd` (`sync_uv_lock_cmd`).
340
341
5.**A read-only command may expose mutation via `--fix`.** When a query command (like `audit`) gains a `--fix` autofix mode, the autofix operation keeps its own `fix-X` job ID, PR branch, and template and invokes `<command> --fix` (`fix-vulnerable-deps` runs `audit --fix`). That command name is then exempt from rule 3: it is a general-purpose query command (like `metadata`), not the operation's namesake.
341
342
6.**`dep` when attributive, `deps` when the object.** A "dep" prefix modifying another noun stays singular, following English compound-noun convention (`dep-graph`, `dep-sources`, `dep_report.py`); "deps" appears only where the dependencies are themselves the object of the verb (`sync-deps`, `vulnerable-deps`).
343
+
7.**An operation whose output is a GitHub issue takes a bare noun, no verb prefix** (`runner-images`, `setup-guide`). It writes no repository file: it upserts one issue through {func}`~repomatic.github.issue.manage_issue_lifecycle`, which opens, updates, closes and reopens it, matched on the exact title. Every such command shares that one mechanism, so a verb would name the mechanism rather than the subject and they would all collapse onto the same prefix; the noun instead names what the issue tracks, keeping the command and the issue title the same phrase. Body fragments live in `repomatic/templates/{name}[-{fragment}].md` and render through {func}`~repomatic.github.pr_body.render_template`: that renderer is shared with PR bodies, but these are issue bodies, so `pr-body --template` never names one and rule 3's PR-branch and PR-template dimensions do not apply. Every issue carries {data}`~repomatic.github.issue.BOT_ISSUE_LABEL`.
0 commit comments