feat: extract shared actions, own @claude, add implement/pr-checks/release/zavet workflows - #3
Merged
Merged
Conversation
Three reusable workflows each carried their own copy of the same gate, the same claude-code-action call, and (in deps-verify) its own toolchain setup. Three copies of a rule is three chances for one of them to be subtly wrong, and one already was: agent:no-touch was correct in all three only because three authors each remembered to put it first. actions/agent-gate evaluates agent:no-touch FIRST, structurally, in one place. Its logic lives in gate.sh rather than inline YAML so it can be run without a runner, and test.sh asserts the kill switch across all five workflow shapes — 22 cases, including no-touch beating workflow_dispatch, an explicit @claude command and a maintainer label. DODI-00008 was a claim about behaviour; it is now a test. actions/run-agent centralises the two footguns that cost real debugging time: --allowed-tools is variadic (a comma-joined list silently denies every Bash call and still reports success), and display_report defaults to false (a run that did nothing looks like a run that worked). Both are now impossible to get wrong. actions/setup-stack makes the stack a VALUE instead of a template. The old shape needed a thin-caller file per stack, which does not survive five stacks across three workflows — and the Gradle one had already grown a hardcoded assembleInfrasensingDebug, one product's task name in a template meant to be copied. Package caches are job-scoped here because a persistent runner shares them and a half-written entry poisons every later run, which restore-keys then faithfully restores. deps-verify takes `stack` instead of `setup`, with a deprecated alias so callers pinned at @main keep working until they are migrated. Commands default to the sentinel "@stack" so an explicit "" can still mean "skip this step" — a Gradle repo genuinely has no install step, and a plain default cannot express both. pick-runner stops failing silently. A missing GH_APP_CLIENT_ID meant validation fell back to a repo-scoped token that cannot read the org runner list, printed "skipping", and exited 0 — indistinguishable from a clean pass. That absence is now a warning that says what it costs. GH_APP_CLIENT_ID stays the single name: upstream has deprecated app-id in favour of client-id, so repos still on GH_APP_ID are migrated rather than accommodated. Adds self-test.yml, so the repo that defines the org's CI finally has some. Why: three copies of the kill switch is not one rule; and a template per stack does not reach android, flutter, ios and rust. Refs: DODI-00005, DODI-00008, DODI-00010, DODI-00012 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…checks, release and zavet checks claude-assist replaces the claude.yml every repo hand-maintained. That file was the one agent workflow the standard did not own, and it did not check agent:no-touch — the kill switch had a hole in the most permissive surface in the org, a workflow that answers any mention with write access. It also had to decline the verbs other workflows own, an exclusion hand-copied into every repo in two if: blocks each, which had already drifted: one repo guarded pull_request_review_comment and the other did not. That list is now one input. issue-implement makes agent:implement real: branch, code, draft PR, never merge. The plan requirement is a DETERMINISTIC gate, not a prompt instruction — it reads Triage state with jq and stops before a runner is picked. An agent asked to judge whether a plan is good enough will sometimes accept a two-line issue body, and the failure mode is twenty minutes of confident work on the wrong thing. There is no override, not workflow_dispatch and not a maintainer. Agent mode decides who may trigger it; the field was documented as "policy, not enforcement" and this is the first workflow that enforces it. pr-checks replaces two hand-written CI files that were the same shape with a different toolchain, and differed mostly in ways nobody chose: one had a concurrency group and the other did not, so every push to the busier repo ran a full Next.js build to completion, and neither had a path filter. release collapses five near-identical jobs that each checked out, each set up Node, and each ran the same nine-package npm install before calling semantic-release with a different --extends. They were chained to order them, not to parallelise, so they are now one ordered loop and one install. zavet-check puts the knowledge layer in CI, where it has never been. Its guarantees held only as long as every contributor had the git hooks installed and never used --no-verify. Severity mirrors those hooks exactly: decision checks and guard trailers fail, spec staleness warns. Inverting either would make CI disagree with the hooks, and then people learn to distrust one of them. Guards are checked per commit, because a trailer belongs to the commit that touched the guarded path. setup-stack now resolves command overrides, so callers read a resolved output instead of reimplementing the "@stack" precedence rule — three workflows had started to. default.json5 extracts the ~60% of Renovate config that was identical between the two repos. Only what is true of every repo is in it: ecosystem rules stay with the repo that has that ecosystem, because a rule matching nothing reads as coverage. Why: the kill switch has to cover every agent workflow, and implementing an unplanned issue is the expensive way to find out there was no plan. Refs: DODI-00003, DODI-00004, DODI-00005, DODI-00008, DODI-00011 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…o going private Eight links pointed at dodi-smart/dev-standards across README, AGENTS and CLAUDE. That repo is becoming private, at which point every one of them is a 404 for anyone outside the org — including anyone reading these workflows to work out what they do. A dead link is worse than no link, because it reads as an offer. So the reasoning is restated here rather than referenced. DODI-nnnnn ids stay as citations, but each now sits next to enough prose to act on without resolving it. That duplication is accepted deliberately: the alternative is a public repo that cannot explain itself, or a second id sequence, which was already rejected because two sequences sharing a prefix eventually mint the same id. README is now a usable public reference — quick start, required secrets, every workflow and action, the stack model, runner selection, versioning, and what to check when nothing happens. AGENTS.md keeps the governing-decision table but writes out each rule instead of pointing at it, and records the facts that have cost real debugging time: --allowed-tools is variadic, display_report defaults off, allowed_bots is load-bearing for deps-verify, the secrets context is not available in a step-level if:, and actionlint's snapshot of create-github-app-token predates client-id replacing app-id. CLAUDE.md was a byte-for-byte copy of AGENTS.md, so every change had to be made twice and the second was sometimes forgotten. It is now a pointer. Also removes three things that should not be published: a machine-class nickname list, and two product names that had crept into examples and comments — one in a release `modules:` sample, one in the comment explaining why per-stack templates were retired. The irony of naming a product in the comment about not naming products in templates is noted. Why: the repo has to stand alone the moment the other one goes private. Refs: DODI-00018 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r to fix it A @v1-pinned caller currently still picks up main's pick-runner and composite actions, which undercuts what pinning is for. The order is deliberate — repos still on @main would break if the internal refs moved first — but step 4 of that sequence is the one that gets forgotten, so it is written down rather than left as tribal knowledge. Refs: DODI-00017 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oted args `uses: docker://rhysd/actionlint` passes `args` through word-splitting that does not honour quotes, so an -ignore pattern containing spaces was torn apart and its tail read as a filename — the job failed with 'could not read "actions/create- github-app-token' -ignore ..."'. Running the pinned binary gives real shell quoting, and pins the version rather than tracking :latest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge this before https://github.com/dodi-smart/dev-standards/pull/3 — that repo’s thin callers pin
@v1here, and three of its decision checks fetch these files over the API.Why
Three reusable workflows each carried their own copy of the same gate, the same
claude-code-actioncall, and its own toolchain setup. Three copies of a rule is three chances for one to be subtly wrong — and one already was.The bugs this fixes
claude.ymlnever checkedagent:no-touch. It was the one agent workflow the standard did not own, and the most permissive in the org: it answers any@claudemention, with write access. DODI-00008 says the kill switch is evaluated before every other rule in every agent workflow; "every" has to include the widest surface.claude-assist.ymlreplaces it.@claudeexclusion guard had already drifted. It was hand-copied into every repo, twice per repo.infra-sensing-preventionguardedpull_request_review_commentandkodni.medid not, so the same comment behaved differently depending on where it was left. The verb list is now one input.pick-runnerfailed silently. A missingGH_APP_CLIENT_IDmeant validation fell back to a repo-scoped token that cannot read the org runner list, printed "skipping", and exited 0 — indistinguishable from a clean pass. That absence is now a warning that says what it costs.What is new
actions/agent-gateagent:no-touchfirst, structurally. Logic ingate.sh, so it runs without a runneractions/run-agent--allowed-toolsis variadic (a comma-joined list silently denies every Bash call and still reports success);display_reportdefaults offactions/setup-stackclaude-assist.yml@claude, declines the verbs others ownissue-implement.ymlpr-checks.yml/release.ymlzavet-check.ymlself-test.ymlThe implement gate is code, not a prompt
Triage statemust bePlan ready. Checked withjq, in a gate job, before a runner is picked, with no override — notworkflow_dispatch, not a maintainer.An agent asked "is there a plan?" is making a judgement call and will sometimes accept a two-line issue body. The cost is twenty minutes of confident work on the wrong thing, plus a PR someone must read to discover it was wrong.
Verification
kodni.me#158)self-test.ymlwill not run on this PR —claude-code-actionrefuses when the workflow differs from the default-branch copy (DODI-00013). Verify on the next PR after merge.Merge order
v1@v1@mainself-references to@v1and move the tag — step 4 is the one that gets forgotten, so it is written down inAGENTS.mdRefs: DODI-00004, DODI-00005, DODI-00008, DODI-00010, DODI-00012, DODI-00014, DODI-00015, DODI-00017, DODI-00018, DODI-00019
🤖 Generated with Claude Code