From 4dafac0d1360bba6fd2c7290f708f7cb9d8445e4 Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Wed, 22 Jul 2026 22:43:37 -0400 Subject: [PATCH] feat(reusable): resolve the reviewer image tag at runtime (GADFLY_REVIEWER_TAG) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 @ 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 --- .gitea/workflows/review-reusable.yml | 41 ++++++++++++++++++++-------- README.md | 7 +++++ examples/reusable.yml | 5 ++-- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/.gitea/workflows/review-reusable.yml b/.gitea/workflows/review-reusable.yml index f1e7504..d471fb3 100644 --- a/.gitea/workflows/review-reusable.yml +++ b/.gitea/workflows/review-reusable.yml @@ -24,11 +24,14 @@ # if you accept that exposure; the explicit form is recommended. GITEA_TOKEN is # the automatic job token (no need to forward it). # -# Advisory only — never blocks a merge. The image is pinned to an immutable -# :sha- tag here (act_runner caches :latest); bump it per Gadfly release. -# Consumers should pin `uses: ...@v1` — a curated release tag moved on deliberate -# releases, so central tuning here propagates without per-consumer edits — or a -# full `@` for an immutable pin. Avoid `@main` (moves on every push). +# Advisory only — never blocks a merge. The reviewer image tag ALSO resolves at +# runtime (inputs.reviewer_tag → user var GADFLY_REVIEWER_TAG → the fallback pin +# baked into the `container:` line below), so a Gadfly release is: build the +# image, update ONE variable — no consumer re-pin. Re-pin the workflow ref only +# for structural changes to this file. +# Consumers should pin `uses: ...@` — 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) @@ -43,6 +46,7 @@ on: # NOT re-fetched; only a runtime value or a fresh @ bypasses the cache). # # 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_PROVIDER_LENS_CONCURRENCY (the provider-wide lens budget), # GADFLY_ENDPOINT_NETHERSTORM (the local GPU box endpoint). @@ -59,6 +63,7 @@ on: max_steps: { type: string, default: "14" } # GADFLY_MAX_STEPS worker_model: { type: string, default: "" } # GADFLY_WORKER_MODEL 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 # 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 @@ -98,13 +103,27 @@ jobs: review: runs-on: ubuntu-latest 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- 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: - # Pin the reviewer image to an immutable sha (act_runner caches :latest, so a - # moved :latest is often NOT re-pulled). sha-b37cd09 adds the provider-wide - # 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 + - name: Run the gadfly reviewer + run: /entrypoint.sh env: # --- event context (from the CALLER's github.*) ------------------- GITEA_API: ${{ github.server_url }}/api/v1/repos/${{ github.repository }} diff --git a/README.md b/README.md index 3a333b3..f956552 100644 --- a/README.md +++ b/README.md @@ -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 `@` is the 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-`* — 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 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 | |---|---| +| `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_SPECIALISTS` | the lens suite | | `GADFLY_DEFAULT_PROVIDER_LENS_CONCURRENCY` | the per-provider lens budget (lens passes in flight per provider, shared across its models) | diff --git a/examples/reusable.yml b/examples/reusable.yml index 6d3b385..d3e59ce 100644 --- a/examples/reusable.yml +++ b/examples/reusable.yml @@ -17,8 +17,9 @@ # review never touches), so prefer the explicit form. Pin to an immutable # @: 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 @ -# to adopt a structural change; routine swarm tuning rides owner variables (see -# the gadfly README "Central config via variables") with no re-pin needed. +# only to adopt a structural change; routine swarm tuning AND reviewer image +# 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_) or a provider the reusable # doesn't map, use the full stub in adversarial-review.yml instead.