Repository navigation
chore(ci): stop gh rejecting --slurp with --jq in PR image publish - #146
Conversation
The Publish PR Image workflow's "Check for image artifact" step combined
gh's own --jq flag with --paginate --slurp, which gh rejects at runtime
("the --slurp option is not supported with --jq or --template"), failing
the job on runs that produce a pr-image artifact.
Pipe the slurped output to an external jq instead, preserving the exact
intended filter. This matches the sibling "Resolve trusted PR identity"
step, which already pipes --paginate --slurp output to jq. gh applies its
own --jq per page during --paginate, so the external jq (over the slurped
outer array of page objects) is also what makes the count cover every
page rather than only the last one.
Fixes #145
mix hex.audit fails deterministically on head: decimal 3.1.1 falls inside the vulnerable range (>= 0.1.0, < 3.1.2) of advisory EEF-CVE-2026-97853 (Unbounded allocation in Decimal.round/3, published 2026-10-10T19:43Z), which turned the required Lint and Test gate red ~15h after main's green run. 3.2.0 is the newest version satisfying every consumer constraint (ecto/ecto_sql/ecto_sqlite3 require ~> 3.0). No Mix toolchain exists in this lane, so the lock line was derived from the published decimal-3.2.0.tar: the trailing checksum is hex.pm's authoritative release checksum (190bca80), the fourth field is the canonical inner checksum recomputed from the tarball (sha256 over VERSION, metadata.config and contents.tar.gz per hexpm/hex mix_hex_tarball.erl) and equals the tarball's own CHECKSUM member. The method was validated by exact reproduction of the locked decimal 3.1.1 and bunt 1.0.0 entries. metadata.config confirms no deps, build_tools [mix].
|
Response to the AI review's single blocker ("Lint and Test failed on b39173a, no logs in corpus") — diagnosed and resolved in Root cause, with evidence:
Fix: |
Superseded by a newer automated review for this pull request.
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: glm-5.3-flash (openai) — routed smart (risk match: linked_priority_p1) · pr-reviewer-action v3.2.0
Partial coverage: required-check coverage is incomplete — this review did not resolve every required check and must not be read as a complete pass.
Findings (1 info)
| Severity | Location | Finding |
|---|---|---|
| Info | .github/workflows/pr-image-publish.yml:51 |
The new comment's claim that gh would apply --jq per page during --paginate is consistent with gh's flag documentation but was not directly verified against gh's source in this review; the rejection itself is confirmed by the runtime error in issue PR 145. |
Recommendation: Approve
Both changes are correct, minimal, and verified. The workflow fix resolves the exact runtime failure captured in issue PR 145 (gh rejecting --slurp combined with its own --jq), preserves the intended filter byte-for-byte, and keeps the step fail-closed. The decimal 3.1.1 → 3.2.0 lock bump clears the newly published advisory, satisfies every consumer constraint in mix.lock/mix.exs, and is validated end-to-end by a green Lint and Test run on the head commit (which includes mix deps.get, mix hex.audit, and the full ex_unit suite).
Change-by-change findings
1. .github/workflows/pr-image-publish.yml — Check for image artifact step
The change replaces gh's own --jq with a pipe to an external jq, keeping the filter [.[] | .artifacts[] | select(.name == "pr-image")] | length identical. Verified against the full workflow file:
- The step still runs under
set -euo pipefail; withpipefail, a failedghorjqin the pipeline makes the command substitution (and thus the step) exit non-zero, so the fail-closed property is preserved. - The sibling
Resolve trusted PR identitystep (~line 105) already used exactly this pattern (gh api --paginate --slurp … | jq --arg repo … '[.[][] | …]') and is untouched, as the PR states. - A grep of
.github/workflows/for--slurp|--paginate|--jq|--templatereturns matches only in this file (lines 49, 51, 53, 105): the two--slurpcall sites both pipe to externaljq, and no--paginategh invocation anywhere combines--slurpwith--jq/--template. The PR's grep assertion holds. - The jq filter is semantically correct for this endpoint:
/actions/runs/{id}/artifactsreturns a page object ({total_count, artifacts:[…]}) per page,--slurpwraps all pages into one outer array (confirmed by gh's own flag help captured in the issue log: "return an array of all pages of either JSON arrays or objects"), so.[] | .artifacts[]reduces over every artifact on every page andlengthyields one total. The sibling step's.[][]differs because/pullsreturns an array per page — the asymmetry is intentional and correct.
Claim scorecard:
- Claim 1 (gh applies
--jqper page during--paginate): partially verified. The load-bearing half — gh rejects--slurpwith--jq/--template— is confirmed by the runtime error captured in issue PR 145's log. The per-page application detail was not directly observed: the fetchedcli/clipkg/cmd/api/api.goblob (both via the contents API and the git-blobs API) was truncated before the paginate loop, though theApiOptionsstruct withSlurp/FilterOutputfields and the flag help text were seen. This detail appears only in an explanatory comment and does not affect the fix's correctness. - Claim 2 (
--paginate --slurp+ externaljqyields a single total across pages): held, 3/3 workflow items. The fourth listed item (mix.lock:L12) is the decimal lock line, not a call site of this filter — an extraction artifact, not a counterexample. - Claim 3 (no
--paginate --slurpcombined with gh's--jq/--templateanywhere in.github/workflows/; sibling already correct): held, 3/3 items — independently reproduced by grep. - Claim 4 (count covers every page, not just the first 30): held for all workflow items;
mix.lock:L12is again not a call site of the count logic. The multi-page/no-match/empty simulations described in the PR body match the filter's semantics as read. - Claim 5 (fail-closed under
set -euo pipefail): held, 4/4 items — verified in the file;mix.lock:L12is not a shell call site.
2. mix.lock — decimal 3.1.1 → 3.2.0
- Consumer constraints: verified directly in
mix.lock—ecto ~> 3.0,ecto_sql ~> 3.0,ecto_sqlite3 ~> 3.0, andjason's optional~> 1.0 or ~> 2.0 or ~> 3.0all permit 3.2.0.mix.exspins{:decimal, "~> 3.0", override: true}, also satisfied. No upper bound is violated. - Release existence and content: the
v3.2.0tag exists onericmj/decimal(published 2026-10-10T21:14:30Z) and its notes include multipleDecimal.round/3fixes, consistent with the advisory's subject ("Unbounded allocation inDecimal.round/3", fixed in ≥ 3.1.2 per the quoted range; 3.2.0 > 3.1.2). - Lock-line integrity: I could not independently recompute the hex.pm checksums in this lane, but this is covered by stronger evidence: CI's
Lint and Teston the head commit succeeded, and itsmix deps.getstep verifies the fetched tarball against the lock's outer checksum — a fabricated or corrupted lock line would have failed there, beforemix checkeven ran. - Behavioral changes in 3.2.0: the release notes list real behavior changes (subnormal/
:clampedsignaling,round/3now raisingDecimal.Errorin some cases,compare/2raising on NaN). These are mitigated here: per themix.exscomment, no schema or migration uses the:decimaltype, and the full test suite passed on the head commit.
Standards Compliance
- The PR does not touch Elixir source, schemas, workers, queues, or migrations, so the aliasing, module-naming, queue-name, and
@allowed_fieldsconventions are not engaged. - Trace-scope requirement req-cec06a117f01 (never assume the checkout lives at a particular path): not_applicable — the diff adds no fixtures or tests and no absolute paths; the workflow change uses env-var interpolation only. See
requirement_coverage. - CI convention compliance: the required
Lint and Testgate is green on the head commit per the authoritative CI status results. - No documented behaviour in
AGENTS.md/CODEBASE.md/DEVELOPMENT.mdis invalidated, so no doc updates are required.
Linked Issue Fit
Issue PR 145 reports Publish PR Image failing repeatedly on the default branch with exactly the --slurp option is not supported with --jq or --template in the Check for image artifact step. The PR fixes precisely that step and nothing else in the workflow, preserving the pagination/slurp intent stated in the issue's own inline comment. The second commit addresses the downstream consequence the issue's fix exposed (the required gate turning red on an external advisory), which the PR body documents with timestamps. The issue's acceptance criterion — the workflow no longer failing on this error — is directly satisfied by the diff, and the head commit's CI is green.
Tool Harness Findings
read_fileon.github/workflows/pr-image-publish.ymlreturned the full file, confirming the new pipeline form, the untouched sibling step, andset -euo pipefail.git_grepover.github/workflowsconfirmed the only--slurp/--paginate/--jq/--templateoccurrences are the two call sites in this workflow, both piping to externaljq.git_grepovermix.lockandread_fileonmix.exsconfirmed all decimal consumer constraints permit 3.2.0 and theoverride: truepin is~> 3.0.web_fetchof the decimal v3.2.0 GitHub release confirmed the release exists, its publish timestamp, and its round/3-focused fix list.- Attempts to read
cli/cli'sapi.gopaginate loop were truncated in both fetch paths, and severalgh_apicalls failed with transport errors (code search, check-runs, PR files/commits); none of these blocked the verdict, since the rejection is proven by the issue's runtime log and the lock line by CI.
Unknowns or Needs Verification
- The claim that gh's
--jqis applied per page during--paginate(Claim 1's second half) was not directly observed in the gh source fetched here; the fetched blob was truncated before the paginate loop. It is consistent with gh's documented flag behavior and is immaterial to the fix, but it remains an unverified explanatory detail in the new comment. - The hex.pm checksums in the new
mix.lockline were not independently recomputed in this review; they are validated indirectly by the green CI run, whosemix deps.getstep fails on any checksum mismatch. - The advisory EEF-CVE-2026-97853's exact text was taken from the PR body/comment (hex.pm API fetch was not reachable in this lane); the version range quoted is internally consistent with the bump chosen.
Requirement trace
1 of 5 requirement(s) not fully traced to enforcement and a test:
req-cec06a117f01— unverifiable (no valid enforcement location)
Approval withheld: this review's coverage is incomplete — required-check coverage or the tool-loop investigation did not finish, so it is publishing as an advisory comment rather than an approval.
What
Fixes #145. Two commits:
b39173a— the Publish PR Image workflow'sPush PR Image to GHCRjob failed repeatedly on the default branch. TheCheck for image artifactstep combinedgh api --paginate --slurpwith gh's own--jqflag, whichghrejects at runtime:The fix pipes the
--slurp-ed output to an externaljqprocess, keeping the intended filter byte-for-byte. This is the same pattern the siblingResolve trusted PR identitystep already uses.bedf622— unblocks the requiredLint and Testgate, which turned red on this branch through no fault of the diff:mix hex.auditbegan failing on advisory EEF-CVE-2026-97853 (decimal>= 0.1.0, < 3.1.2, "Unbounded allocation inDecimal.round/3", published 2026-10-10T19:43Z — ~25 min after main's green run at 19:16Z and ~15h before this PR's red run). Lockeddecimal3.1.1 is inside the range, so the failure is deterministic and external-advisory drift, not the workflow diff. Bumpsmix.lockdecimal3.1.1 → 3.2.0 (newest version satisfying every consumer constraint; also clears the two older< 3.0.0decimal advisories).Why the workflow fix is correct
From
gh's source (pkg/cmd/api/api.go),--jq(FilterOutput) is applied to each page individually during--paginate. So:--paginate --jq(no--slurp) would emit one count per page, not a total.--paginate --slurp+ gh's--jq— the combination that was there — is the exact casegherrors on.--paginate --slurpwraps all pages into one outer JSON array; feeding that to an externaljqproduces a single total across every page, which is what the author's original filter ([.[] | .artifacts[] | select(.name == "pr-image")] | length) was written for. The/actions/runs/{id}/artifactsendpoint returns an object per page ({total_count, artifacts:[...]}), so the filter traverses.[].artifacts[]; the sibling step counts over/pulls, which returns an array per page, hence its.[][]— the two differ on purpose.Why the decimal lock line is trustworthy without a Mix toolchain
No Elixir toolchain exists in this lane, so the lock line was derived and cryptographically verified rather than generated by
mix deps.update:190bca80…is hex.pm's authoritative release checksum (/api/packages/decimal/releases/3.2.0).f1235f05…is the canonical inner checksum recomputed fromdecimal-3.2.0.tarperhexpm/hexmix_hex_tarball.erl(sha256 overVERSION++metadata.config++contents.tar.gz), matching the tarball's ownCHECKSUMmember.decimal3.1.1 andbunt1.0.0 entries, and independently recomputed by the review pass from fresh tarball fetches.metadata.configconfirmsbuild_tools [mix]and empty requirements → lock fields[:mix],[].ecto/ecto_sql/ecto_sqlite3require~> 3.0;jason's optional~> 1.0 or ~> 2.0 or ~> 3.0— all permit 3.2.0; no upper bound is violated.Invariants kept, and the other paths that enforce each
--paginate --slurpwith gh's own--jq/--template— aggregate in an externaljq. Enforced by this step and byResolve trusted PR identity(~line 103). Kept: the sibling is already correct and is left untouched; a grep of.github/workflows/confirms these are the only--slurpuses and no--paginategh call anywhere combines--slurpwith--jq/--template.pr-imageacross every page, not just the first 30. Kept:--paginate --slurpstill wraps every page in an outer array and the externaljqreduces over all page-objects, socountis a single overall integer (verified with a multi-page / no-match / empty simulation →1/0/0, matching theexists=true/exists=falsebranches).gh/jqaborts the step rather than silently reportingexists=false. Kept: the step runs underset -euo pipefail; withpipefaila failedghin the pipeline exits non-zero andset -estops the step. A genuine no-match emits0(never empty output), so only a real zero count reachesexists=false.runblocks only via env vars with quoted expansions. Kept: the changed line interpolates only${REPOSITORY}/${RUN_ID}(trustedgithub.repository/github.event.workflow_run.idrun context) into the URL argument, double-quoted; thejqprogram is a single-quoted literal; artifact JSON enters only via stdin and leaves only as an integer. The gating script (tooling/pr_image_metadata.sh) and the download-to-$RUNNER_TEMPisolation are untouched.steps.check.outputs.existsstill gates every later step; the trusted-identity resolution, metadata validation, push, and PR comment are all unchanged.mix hex.auditpasses with only the two documented Cowlib exceptions (HEX_IGNORE_ADVISORIES=GHSA-w4f7-4cxr-rv3c,GHSA-g2wm-735q-3f56intooling/.check.exs; the rationale for that exception lives there). Kept: the decimal bump moves to 3.2.0, outside EEF-CVE-2026-97853 (< 3.1.2) and the two older< 3.0.0decimal advisories; no new ignore entries were added. The only othermix.lock-wide audit path ismix deps.audit(Mix advisory DB), which does not yet carry the decimal advisory; the bump satisfies both regardless.mix.lockstays exactly whatmix deps.update decimalwould produce (single-line change, canonical checksums,[:mix]/[]frommetadata.config). Kept: verified as described above; no other dependency line moved.jqis preinstalled onubuntu-latest, so the external binary is available where the workflow runs.Local validation run
yarn run lint:check(Prettier — the CI gate that covers.github/workflows/*.yml): exit 0 on headbedf622.bash -non the extractedCheck for image artifactscript: syntax OK.bedf622: verdict SHIP, no findings.mix check/mix test/mix hex.audit— no Elixir, Erlang, or Docker in any environment available here. CI is the first real execution ofmix deps.getagainst the new lock line; that verification is pending, and I am not claiming CI green.Still to verify externally
Lint and Testonbedf622: expected green (decimal leaves the advisory range; everything else is unchanged from main's greene9b39a6), pending CI execution —deps.getchecksum validation of the constructed lock line is the one thing only Mix can confirm.workflow_runand executes only on the default branch, so the fixed step cannot run from a PR branch. The greenPush PR Image to GHCRrun is confirmed after merge.Head:
bedf622