feat(reusable): resolve the reviewer image tag at runtime (GADFLY_REVIEWER_TAG)
Build & push image / build-and-push (pull_request) Successful in 5s
Gadfly review (reusable) / review (pull_request) Successful in 6m42s
Adversarial Review (Gadfly) / review (pull_request) Successful in 6m43s

Nearly every consumer re-pin of review-reusable.yml was a reviewer IMAGE bump,
not a structural change — but the tag was baked into a `uses: docker://` step,
which is parsed before any expression context exists and so can't read a
variable. Run the reviewer as the JOB container instead (container.image DOES
accept expressions) with an explicit `run: /entrypoint.sh` step: the tag now
resolves per-run via inputs.reviewer_tag → user var GADFLY_REVIEWER_TAG → the
baked fallback pin (sha-b37cd09, unchanged).

A Gadfly release becomes: build the image, update the ONE user-level variable —
every consumer pinned to this file's @<sha> picks it up on its next review with
no re-pin. Workflow re-pins remain only for structural yml changes (this is one
— intended to be the last routine one).

- Entrypoint contract unchanged: same env block, same /entrypoint.sh brains
  (bash + entrypoint already live in the image; ENTRYPOINT was /entrypoint.sh).
- Keep the variable on immutable sha- tags, never :latest (act_runner caches
  :latest and often does not re-pull a moved one).
- Tradeoff noted in comments: vars are unmasked and move without a commit
  trail — same blast radius as editing this file, minus the audit trail.
- README (image/pinning para + Central config table) and examples/reusable.yml
  updated per the maintenance rule.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-07-22 22:43:37 -04:00
