Skip to content

feat: extract shared actions, own @claude, add implement/pr-checks/release/zavet workflows - #3

Merged
azlekov merged 5 commits into
mainfrom
feat/shared-workflow-foundation
Aug 18, 2026
Merged

feat: extract shared actions, own @claude, add implement/pr-checks/release/zavet workflows#3
azlekov merged 5 commits into
mainfrom
feat/shared-workflow-foundation

Conversation

@azlekov

@azlekov azlekov commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Merge this before https://github.com/dodi-smart/dev-standards/pull/3 — that repo’s thin callers pin @v1 here, 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-action call, 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.yml never checked agent:no-touch. It was the one agent workflow the standard did not own, and the most permissive in the org: it answers any @claude mention, 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.yml replaces it.
  • The @claude exclusion guard had already drifted. It was hand-copied into every repo, twice per repo. infra-sensing-prevention guarded pull_request_review_comment and kodni.me did not, so the same comment behaved differently depending on where it was left. The verb list is now one input.
  • pick-runner failed 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.
  • A product’s Gradle task name was baked into a shared template, in a file every other Gradle repo was told to copy.

What is new

actions/agent-gate agent:no-touch first, structurally. Logic in gate.sh, so it runs without a runner
actions/run-agent --allowed-tools is variadic (a comma-joined list silently denies every Bash call and still reports success); display_report defaults off
actions/setup-stack 8 stacks, job-scoped caches, resolved commands
claude-assist.yml owns @claude, declines the verbs others own
issue-implement.yml branch → draft PR, never merges
pr-checks.yml / release.yml replace two hand-written CI files and five near-clone release jobs
zavet-check.yml knowledge layer in CI for the first time
self-test.yml the repo defining the org’s CI now has some

The implement gate is code, not a prompt

Triage state must be Plan ready. Checked with jq, in a gate job, before a runner is picked, with no override — not workflow_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

  • 28/28 gate tests, including all 6 kill-switch cases across every workflow shape
  • Field-read logic verified against real data (kodni.me#158)
  • actionlint + shellcheck clean, every YAML parses, Renovate preset parses

self-test.yml will not run on this PR — claude-code-action refuses when the workflow differs from the default-branch copy (DODI-00013). Verify on the next PR after merge.

Merge order

  1. Merge this
  2. Cut v1
  3. Migrate repos to @v1
  4. Then flip the internal @main self-references to @v1 and move the tag — step 4 is the one that gets forgotten, so it is written down in AGENTS.md

Refs: DODI-00004, DODI-00005, DODI-00008, DODI-00010, DODI-00012, DODI-00014, DODI-00015, DODI-00017, DODI-00018, DODI-00019

🤖 Generated with Claude Code

azlekov and others added 5 commits August 18, 2026 20:44
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>
@azlekov
azlekov merged commit 8cab0c8 into main Aug 18, 2026
1 check passed
@azlekov
azlekov deleted the feat/shared-workflow-foundation branch August 18, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant