# Operations
Repo and CI setup, moved out of `AGENTS.md`. Anything this page cannot know from the
repository's own files is marked **verify in repo settings**.
## Repo / CI setup
- GitHub: `ckeller42/trackiwi-client`, **public**, MIT-licensed (`LICENSE` at the
repo root, wired into `pyproject.toml` via `license = "MIT"` / `license-files`,
with the badge in the README).
- **Docs hosting is live.** `pages.yml` builds the Sphinx site
(`sphinx-build -b html -W docs docs/_build/html`) on every push to `main` (and on
manual dispatch) and deploys it with `actions/deploy-pages`; the README links
the result at . It needs Pages
set to "GitHub Actions" as the source (**verify in repo settings**). The same
`-W` build also gates every PR through the `docs` job in `ci.yml`.
- **Branch ruleset on `main` is active** (AGENTS.md, "Workflow"): a PR is required,
conversations must be resolved, and these checks must pass with the branch up
to date. The names are load-bearing; do not rename the workflow (`CI`) or its jobs:
`pre-commit`, `test (3.11)`, `test (3.12)`, `test (3.13)`, `typecheck`, `docs`.
Never add a `name:` to the matrix job (it would change `test (3.x)`) and keep the
matrix values strings. The `build` job (packaging smoke test) and the Claude jobs
are **not** required.
- The repo's reference ruleset payload is kept below so it can be re-applied or
compared. Whether the live ruleset still matches it exactly is **verify in repo
settings** (Settings, Rules, Rulesets).
```bash
gh api repos/ckeller42/trackiwi-client/rulesets -X POST --input - <<'JSON'
{
"name": "main: PR required, conversations resolved, CI green",
"target": "branch",
"enforcement": "active",
"bypass_actors": [],
"conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } },
"rules": [
{ "type": "pull_request", "parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": false,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": true,
"automatic_copilot_code_review_enabled": false,
"allowed_merge_methods": ["merge", "squash", "rebase"] } },
{ "type": "required_status_checks", "parameters": {
"strict_required_status_checks_policy": true,
"do_not_enforce_on_create": false,
"required_status_checks": [
{ "context": "pre-commit" },
{ "context": "test (3.11)" }, { "context": "test (3.12)" },
{ "context": "test (3.13)" },
{ "context": "typecheck" }, { "context": "docs" } ] } },
{ "type": "deletion" },
{ "type": "non_fast_forward" }
]
}
JSON
```
Rulesets are preferred over legacy branch protection here. Two parameters
are deliberate and should not be "corrected": `required_approving_review_count:
0`, because a solo owner cannot approve their own PR and `1` would lock them
out of merging entirely; and `bypass_actors: []`, which is the rulesets
equivalent of `enforce_admins: true` — without it the owner can push straight
past the rule, which defeats the point.
- GitHub-native secret scanning and push protection: **verify in repo settings**
(they complement the three scanners below).
- **Workflow conventions** (every workflow, enforced by review and by `zizmor`):
top-level `permissions` of `contents: read` or `{}` with wider scopes only on the
job that needs them; `persist-credentials: false` on every checkout; a
`timeout-minutes` on every job; every `uses:` pinned to a full commit SHA with a
`# vX.Y.Z` comment. `ci.yml` cancels superseded runs on pull requests only, so a
push to `main` always runs to completion.
- **Workflow linting** runs inside the existing `pre-commit` job: `actionlint` and
`zizmor` (offline audits only; the hook has no token). The one deliberate
`zizmor` exception is the `workflow_run` trigger in `dependabot-auto-merge.yml`,
annotated in place with its reason.
- **Dependabot** (`.github/dependabot.yml`): weekly, with a 7-day cooldown for
version updates (security updates ignore it). github-actions, pip and pre-commit
updates are grouped by minor/patch; majors stay one PR each. `ruff` is pinned in
both `pyproject.toml` and `.pre-commit-config.yaml` and the two must match, so
both bumps share one cross-ecosystem `ruff` group (a single PR). That grouping
uses Dependabot's `multi-ecosystem-groups`; check the first such PR actually
carries both files.
- **Packaging smoke test:** the `build` job in `ci.yml` runs `python -m build`,
`twine check --strict dist/*`, installs the wheel into a clean venv and runs
`trackiwi --help`. Releases themselves are manual (tag the merge commit, create
a GitHub release; see AGENTS.md).
- **The Claude review job (`claude-code-review.yml`) fails on a silent
non-review, by design.** Its last step reads the action's `execution_file`
and exits 1 when the result carries any `permission_denials` (printing the
denied tool and input; the sanitised log shows only a count), when Claude
produced no `result` message or a non-`success` one, or when Claude never
ran. Three things learned fixing #8 that the action does not tell you:
1. **A PR that edits that workflow is never reviewed by it.** The action's
OIDC token exchange refuses a workflow file that differs from `main`
("Workflow validation failed … identical content to the version on the
repository's default branch") and the job used to pass anyway. Now the
gate step turns it red with a message naming this cause. To test a
workflow change on its own PR, pass `github_token: ${{ github.token }}`
temporarily (it skips the exchange; comments then come from
`github-actions[bot]`) and remove it before merge.
2. **The prompt is a slash command, so the CLI runs it through the `Skill`
tool**, which must be in `--allowedTools` as
`Skill(code-review:code-review *)`. This was the one denial left after
#6 and #7.
3. **Sub-agents must run in the foreground.** In a headless (SDK) session a
sub-agent runs in the background unless Claude asks otherwise, and
nothing ever wakes the session up again, so the review ended after six
turns with zero denials and nothing posted.
`CLAUDE_CODE_DISABLE_BACKGROUND_TASKS=1` on the action step forces the
foreground. `gh run rerun --debug` does **not** reveal the transcript
(the action keys on the `ACTIONS_STEP_DEBUG` env var, which a debug
re-run does not set); `show_full_output: true` does, temporarily.
`gh api` is deliberately not allowed: it can write with `-X`, and the review
gets the same data from `gh pr view --json` / `gh pr diff`. A sub-agent
that improvises one still trips the gate, which is the intended trade-off.
- **The CodeRabbit app install is a manual browser step** and cannot be
automated: . The committed
`.coderabbit.yaml` only configures it once installed.
- Dependabot auto-merge is `workflow_run`-triggered, so it only runs after CI
already passed for that exact commit. It merges **minor/patch only**; majors
are left for manual review (it reads the `update-type` trailer of every
dependency in the PR's commits, so a grouped PR is merged only if none of its
members is a major). It deliberately does not rely on the repo's
"Allow auto-merge" setting — do not enable that toggle expecting it to help.
- **CI's `pre-commit` job runs `pre-commit run --all-files`, never a bare `ruff`
call.** Two reasons, both learned the hard way: the `ruff-format` hook is
scoped to Python on purpose, and a bare `ruff format` also rewrites Python
code fences inside `docs/` (it silently restructured a fenced snippet in the
plan document once, changing its meaning); and `pre-commit` in CI re-runs the
no-private-data guard, so a local `git commit --no-verify` that skipped it is
*detected* after the push — at which point the data is already in the remote's
history and the fix is a history rewrite, not a blocked commit. CI catches the
bypass; it does not prevent it.