co-authored by Claude Fable 5
parent c9dab69d14
commit 4dafac0d13
3 changed files with 40 additions and 13 deletions
+30 -11
View File
@@ -24,11 +24,14 @@
# if you accept that exposure; the explicit form is recommended. GITEA_TOKEN is # if you accept that exposure; the explicit form is recommended. GITEA_TOKEN is
# the automatic job token (no need to forward it). # the automatic job token (no need to forward it).
# #
# Advisory only — never blocks a merge. The image is pinned to an immutable # Advisory only — never blocks a merge. The reviewer image tag ALSO resolves at
# :sha- tag here (act_runner caches :latest); bump it per Gadfly release. # runtime (inputs.reviewer_tag → user var GADFLY_REVIEWER_TAG → the fallback pin
# Consumers should pin `uses: ...@v1` — a curated release tag moved on deliberate # baked into the `container:` line below), so a Gadfly release is: build the
# releases, so central tuning here propagates without per-consumer edits — or a # image, update ONE variable — no consumer re-pin. Re-pin the workflow ref only
# full `@<sha>` for an immutable pin. Avoid `@main` (moves on every push). # for structural changes to this file.
# Consumers should pin `uses: ...@<sha>` — long-lived act_runners cache this file
# by ref, so a moved tag (@v1) or @main is often NOT re-fetched and silently runs
# a stale copy.
name: Gadfly review (reusable) name: Gadfly review (reusable)
@@ -43,6 +46,7 @@ on:
# NOT re-fetched; only a runtime value or a fresh @<sha> bypasses the cache). # NOT re-fetched; only a runtime value or a fresh @<sha> bypasses the cache).
# #
# Owner-set user-scope variables (see README "Central config via variables"): # Owner-set user-scope variables (see README "Central config via variables"):
# GADFLY_REVIEWER_TAG (the reviewer image tag this reusable runs),
# GADFLY_DEFAULT_MODELS, GADFLY_DEFAULT_SPECIALISTS, # GADFLY_DEFAULT_MODELS, GADFLY_DEFAULT_SPECIALISTS,
# GADFLY_DEFAULT_PROVIDER_LENS_CONCURRENCY (the provider-wide lens budget), # GADFLY_DEFAULT_PROVIDER_LENS_CONCURRENCY (the provider-wide lens budget),
# GADFLY_ENDPOINT_NETHERSTORM (the local GPU box endpoint). # GADFLY_ENDPOINT_NETHERSTORM (the local GPU box endpoint).
@@ -59,6 +63,7 @@ on:
max_steps: { type: string, default: "14" } # GADFLY_MAX_STEPS max_steps: { type: string, default: "14" } # GADFLY_MAX_STEPS
worker_model: { type: string, default: "" } # GADFLY_WORKER_MODEL worker_model: { type: string, default: "" } # GADFLY_WORKER_MODEL
allowed_users: { type: string, default: "" } # GADFLY_ALLOWED_USERS (consumer-specific; set in your stub) allowed_users: { type: string, default: "" } # GADFLY_ALLOWED_USERS (consumer-specific; set in your stub)
reviewer_tag: { type: string, default: "" } # reviewer image tag (e.g. "sha-b37cd09") — empty falls back to user var GADFLY_REVIEWER_TAG, then the pin baked into the container: line
# Gitea >= 1.27 does not propagate the CALLER's workflow_dispatch inputs into a # Gitea >= 1.27 does not propagate the CALLER's workflow_dispatch inputs into a
# called workflow's github.event, so a manual "review PR #N" dispatch arrived # called workflow's github.event, so a manual "review PR #N" dispatch arrived
# here with an empty PR and died at the entrypoint's "PR required" check. The # here with an empty PR and died at the entrypoint's "PR required" check. The
@@ -98,13 +103,27 @@ jobs:
review: review:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: ${{ inputs.timeout_minutes }} timeout-minutes: ${{ inputs.timeout_minutes }}
# The reviewer runs as the JOB container (steps exec inside it), not a
# `uses: docker://` step: a job container image accepts ${{ }} expressions,
# while a `uses:` ref is parsed before any context exists. That lets the tag
# resolve per-run — inputs.reviewer_tag → user var GADFLY_REVIEWER_TAG → the
# baked fallback — so bumping the reviewer image is a ONE-variable edit that
# reaches every consumer despite their cached workflow ref.
#
# Always point the variable at an immutable :sha-<short> tag, never :latest —
# act_runner caches :latest and often does NOT re-pull a moved one; a fresh
# unique tag forces the pull. NB: vars are editable at will — whoever can edit
# the owner's variables redirects every consumer's reviewer image (same blast
# radius as editing this file, but without a commit trail).
#
# Fallback pin: sha-b37cd09 — the provider-wide lens budget (PR #27) on top of
# the opencode CLI engine (PR #26) and the Gitea >= 1.27 workflow_call
# reclassification. Keep it current-ish when touching this file anyway.
container:
image: gitea.stevedudenhoeffer.com/steve/gadfly:${{ inputs.reviewer_tag || vars.GADFLY_REVIEWER_TAG || 'sha-b37cd09' }}
steps: steps:
# Pin the reviewer image to an immutable sha (act_runner caches :latest, so a - name: Run the gadfly reviewer
# moved :latest is often NOT re-pulled). sha-b37cd09 adds the provider-wide run: /entrypoint.sh
# lens budget (PR #27: one shared lens-permit pool per provider, the model cap
# removed) on top of the opencode CLI engine (PR #26) and the Gitea >= 1.27
# workflow_call reclassification. Bump per Gadfly release.
- uses: docker://gitea.stevedudenhoeffer.com/steve/gadfly:sha-b37cd09
env: env:
# --- event context (from the CALLER's github.*) ------------------- # --- event context (from the CALLER's github.*) -------------------
GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }}
+7
View File
@@ -401,6 +401,12 @@ pinned version (plus `:latest`). Pin full-stub consumers to a `:vN` image tag fo
(`@v1`) or `@main` is often **not** re-fetched and silently runs a stale copy. A fresh `@<sha>` is the (`@v1`) or `@main` is often **not** re-fetched and silently runs a stale copy. A fresh `@<sha>` is the
only reliable way to roll out a *structural* change to the reusable. only reliable way to roll out a *structural* change to the reusable.
Structural changes are the rare case, though: the reviewer **image tag** the reusable runs resolves at
runtime (`reviewer_tag` input → user var `GADFLY_REVIEWER_TAG` → the fallback pin baked into the
reusable), so a routine Gadfly release is *build the image → update `GADFLY_REVIEWER_TAG` to the new
`sha-<short>`* — every pinned consumer picks it up on its next review, no re-pin. Always point the
variable at an immutable `sha-` tag, never `:latest` (the runner caches `:latest`).
### Central config via variables ### Central config via variables
So you don't have to re-pin every consumer just to retune the swarm, the reusable resolves its config So you don't have to re-pin every consumer just to retune the swarm, the reusable resolves its config
@@ -410,6 +416,7 @@ on its next review **without** a re-pin or a tag move:
| Variable (user/org scope) | Sets | | Variable (user/org scope) | Sets |
|---|---| |---|---|
| `GADFLY_REVIEWER_TAG` | the reviewer **image tag** the reusable runs (e.g. `sha-b37cd09`); empty ⇒ the fallback pin baked into the reusable |
| `GADFLY_DEFAULT_MODELS` | `GADFLY_MODELS` (csv) | | `GADFLY_DEFAULT_MODELS` | `GADFLY_MODELS` (csv) |
| `GADFLY_DEFAULT_SPECIALISTS` | the lens suite | | `GADFLY_DEFAULT_SPECIALISTS` | the lens suite |
| `GADFLY_DEFAULT_PROVIDER_LENS_CONCURRENCY` | the per-provider lens budget (lens passes in flight per provider, shared across its models) | | `GADFLY_DEFAULT_PROVIDER_LENS_CONCURRENCY` | the per-provider lens budget (lens passes in flight per provider, shared across its models) |
+3 -2
View File
@@ -17,8 +17,9 @@
# review never touches), so prefer the explicit form. Pin to an immutable # review never touches), so prefer the explicit form. Pin to an immutable
# @<sha>: long-lived act_runners CACHE the reusable by ref, so a moved tag (@v1) # @<sha>: long-lived act_runners CACHE the reusable by ref, so a moved tag (@v1)
# or @main is often not re-fetched and silently runs a stale copy. Bump the @<sha> # or @main is often not re-fetched and silently runs a stale copy. Bump the @<sha>
# to adopt a structural change; routine swarm tuning rides owner variables (see # only to adopt a structural change; routine swarm tuning AND reviewer image
# the gadfly README "Central config via variables") with no re-pin needed. # releases ride owner variables (GADFLY_DEFAULT_*, GADFLY_REVIEWER_TAG — see the
# gadfly README "Central config via variables") with no re-pin needed.
# #
# For custom named endpoints (GADFLY_ENDPOINT_<NAME>) or a provider the reusable # For custom named endpoints (GADFLY_ENDPOINT_<NAME>) or a provider the reusable
# doesn't map, use the full stub in adversarial-review.yml instead. # doesn't map, use the full stub in adversarial-review.yml instead.