Skip to content

Commit f71f33d

Browse files
committed
Cleanup
1 parent 67cea02 commit f71f33d

5 files changed

Lines changed: 19 additions & 8 deletions

File tree

.claude/skills/repomatic-ship/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ The checks:
119119
- **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.
120120
- **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.
121121
- 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.
122123
- **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:
123124
- 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).
124125
- **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.

claude.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,15 +330,17 @@ CLI commands, workflow job IDs, PR branch names, and PR body template names must
330330
| `lint-X` | Check content without modifying it. | Linter rules | Yes | `lint-changelog` |
331331
| `pack-X` | Assemble a distributable artifact set for release. | Repository tree | Yes | `pack-binaries`, `pack-plugin` |
332332
| `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` |
333334

334335
**Rules:**
335336

336337
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`.
337338
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).
339340
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`).
340341
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.
341342
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`.
342344

343345
### Automated operation contracts
344346

repomatic/github/issue.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@
5050
from typing import Any
5151

5252

53+
BOT_ISSUE_LABEL = "🤖 ci"
54+
"""Label carried by every issue this module's lifecycle helper maintains.
55+
56+
Applied by {func}`~repomatic.github.issue.manage_issue_lifecycle` on creation.
57+
Lives here rather than in a calling module because both callers reach the label
58+
through that helper, and neither should have to import the other to agree on
59+
it. The value is one of the labels `repomatic/data/labels.toml` declares, so
60+
renaming it there means renaming it here: an issue labelled with a name the
61+
registry does not carry is created unlabelled, silently.
62+
"""
63+
5364
LOCKED_CONVERSATION_MARKER = "is locked"
5465
"""Substring GitHub returns when a write is refused on a locked conversation.
5566

repomatic/runner_images.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
from dataclasses import dataclass
5858

5959
from .github.gh import gh_api_json
60-
from .github.issue import manage_issue_lifecycle
60+
from .github.issue import BOT_ISSUE_LABEL, manage_issue_lifecycle
6161
from .github.pr_body import render_template, sanitize_markdown_mentions
6262

6363
TYPE_CHECKING = False
@@ -81,9 +81,6 @@
8181
repository rather than renaming it.
8282
"""
8383

84-
ISSUE_LABEL = "🤖 ci"
85-
"""Label applied when the issue is first created."""
86-
8784
DEPRECATION_MARKERS = (
8885
"deprecation",
8986
"deprecated",
@@ -289,7 +286,7 @@ def manage_runner_images_issue(known_runners: Iterable[str]) -> None:
289286
manage_issue_lifecycle(
290287
has_issues=bool(announcements),
291288
body=body,
292-
labels=[ISSUE_LABEL],
289+
labels=[BOT_ISSUE_LABEL],
293290
title=ISSUE_TITLE,
294291
no_issues_comment="No open runner image announcements.",
295292
)

repomatic/setup_guide.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
from .github import token
3535
from .github.gh import run_gh_command
36-
from .github.issue import manage_issue_lifecycle
36+
from .github.issue import BOT_ISSUE_LABEL, manage_issue_lifecycle
3737
from .github.pr_body import render_template
3838
from .lint_repo import (
3939
check_branch_ruleset_on_default,
@@ -417,7 +417,7 @@ def manage_setup_guide(
417417
manage_issue_lifecycle(
418418
has_issues=needs_issue,
419419
body=setup_body,
420-
labels=["🤖 ci"],
420+
labels=[BOT_ISSUE_LABEL],
421421
title="Repomatic setup guide",
422422
no_issues_comment=(
423423
"PAT configured, all permissions verified, repository settings complete."

0 commit comments

Comments
 (0)