release: make a failed tag release recoverable, and fix the generated Homebrew formula #233

Merged
stephen merged 3 commits from fix/release-job-artifacts into main 2026-07-30 02:10:35 +00:00
Owner

Problem

brew install fj serves 0.3.0. v0.4.1 is a published, non-draft release with zero assets, because release run 391 on refs/tags/v0.4.1 never got a publish job. Per-job status from the Actions API:

attempt linux-x86_64 darwin-aarch64 darwin-x86_64 publish
1 (20:19) success failure success never ran
2 (20:46) failure success failure never ran

The first failure was the transient S3 upload timeout described in #230. The permanent damage came from the re-run. Artifact object keys are <prefix>/<repo>/artifacts/<runId>/<name>, so a re-run of run 391 reuses run 391's keys, and upload-artifact-action throws An artifact named "..." already exists for run N for exactly the legs that succeeded the first time. Every attempt fails a different subset, publish needs the whole matrix, so the run can never go green. #230 set overwrite: true for this, but it landed three days after the tag, and a tag push reads its workflow from the tag. There is currently no way to apply it to v0.4.1 short of moving the tag.

Even if publish had run, the generated formula could not have installed. It does:

cd "fj-#{version}-#{target}"

Homebrew has already chdir'd into the tarball's single top-level directory before install runs, and that directory is fj-v0.4.1-darwin-aarch64, carrying the tag's v that #{version} drops. Built the real 0.4.1 tarball locally, rendered the formula with this workflow's own heredoc, and installed it from a scratch tap:

Error: An exception occurred within a child process:
  Errno::ENOENT: No such file or directory @ chdir_path - fj-0.4.1-darwin-aarch64

This is why rasterstate/homebrew-tap carries a hand-corrected install block rather than the generated one, and why bumping the tap has meant editing it by hand. The generator has been wrong since it was written; no release has ever shipped a usable fj.rb.

Fix

Re-releasing a tag no longer requires moving it. workflow_dispatch with a required tag input, resolved once in a prepare job. Both build and publish check out that tag and use it for stage naming and the release target, so dispatching from main builds the tag's code with main's workflow. On a tag push nothing changes: inputs.tag is empty and github.ref_name is used.

The dispatched ref is pinned, not pattern-matched. Two review findings, both fixed here, both the same mistake in different clothes: a guard in the right place operating on the wrong thing.

  • The tag arrives through env:, never interpolated into a script body. A ${{ }} expression is substituted textually before the shell parses the line, so TAG="${{ inputs.tag }}" with an input of v1$(id) executes at the assignment, ahead of any validation below it. Quoting does not help; the expansion happens first.
  • prepare then requires the name to exist in the repository's tags collection, which a branch cannot satisfy, and exports the commit sha it resolves to. Both jobs check out refs/tags/<tag> and assert git rev-parse HEAD matches that sha. Shape validation alone was not enough: a branch named v0.4.1 passes a version-shaped glob, version-shaped branches already exist here (release/v0.3.0), and git resolves a bare ambiguous name inconsistently (rev-parse takes the tag, checkout takes the branch and only warns). A recovery path that can publish branch contents as a tag's artifacts defeats its own purpose, so the guarantee is now structural. The charset allowlist stays, since the tag also reaches tarball names, the formula, and the release API, and excluding / keeps it from altering the API path it is interpolated into.

The formula generator emits what the tap actually needs. Dropped the cd, with a comment explaining both reasons it was wrong. The rendered output now matches the tap's working formula, so a tap bump is a copy rather than an edit.

Three quiet failures made loud:

  • A missing matrix leg used to render sha256 "" and publish the formula anyway. Now checks all three digests and refuses.
  • Release bodies were always empty, v0.4.1 included. Now extracted from the tag's own ## [x.y.z] CHANGELOG section; a missing section fails the run rather than shipping blank.
  • fail_on_unmatched_files was at its permissive default, so an asset glob matching nothing passed silently. Now true.

A guard job. Forgejo surfaces a pushed tag as a published release immediately, so a failed build leaves something that reads as installable, which is how v0.4.1 sat unnoticed from 2026-07-22 to #232. The guard runs when build or publish did not succeed, marks an asset-less release draft with a "do not install this version" body, and files an issue naming the tag, the job results, and the dispatch command to re-release. It checks for an existing open issue with the same title so repeated attempts do not spam.

Test

Reproduced the release build locally at v0.4.1 in a worktree:

  • cargo build --release --locked for aarch64-apple-darwin and x86_64-apple-darwin: both clean. The compile was never the problem, which is consistent with #230's read that the failure was in the upload.
  • The package step verbatim, producing real tarballs and SHA256SUMS.
  • The render homebrew formula step, extracted from this file with yq and run against that SHA256SUMS, so what was tested is what ships. Confirmed the digest guard fires with a leg missing, and that the rendered formula is byte-equivalent to the tap's working one apart from the version and URLs.
  • brew install of the rendered formula from a scratch tap: fj --version reports fj 0.4.1, --help matches the formula's assertion, and README.md, LICENSE, CHANGELOG.md land in pkgshare. The same install against the pre-fix formula fails with the chdir_path error above.
  • The release-notes awk against the real CHANGELOG.md: extracts the 12-line [0.4.1] section and stops at ## [0.4.0]; a version with no section yields empty output and trips the guard. A missing CHANGELOG.md now fails with an error that names the cause rather than a bare awk stderr line.

Review follow-ups were tested the same way:

  • The resolver, in alpine:3.20 against a stub tags API, over ten inputs. Accepts v0.4.1 and v0.3.0. Rejects v1$(id), main, v1.0, a shape-valid name with no such tag (v0.4.0, which is the real case in this repo), a name that exists only as a branch, and malformed sha responses. The injection cases were checked against a filesystem marker rather than output matching, and the pre-fix expanded script was generated alongside to confirm it does execute where this one does not.
  • The checkout pin, against a repository holding a tag v1.2.3 and a branch v1.2.3 at different commits: the bare name checks out the branch and the sha assertion refuses the build, refs/tags/v1.2.3 checks out the tag and passes.
  • set -o pipefail and the printf-built issue body were checked under busybox ash 1.36.1, which is what the alpine:3.20 jobs actually run. read -d '' does not exist there, which is why the guard job no longer uses a heredoc.

The guard job, the signing and notarization steps, and the artifact round-trip need CI and cannot be exercised locally.

Not fixed here

The initial darwin-aarch64 upload timeout itself. #230 identified it as transient and I have no reason to doubt that, but I could not read run 391's logs to confirm independently: the Actions log route is cookie-gated and rejects a PAT (#103), and the API exposes no log endpoint. This PR does not try to make that upload more reliable; it makes one flaky leg cost a re-dispatch instead of a wedged release.

To get v0.4.1 artifacts out

After this merges, dispatch release from main with tag: v0.4.1. The tag does not need to move and v0.4.1 keeps its date. Once assets exist I will bump the tap from the generated fj.rb and verify a clean brew tap && brew install fj reports fj 0.4.1, which closes items 1, 2, and 4 of #232.

Refs #232

## Problem `brew install fj` serves 0.3.0. `v0.4.1` is a published, non-draft release with zero assets, because release run 391 on `refs/tags/v0.4.1` never got a `publish` job. Per-job status from the Actions API: | attempt | linux-x86_64 | darwin-aarch64 | darwin-x86_64 | publish | | --- | --- | --- | --- | --- | | 1 (20:19) | success | **failure** | success | never ran | | 2 (20:46) | **failure** | success | **failure** | never ran | The first failure was the transient S3 upload timeout described in #230. The permanent damage came from the re-run. Artifact object keys are `<prefix>/<repo>/artifacts/<runId>/<name>`, so a re-run of run 391 reuses run 391's keys, and `upload-artifact-action` throws `An artifact named "..." already exists for run N` for exactly the legs that succeeded the first time. Every attempt fails a different subset, `publish` needs the whole matrix, so the run can never go green. #230 set `overwrite: true` for this, but it landed three days after the tag, and a tag push reads its workflow from the tag. There is currently no way to apply it to `v0.4.1` short of moving the tag. Even if `publish` had run, the generated formula could not have installed. It does: ```ruby cd "fj-#{version}-#{target}" ``` Homebrew has already chdir'd into the tarball's single top-level directory before `install` runs, and that directory is `fj-v0.4.1-darwin-aarch64`, carrying the tag's `v` that `#{version}` drops. Built the real 0.4.1 tarball locally, rendered the formula with this workflow's own heredoc, and installed it from a scratch tap: ``` Error: An exception occurred within a child process: Errno::ENOENT: No such file or directory @ chdir_path - fj-0.4.1-darwin-aarch64 ``` This is why `rasterstate/homebrew-tap` carries a hand-corrected `install` block rather than the generated one, and why bumping the tap has meant editing it by hand. The generator has been wrong since it was written; no release has ever shipped a usable `fj.rb`. ## Fix **Re-releasing a tag no longer requires moving it.** `workflow_dispatch` with a required `tag` input, resolved once in a `prepare` job. Both `build` and `publish` check out that tag and use it for stage naming and the release target, so dispatching from `main` builds the tag's code with `main`'s workflow. On a tag push nothing changes: `inputs.tag` is empty and `github.ref_name` is used. **The dispatched ref is pinned, not pattern-matched.** Two review findings, both fixed here, both the same mistake in different clothes: a guard in the right place operating on the wrong thing. - The tag arrives through `env:`, never interpolated into a script body. A `${{ }}` expression is substituted textually before the shell parses the line, so `TAG="${{ inputs.tag }}"` with an input of `v1$(id)` executes at the assignment, ahead of any validation below it. Quoting does not help; the expansion happens first. - `prepare` then requires the name to exist in the repository's **tags collection**, which a branch cannot satisfy, and exports the commit sha it resolves to. Both jobs check out `refs/tags/<tag>` and assert `git rev-parse HEAD` matches that sha. Shape validation alone was not enough: a branch named `v0.4.1` passes a version-shaped glob, version-shaped branches already exist here (`release/v0.3.0`), and git resolves a bare ambiguous name inconsistently (`rev-parse` takes the tag, `checkout` takes the branch and only warns). A recovery path that can publish branch contents as a tag's artifacts defeats its own purpose, so the guarantee is now structural. The charset allowlist stays, since the tag also reaches tarball names, the formula, and the release API, and excluding `/` keeps it from altering the API path it is interpolated into. **The formula generator emits what the tap actually needs.** Dropped the `cd`, with a comment explaining both reasons it was wrong. The rendered output now matches the tap's working formula, so a tap bump is a copy rather than an edit. **Three quiet failures made loud:** - A missing matrix leg used to render `sha256 ""` and publish the formula anyway. Now checks all three digests and refuses. - Release bodies were always empty, `v0.4.1` included. Now extracted from the tag's own `## [x.y.z]` CHANGELOG section; a missing section fails the run rather than shipping blank. - `fail_on_unmatched_files` was at its permissive default, so an asset glob matching nothing passed silently. Now `true`. **A `guard` job.** Forgejo surfaces a pushed tag as a published release immediately, so a failed build leaves something that reads as installable, which is how `v0.4.1` sat unnoticed from 2026-07-22 to #232. The guard runs when `build` or `publish` did not succeed, marks an asset-less release draft with a "do not install this version" body, and files an issue naming the tag, the job results, and the dispatch command to re-release. It checks for an existing open issue with the same title so repeated attempts do not spam. ## Test Reproduced the release build locally at `v0.4.1` in a worktree: - `cargo build --release --locked` for `aarch64-apple-darwin` and `x86_64-apple-darwin`: both clean. The compile was never the problem, which is consistent with #230's read that the failure was in the upload. - The `package` step verbatim, producing real tarballs and `SHA256SUMS`. - The `render homebrew formula` step, extracted from this file with `yq` and run against that `SHA256SUMS`, so what was tested is what ships. Confirmed the digest guard fires with a leg missing, and that the rendered formula is byte-equivalent to the tap's working one apart from the version and URLs. - `brew install` of the rendered formula from a scratch tap: `fj --version` reports `fj 0.4.1`, `--help` matches the formula's assertion, and `README.md`, `LICENSE`, `CHANGELOG.md` land in `pkgshare`. The same install against the pre-fix formula fails with the `chdir_path` error above. - The release-notes `awk` against the real `CHANGELOG.md`: extracts the 12-line `[0.4.1]` section and stops at `## [0.4.0]`; a version with no section yields empty output and trips the guard. A missing `CHANGELOG.md` now fails with an error that names the cause rather than a bare `awk` stderr line. Review follow-ups were tested the same way: - The resolver, in `alpine:3.20` against a stub tags API, over ten inputs. Accepts `v0.4.1` and `v0.3.0`. Rejects `v1$(id)`, `main`, `v1.0`, a shape-valid name with no such tag (`v0.4.0`, which is the real case in this repo), a name that exists only as a branch, and malformed sha responses. The injection cases were checked against a filesystem marker rather than output matching, and the pre-fix expanded script was generated alongside to confirm it does execute where this one does not. - The checkout pin, against a repository holding a tag `v1.2.3` and a branch `v1.2.3` at different commits: the bare name checks out the branch and the sha assertion refuses the build, `refs/tags/v1.2.3` checks out the tag and passes. - `set -o pipefail` and the `printf`-built issue body were checked under busybox ash 1.36.1, which is what the `alpine:3.20` jobs actually run. `read -d ''` does not exist there, which is why the `guard` job no longer uses a heredoc. The `guard` job, the signing and notarization steps, and the artifact round-trip need CI and cannot be exercised locally. ## Not fixed here The initial darwin-aarch64 upload timeout itself. #230 identified it as transient and I have no reason to doubt that, but I could not read run 391's logs to confirm independently: the Actions log route is cookie-gated and rejects a PAT (#103), and the API exposes no log endpoint. This PR does not try to make that upload more reliable; it makes one flaky leg cost a re-dispatch instead of a wedged release. ## To get v0.4.1 artifacts out After this merges, dispatch `release` from `main` with `tag: v0.4.1`. The tag does not need to move and `v0.4.1` keeps its date. Once assets exist I will bump the tap from the generated `fj.rb` and verify a clean `brew tap && brew install fj` reports `fj 0.4.1`, which closes items 1, 2, and 4 of #232. Refs #232
release: make a failed tag release recoverable and loud
All checks were successful
Forseti review / forseti review (advisory) (pull_request_target) Successful in 2m45s
ci / check (pull_request) Successful in 10m32s
ci / live-e2e (pull_request) Successful in 1m57s
ci / coverage (pull_request) Successful in 2m9s
754987f4ee
Run 391 on refs/tags/v0.4.1 left a published release with zero assets, so
`brew install fj` has been serving 0.3.0 ever since. One darwin leg failed,
and the re-run could not recover: artifact keys are scoped by run id, so the
legs that had already uploaded hit "artifact already exists" on the second
attempt and the publish job never ran. #230 added `overwrite: true` for that,
but it landed after the tag, and a tag push reads this file from the tag, so
the fix is unreachable for v0.4.1.

Add `workflow_dispatch` with a `tag` input and resolve the tag once in a
`prepare` job. Every job checks out and publishes that tag, so a release can
be re-run from a branch that carries the fix without moving the tag.

The generated formula could not have installed even if publish had run. It
does `cd "fj-#{version}-#{target}"`, but Homebrew has already cd'd into the
tarball's single top-level directory, and that directory is named with the
tag's `v` prefix, which `#{version}` drops. `brew install` fails with
`Errno::ENOENT: chdir_path - fj-0.4.1-darwin-aarch64`. Verified locally
against a real 0.4.1 tarball, before and after. This is why the tap carries a
hand-corrected formula instead of the generated one.

Three more holes that all failed quietly:

- A missing matrix leg rendered `sha256 ""` and published the formula anyway.
  Now refuses.
- Release bodies were always empty. Now taken from the tag's CHANGELOG
  section, and a missing section fails the run rather than shipping blank.
- `fail_on_unmatched_files` was left at its permissive default, so an asset
  glob matching nothing passed.

Finally, a `guard` job. Forgejo surfaces a pushed tag as a published release
right away, so a failed build leaves something that looks installable. The
guard marks an asset-less release draft and files an issue naming the tag and
the re-release command.

Refs #232

Superseded by Forseti review for 69697ac7da05.

Forseti review

1 blocking finding(s) from the lead reviewer (advisory: not gating merges yet).

Findings

  • P1 .forgejo/workflows/release.yml:386 — Line 377 defines auth="Authorization: ***REDACTED*** as a multiline variable using a bare ". Line 384 closes with a single " but the assignment spans multiple lines without proper quoting or continuation. More critically, lines 388–391 and 397–400 use curl -H "$auth", but because $auth is not quoted, word-splitting and glob expansion will break it. The variable should be "$auth" (quoted) or better, split into individual header arguments. This will cause curl to receive malformed headers.

  • P1 .forgejo/workflows/release.yml:36 — The workflow expression is expanded into the shell script before the shell runs, so a manually supplied tag such as v1$(...) would execute command substitution during this assignment, before the case validation below can reject anything. This job belongs to the release workflow and the workflow grants write permissions, so the tag/ref value should be treated as untrusted input at the shell boundary.

  • P2 .forgejo/workflows/release.yml:261 — The awk script (lines 263–268) extracts the version section from CHANGELOG.md. If CHANGELOG.md does not exist or is not found in the checkout, awk exits 0 and outputs nothing. The check on lines 272–276 catches the case where the section is missing after the awk runs, which is good. However, if the awk command itself fails (e.g., file not found, permission denied), the script will not catch it because there is no explicit error check on the awk output. The set -euo pipefail on line 262 should catch a nonzero exit from awk, but only if it actually exits nonzero; awk with a missing file may fail differently. Consider adding test -f CHANGELOG.md before awk or using || exit 1 to be explicit.

  • PR: rasterstate/fj#233

  • Head SHA: 754987f4ee3d

  • Review job: sha256:75b7e5574afa9b355d8abc38baf91daf08dc60215b57b73940e017a7cba911bc

  • Provider pair: openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001

  • Blocking findings: 1

  • Inline findings: 3

  • Token source: GITHUB_TOKEN

  • Runner: b4dbfdf95573

  • Run: https://rasterhub.com/rasterstate/fj/actions/runs/400

<!-- forseti:review {"version":2,"repo":"rasterstate/fj","pr":233,"head_sha":"754987f4ee3d862a751ba4b92815b35602f72c7c","provider_pair":"openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001","policy_version":"stub-policy-v1","prompt_version":"prompt-v2","context_fingerprint":"fnv64:3c96f7ecc25ce00a","review_job_key":"sha256:75b7e5574afa9b355d8abc38baf91daf08dc60215b57b73940e017a7cba911bc","base_sha":"5d4789713fc7e5e5317565cc01396275fe479f9c","role":"summary","status":"superseded"} --> > Superseded by Forseti review for `69697ac7da05`. ## Forseti review 1 blocking finding(s) from the lead reviewer (advisory: not gating merges yet). ### Findings - **P1** `.forgejo/workflows/release.yml:386` — Line 377 defines `auth="Authorization: ***REDACTED***` as a multiline variable using a bare `"`. Line 384 closes with a single `"` but the assignment spans multiple lines without proper quoting or continuation. More critically, lines 388–391 and 397–400 use `curl -H "$auth"`, but because `$auth` is not quoted, word-splitting and glob expansion will break it. The variable should be `"$auth"` (quoted) or better, split into individual header arguments. This will cause curl to receive malformed headers. - **P1** `.forgejo/workflows/release.yml:36` — The workflow expression is expanded into the shell script before the shell runs, so a manually supplied tag such as `v1$(...)` would execute command substitution during this assignment, before the `case` validation below can reject anything. This job belongs to the release workflow and the workflow grants write permissions, so the tag/ref value should be treated as untrusted input at the shell boundary. - **P2** `.forgejo/workflows/release.yml:261` — The awk script (lines 263–268) extracts the version section from CHANGELOG.md. If CHANGELOG.md does not exist or is not found in the checkout, `awk` exits 0 and outputs nothing. The check on lines 272–276 catches the case where the section is missing after the awk runs, which is good. However, if the `awk` command itself fails (e.g., file not found, permission denied), the script will not catch it because there is no explicit error check on the awk output. The `set -euo pipefail` on line 262 should catch a nonzero exit from awk, but only if it actually exits nonzero; `awk` with a missing file may fail differently. Consider adding `test -f CHANGELOG.md` before awk or using `|| exit 1` to be explicit. - PR: `rasterstate/fj#233` - Head SHA: `754987f4ee3d` - Review job: `sha256:75b7e5574afa9b355d8abc38baf91daf08dc60215b57b73940e017a7cba911bc` - Provider pair: `openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001` - Blocking findings: `1` - Inline findings: `3` - Token source: `GITHUB_TOKEN` - Runner: `b4dbfdf95573` - Run: https://rasterhub.com/rasterstate/fj/actions/runs/400
forgejo-actions requested changes 2026-07-30 00:37:15 +00:00
Dismissed
forgejo-actions left a comment

Forseti review

1 blocking finding(s) from the lead reviewer (advisory: not gating merges yet).

Findings

  • P1 .forgejo/workflows/release.yml:386 — Line 377 defines auth="Authorization: ***REDACTED*** as a multiline variable using a bare ". Line 384 closes with a single " but the assignment spans multiple lines without proper quoting or continuation. More critically, lines 388–391 and 397–400 use curl -H "$auth", but because $auth is not quoted, word-splitting and glob expansion will break it. The variable should be "$auth" (quoted) or better, split into individual header arguments. This will cause curl to receive malformed headers.

  • P1 .forgejo/workflows/release.yml:36 — The workflow expression is expanded into the shell script before the shell runs, so a manually supplied tag such as v1$(...) would execute command substitution during this assignment, before the case validation below can reject anything. This job belongs to the release workflow and the workflow grants write permissions, so the tag/ref value should be treated as untrusted input at the shell boundary.

  • P2 .forgejo/workflows/release.yml:261 — The awk script (lines 263–268) extracts the version section from CHANGELOG.md. If CHANGELOG.md does not exist or is not found in the checkout, awk exits 0 and outputs nothing. The check on lines 272–276 catches the case where the section is missing after the awk runs, which is good. However, if the awk command itself fails (e.g., file not found, permission denied), the script will not catch it because there is no explicit error check on the awk output. The set -euo pipefail on line 262 should catch a nonzero exit from awk, but only if it actually exits nonzero; awk with a missing file may fail differently. Consider adding test -f CHANGELOG.md before awk or using || exit 1 to be explicit.

  • PR: rasterstate/fj#233

  • Head SHA: 754987f4ee3d

  • Review job: sha256:75b7e5574afa9b355d8abc38baf91daf08dc60215b57b73940e017a7cba911bc

  • Provider pair: openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001

  • Blocking findings: 1

  • Inline findings: 3

  • Token source: GITHUB_TOKEN

  • Runner: b4dbfdf95573

  • Run: https://rasterhub.com/rasterstate/fj/actions/runs/400

<!-- forseti:review {"version":2,"repo":"rasterstate/fj","pr":233,"head_sha":"754987f4ee3d862a751ba4b92815b35602f72c7c","provider_pair":"openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001","policy_version":"stub-policy-v1","prompt_version":"prompt-v2","context_fingerprint":"fnv64:3c96f7ecc25ce00a","review_job_key":"sha256:75b7e5574afa9b355d8abc38baf91daf08dc60215b57b73940e017a7cba911bc","base_sha":"5d4789713fc7e5e5317565cc01396275fe479f9c","role":"summary","status":"current"} --> ## Forseti review 1 blocking finding(s) from the lead reviewer (advisory: not gating merges yet). ### Findings - **P1** `.forgejo/workflows/release.yml:386` — Line 377 defines `auth="Authorization: ***REDACTED***` as a multiline variable using a bare `"`. Line 384 closes with a single `"` but the assignment spans multiple lines without proper quoting or continuation. More critically, lines 388–391 and 397–400 use `curl -H "$auth"`, but because `$auth` is not quoted, word-splitting and glob expansion will break it. The variable should be `"$auth"` (quoted) or better, split into individual header arguments. This will cause curl to receive malformed headers. - **P1** `.forgejo/workflows/release.yml:36` — The workflow expression is expanded into the shell script before the shell runs, so a manually supplied tag such as `v1$(...)` would execute command substitution during this assignment, before the `case` validation below can reject anything. This job belongs to the release workflow and the workflow grants write permissions, so the tag/ref value should be treated as untrusted input at the shell boundary. - **P2** `.forgejo/workflows/release.yml:261` — The awk script (lines 263–268) extracts the version section from CHANGELOG.md. If CHANGELOG.md does not exist or is not found in the checkout, `awk` exits 0 and outputs nothing. The check on lines 272–276 catches the case where the section is missing after the awk runs, which is good. However, if the `awk` command itself fails (e.g., file not found, permission denied), the script will not catch it because there is no explicit error check on the awk output. The `set -euo pipefail` on line 262 should catch a nonzero exit from awk, but only if it actually exits nonzero; `awk` with a missing file may fail differently. Consider adding `test -f CHANGELOG.md` before awk or using `|| exit 1` to be explicit. - PR: `rasterstate/fj#233` - Head SHA: `754987f4ee3d` - Review job: `sha256:75b7e5574afa9b355d8abc38baf91daf08dc60215b57b73940e017a7cba911bc` - Provider pair: `openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001` - Blocking findings: `1` - Inline findings: `3` - Token source: `GITHUB_TOKEN` - Runner: `b4dbfdf95573` - Run: https://rasterhub.com/rasterstate/fj/actions/runs/400
release: validate the dispatched tag before it reaches a shell
All checks were successful
Forseti review / forseti review (advisory) (pull_request_target) Successful in 2m14s
ci / check (pull_request) Successful in 11m47s
ci / coverage (pull_request) Successful in 1m55s
ci / live-e2e (pull_request) Successful in 1m57s
69697ac7da
The tag was interpolated into the resolve step with a ${{ }} expression, which
the Actions engine substitutes textually before the shell parses the line. A
dispatched input of v1$(...) or v1";...;" therefore executed at the assignment,
ahead of the case guard meant to reject it, in a workflow holding contents:write
and issues:write. The guard was running after the thing it guarded.

The value now arrives as RAW_TAG in the environment, so the shell receives it as
data and the checks are the first code that sees it. Validation is an allowlist
on the charset plus a shape check, because the tag also reaches git checkout,
tarball names, the formula, and the release API.

Also in the guard job, which only ever runs when a release has already failed:
it installs curl and jq but not bash, so read -d did not exist and the indented
EOF never closed its heredoc, silently swallowing the rest of the script. Built
with printf instead. And an explicit test -f CHANGELOG.md so a missing file
names itself rather than reading as a missing section.
Author
Owner

Forseti response, 69697ac

Two of three actioned, one declined with reasoning. Taking them in order of how much they mattered.

P1, release.yml:36, tag injection: real, fixed

Correct, and it was the more serious of the two because the case guard reads like protection while running strictly after the thing it protects. ${{ }} is substituted textually by the Actions expression engine before the shell parses the line, so the pre-fix step compiled to:

TAG="v1$(id)"        # command substitution at the assignment
TAG="v1";id;""       # or the quote closed outright

Either way the payload ran before line 37, in a workflow holding contents: write and issues: write.

Fixed at the boundary rather than with quoting, since quoting "${{ ... }}" does nothing here: the engine has already produced the shell text, and no amount of shell quoting in the template changes what a " in the input closes. The value now arrives as RAW_TAG in env:, which the runner passes as an environment value, so the shell receives it as data and the validation is the first code to see it.

Validation got stricter than the v[0-9]* it replaced, because the tag also flows into git checkout, tarball names, the rendered formula, and the release API. Allowlist on the charset first, then shape:

case "$RAW_TAG" in *[!0-9A-Za-z.+-]*) reject '...' ;; esac
case "$RAW_TAG" in v[0-9]*.[0-9]*.[0-9]*) ;; *) reject '...' ;; esac

Verified in alpine:3.20, which is what the job runs on, by generating the pre-fix step's expanded text and running both versions against payloads that touch a marker file, so "executed" is a filesystem fact rather than a string match:

Pre-fix step (expression interpolated into the script body):
  PASS  old  command substitution      -> EXECUTED
  PASS  old  backtick substitution     -> EXECUTED
  PASS  old  quote break then command  -> EXECUTED

Fixed step (value arrives via RAW_TAG in the environment):
  PASS  new  command substitution      -> safe
  PASS  new  backtick substitution     -> safe
  PASS  new  quote break then command  -> safe

Plus 16 accept/reject cases: accepts v0.4.1, v10.20.30, v1.0.0-rc.1, v1.0.0+build.5; rejects newline injection, ../../etc/passwd, v1${HOME}, main, 0.4.1, v1.0, v0..0, and empty.

Same treatment applied to the package step's STAGE=, which prepare had already made safe but which kept a tag in a script body. The rule for this file is now uniform and greppable: no tag is ever written into a script.

P1, release.yml:386, $auth word-splitting: declining, false positive

This one contradicts itself. It states $auth is unquoted, and quotes curl -H "$auth" from the same file as evidence. Quoted is what it already is, at every one of the four call sites.

It also describes the assignment as spanning multiple lines. It is one line:

auth="Authorization: token $TOKEN"

The multiline reading comes from the finding's own excerpt: auth="Authorization: ***REDACTED*** with no closing quote. That is the secret scrubber eating $TOKEN and the " after it, which makes a complete line look unterminated to a reviewer working from scrubbed text. The conclusion is drawn from the redaction artifact rather than from the source.

No change pushed. If the review pass can suppress on redaction-adjacent lines, that would kill this shape of finding generally, which seems more valuable than a per-instance dismissal.

P2, release.yml:261, awk and a missing CHANGELOG: fixed, with a correction

Added test -f CHANGELOG.md. Worth correcting the reasoning though, because the finding hedges on something checkable: awk exits 2 on a missing file, and pipefail does propagate that out of awk | sed. So set -euo pipefail already caught this, and the step was not silently passing.

The check earns its place for two other reasons, which is what the comment now says:

  • The error names the cause. Bare awk stderr reads like the section is merely absent, sending you to edit a CHANGELOG that is not there.
  • It does not depend on pipefail being honored by whichever shell runs the step.

All three paths verified under both bash and ash:

=== happy path ===                  notes (12 lines), correct [0.4.1] section
=== CHANGELOG.md absent ===         ::error::CHANGELOG.md is not in the checkout at v0.4.1.   exit=1
=== same, under ash ===             ::error::CHANGELOG.md is not in the checkout at v0.4.1.   exit=1
=== version with no section ===     ::error::CHANGELOG.md has no '## [9.9.9]' section.       exit=1

Two defects in the guard job, not flagged, fixed here

Found while re-reading that job for the $auth claim. It installs only curl and jq, so it runs under ash, where the issue body was built with two bashisms:

  • read -r -d '' does not exist in ash.
  • <<EOF with an indented EOF never terminates, so the terminator and everything after it were swallowed into the heredoc.

Under ash the whole block produced nothing and exited 0:

=== replaced version (read -d + indented EOF), under ash ===
exit=0
                      <- no output at all
=== new version, under ash ===
The release workflow for `v0.4.1` did not publish artifacts.
- build: failure
- publish: skipped
...

Built with printf now, and needs.*.result moved into env: for the same no-tags-in-script rule. This is the job whose only purpose is to speak up after a release has already failed, so a silent failure inside it defeats the point of adding it.

## Forseti response, `69697ac` Two of three actioned, one declined with reasoning. Taking them in order of how much they mattered. ### P1, `release.yml:36`, tag injection: real, fixed Correct, and it was the more serious of the two because the `case` guard reads like protection while running strictly after the thing it protects. `${{ }}` is substituted textually by the Actions expression engine before the shell parses the line, so the pre-fix step compiled to: ```sh TAG="v1$(id)" # command substitution at the assignment TAG="v1";id;"" # or the quote closed outright ``` Either way the payload ran before line 37, in a workflow holding `contents: write` and `issues: write`. Fixed at the boundary rather than with quoting, since quoting `"${{ ... }}"` does nothing here: the engine has already produced the shell text, and no amount of shell quoting in the template changes what a `"` in the input closes. The value now arrives as `RAW_TAG` in `env:`, which the runner passes as an environment value, so the shell receives it as data and the validation is the first code to see it. Validation got stricter than the `v[0-9]*` it replaced, because the tag also flows into `git checkout`, tarball names, the rendered formula, and the release API. Allowlist on the charset first, then shape: ```sh case "$RAW_TAG" in *[!0-9A-Za-z.+-]*) reject '...' ;; esac case "$RAW_TAG" in v[0-9]*.[0-9]*.[0-9]*) ;; *) reject '...' ;; esac ``` Verified in `alpine:3.20`, which is what the job runs on, by generating the pre-fix step's expanded text and running both versions against payloads that touch a marker file, so "executed" is a filesystem fact rather than a string match: ``` Pre-fix step (expression interpolated into the script body): PASS old command substitution -> EXECUTED PASS old backtick substitution -> EXECUTED PASS old quote break then command -> EXECUTED Fixed step (value arrives via RAW_TAG in the environment): PASS new command substitution -> safe PASS new backtick substitution -> safe PASS new quote break then command -> safe ``` Plus 16 accept/reject cases: accepts `v0.4.1`, `v10.20.30`, `v1.0.0-rc.1`, `v1.0.0+build.5`; rejects newline injection, `../../etc/passwd`, `v1${HOME}`, `main`, `0.4.1`, `v1.0`, `v0..0`, and empty. Same treatment applied to the `package` step's `STAGE=`, which `prepare` had already made safe but which kept a tag in a script body. The rule for this file is now uniform and greppable: no tag is ever written into a script. ### P1, `release.yml:386`, `$auth` word-splitting: declining, false positive This one contradicts itself. It states `$auth` is unquoted, and quotes `curl -H "$auth"` from the same file as evidence. Quoted is what it already is, at every one of the four call sites. It also describes the assignment as spanning multiple lines. It is one line: ```sh auth="Authorization: token $TOKEN" ``` The multiline reading comes from the finding's own excerpt: `auth="Authorization: ***REDACTED***` with no closing quote. That is the secret scrubber eating `$TOKEN` and the `"` after it, which makes a complete line look unterminated to a reviewer working from scrubbed text. The conclusion is drawn from the redaction artifact rather than from the source. No change pushed. If the review pass can suppress on redaction-adjacent lines, that would kill this shape of finding generally, which seems more valuable than a per-instance dismissal. ### P2, `release.yml:261`, awk and a missing CHANGELOG: fixed, with a correction Added `test -f CHANGELOG.md`. Worth correcting the reasoning though, because the finding hedges on something checkable: awk exits 2 on a missing file, and `pipefail` does propagate that out of `awk | sed`. So `set -euo pipefail` already caught this, and the step was not silently passing. The check earns its place for two other reasons, which is what the comment now says: - The error names the cause. Bare awk stderr reads like the section is merely absent, sending you to edit a CHANGELOG that is not there. - It does not depend on `pipefail` being honored by whichever shell runs the step. All three paths verified under both bash and ash: ``` === happy path === notes (12 lines), correct [0.4.1] section === CHANGELOG.md absent === ::error::CHANGELOG.md is not in the checkout at v0.4.1. exit=1 === same, under ash === ::error::CHANGELOG.md is not in the checkout at v0.4.1. exit=1 === version with no section === ::error::CHANGELOG.md has no '## [9.9.9]' section. exit=1 ``` ### Two defects in the `guard` job, not flagged, fixed here Found while re-reading that job for the `$auth` claim. It installs only `curl` and `jq`, so it runs under ash, where the issue body was built with two bashisms: - `read -r -d ''` does not exist in ash. - `<<EOF` with an indented `EOF` never terminates, so the terminator and everything after it were swallowed into the heredoc. Under ash the whole block produced nothing and exited 0: ``` === replaced version (read -d + indented EOF), under ash === exit=0 <- no output at all === new version, under ash === The release workflow for `v0.4.1` did not publish artifacts. - build: failure - publish: skipped ... ``` Built with `printf` now, and `needs.*.result` moved into `env:` for the same no-tags-in-script rule. This is the job whose only purpose is to speak up after a release has already failed, so a silent failure inside it defeats the point of adding it.

Superseded by Forseti review for a03aa3559691.

Forseti review

1 blocking finding(s) from the lead reviewer (advisory: not gating merges yet).

Findings

  • P1 .forgejo/workflows/release.yml:57 — The prepare job only validates the raw ref name by character set and a loose version-shaped glob. A branch named like v1.2.3 passes this check, and the later checkout uses the bare ref name rather than the tag namespace, so a manual dispatch can build branch contents while the publish step targets that name as the release tag. That breaks the stated guarantee that dispatch builds an existing tag's code.

  • PR: rasterstate/fj#233

  • Head SHA: 69697ac7da05

  • Review job: sha256:dabc8f7bbe6c8334dcdcde4e934f0e24a5fc219eabf99500ed4d2667f3467e42

  • Provider pair: openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001

  • Blocking findings: 1

  • Inline findings: 1

  • Token source: GITHUB_TOKEN

  • Runner: 8df7d6b0bbfb

  • Run: https://rasterhub.com/rasterstate/fj/actions/runs/402

<!-- forseti:review {"version":2,"repo":"rasterstate/fj","pr":233,"head_sha":"69697ac7da05933290f52f2e18762930e3dad29a","provider_pair":"openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001","policy_version":"stub-policy-v1","prompt_version":"prompt-v2","context_fingerprint":"fnv64:7e3667687a676337","review_job_key":"sha256:dabc8f7bbe6c8334dcdcde4e934f0e24a5fc219eabf99500ed4d2667f3467e42","base_sha":"5d4789713fc7e5e5317565cc01396275fe479f9c","role":"summary","status":"superseded"} --> > Superseded by Forseti review for `a03aa3559691`. ## Forseti review 1 blocking finding(s) from the lead reviewer (advisory: not gating merges yet). ### Findings - **P1** `.forgejo/workflows/release.yml:57` — The prepare job only validates the raw ref name by character set and a loose version-shaped glob. A branch named like `v1.2.3` passes this check, and the later checkout uses the bare ref name rather than the tag namespace, so a manual dispatch can build branch contents while the publish step targets that name as the release tag. That breaks the stated guarantee that dispatch builds an existing tag's code. - PR: `rasterstate/fj#233` - Head SHA: `69697ac7da05` - Review job: `sha256:dabc8f7bbe6c8334dcdcde4e934f0e24a5fc219eabf99500ed4d2667f3467e42` - Provider pair: `openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001` - Blocking findings: `1` - Inline findings: `1` - Token source: `GITHUB_TOKEN` - Runner: `8df7d6b0bbfb` - Run: https://rasterhub.com/rasterstate/fj/actions/runs/402
forgejo-actions requested changes 2026-07-30 01:05:59 +00:00
Dismissed
forgejo-actions left a comment

Forseti review

1 blocking finding(s) from the lead reviewer (advisory: not gating merges yet).

Findings

  • P1 .forgejo/workflows/release.yml:57 — The prepare job only validates the raw ref name by character set and a loose version-shaped glob. A branch named like v1.2.3 passes this check, and the later checkout uses the bare ref name rather than the tag namespace, so a manual dispatch can build branch contents while the publish step targets that name as the release tag. That breaks the stated guarantee that dispatch builds an existing tag's code.

  • PR: rasterstate/fj#233

  • Head SHA: 69697ac7da05

  • Review job: sha256:dabc8f7bbe6c8334dcdcde4e934f0e24a5fc219eabf99500ed4d2667f3467e42

  • Provider pair: openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001

  • Blocking findings: 1

  • Inline findings: 1

  • Token source: GITHUB_TOKEN

  • Runner: 8df7d6b0bbfb

  • Run: https://rasterhub.com/rasterstate/fj/actions/runs/402

<!-- forseti:review {"version":2,"repo":"rasterstate/fj","pr":233,"head_sha":"69697ac7da05933290f52f2e18762930e3dad29a","provider_pair":"openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001","policy_version":"stub-policy-v1","prompt_version":"prompt-v2","context_fingerprint":"fnv64:7e3667687a676337","review_job_key":"sha256:dabc8f7bbe6c8334dcdcde4e934f0e24a5fc219eabf99500ed4d2667f3467e42","base_sha":"5d4789713fc7e5e5317565cc01396275fe479f9c","role":"summary","status":"current"} --> ## Forseti review 1 blocking finding(s) from the lead reviewer (advisory: not gating merges yet). ### Findings - **P1** `.forgejo/workflows/release.yml:57` — The prepare job only validates the raw ref name by character set and a loose version-shaped glob. A branch named like `v1.2.3` passes this check, and the later checkout uses the bare ref name rather than the tag namespace, so a manual dispatch can build branch contents while the publish step targets that name as the release tag. That breaks the stated guarantee that dispatch builds an existing tag's code. - PR: `rasterstate/fj#233` - Head SHA: `69697ac7da05` - Review job: `sha256:dabc8f7bbe6c8334dcdcde4e934f0e24a5fc219eabf99500ed4d2667f3467e42` - Provider pair: `openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001` - Blocking findings: `1` - Inline findings: `1` - Token source: `GITHUB_TOKEN` - Runner: `8df7d6b0bbfb` - Run: https://rasterhub.com/rasterstate/fj/actions/runs/402
release: pin the release checkout to the tag namespace
All checks were successful
Forseti review / forseti review (advisory) (pull_request_target) Successful in 2m26s
ci / check (pull_request) Successful in 10m26s
ci / coverage (pull_request) Successful in 1m53s
ci / live-e2e (pull_request) Successful in 2m0s
a03aa35596
The previous commit validated the dispatched ref by character set and a
version-shaped glob, then handed the bare name to actions/checkout. A
shape is not an identity: a BRANCH named v0.4.1 passes both checks, and
version-shaped branches already exist here (release/v0.3.0). git itself
does not agree with itself about what a bare name means when both refs
exist. `git rev-parse v1.2.3` returns the tag, `git checkout v1.2.3`
takes the branch and only warns. So a dispatch could build branch
contents while publish attached them to the release of that name, which
in a workflow whose whole purpose is recovering a broken tag release is
the one outcome that would make it worse than useless.

Make the guarantee structural rather than a pattern match:

- prepare now requires the name to exist in the repository's tags
  collection, which a branch cannot satisfy, and exports the commit sha
  the tag resolves to. The lookup goes through the same API base and
  token the guard job already uses.
- build and publish check out refs/tags/<tag> rather than <tag>, so
  there is no ambiguity left for git to resolve in either direction.
- both then assert git rev-parse HEAD equals the sha prepare resolved.
  That closes the residue the namespace alone does not cover, a tag
  moved between prepare and a build, and it makes the two jobs provably
  the same tree instead of two independent resolutions.

The shape checks stay. They still bound the charset for the tarball
names, the formula, and the release API, and `/` remains excluded so the
tag cannot alter the API path it is interpolated into.

Verified locally against a repository holding both a tag v1.2.3 and a
branch v1.2.3 at different commits: the bare name checks out the branch
and the assertion refuses the build; refs/tags/v1.2.3 checks out the tag
and passes. The resolver was exercised against a stub tags API over ten
inputs, including a version-shaped name with no such tag, which is the
v0.4.0 case in this repo.

Refs #232
Author
Owner

Confirmed and fixed in a03aa35, not justified. The finding is correct, and it is the residue of my own previous fix: 69697ac moved the guard early enough to run before a shell parsed the tag, but what it validated was the shape of a string, and the checkout still resolved a bare name. A shape is not an identity.

This is not theoretical in this repository. release/v0.3.0 already exists as a branch, so version-shaped branch names are normal here, and a branch named v0.4.1 would satisfy both the charset allowlist and the v[0-9]*.[0-9]*.[0-9]* glob.

Worse, git does not agree with itself about what a bare name means when a tag and a branch share it. Against a fixture holding tag v1.2.3 and branch v1.2.3 at different commits:

git show-ref v1.2.3
  2530fec refs/heads/v1.2.3     BRANCH CONTENT (must never be released)
  7632c1f refs/tags/v1.2.3      TAG CONTENT (what a release must ship)

git rev-parse v1.2.3        -> 7632c1f   (the tag)
git checkout --detach v1.2.3 -> 2530fec  (the branch), with only:
  warning: refname 'v1.2.3' is ambiguous.

So the validation and the checkout can disagree about which commit the name refers to, and the disagreement surfaces as a warning rather than a failure. In a workflow that exists specifically to make a failed tag release recoverable, publishing branch contents as that tag's artifacts is the one outcome that would make the recovery path worse than useless.

The fix makes the guarantee structural rather than a pattern match, in three parts:

  1. prepare requires the name to exist in the repository's tags collection, which a branch cannot satisfy, and exports the commit sha it resolves to. Verified against this repo's API: repos/rasterstate/fj/tags/v0.4.1 returns commit.sha 62d4d7f6..., while repos/rasterstate/fj/tags/main errors, so the endpoint does not fall back to branches. The lookup uses the same API base and token the guard job already uses.
  2. Both build and publish check out refs/tags/<tag> instead of <tag>, so there is no ambiguity left for git to resolve in either direction.
  3. Both then assert git rev-parse HEAD equals the sha prepare resolved. The namespace alone does not cover a tag that moves between prepare and a build, and this also makes the two jobs provably the same tree rather than two independent resolutions.

Evidence for the fix, same fixture: the bare name checks out 2530fec and the assertion refuses the build; refs/tags/v1.2.3 checks out 7632c1f and the assertion passes. The resolver itself was exercised against a stub tags API over ten inputs and rejected, among others, v9.9.9 and v0.4.0: both are shape-valid, and v0.4.0 is a name with no tag in this repository, which is exactly the class the old guard waved through.

The shape checks stay. They still bound the charset for the tarball names, the rendered formula, and the release API, and / remains excluded so the tag cannot alter the API path it is interpolated into.

Noting the pattern, since it is the same one twice: both times the guard was in the right place and operating on the wrong thing. First it validated a string after the shell had already expanded it; then it validated a string instead of pinning the ref. The correction in both cases was to make the check structural rather than textual.

Confirmed and fixed in a03aa35, not justified. The finding is correct, and it is the residue of my own previous fix: 69697ac moved the guard early enough to run before a shell parsed the tag, but what it validated was the *shape of a string*, and the checkout still resolved a bare name. A shape is not an identity. This is not theoretical in this repository. `release/v0.3.0` already exists as a branch, so version-shaped branch names are normal here, and a branch named `v0.4.1` would satisfy both the charset allowlist and the `v[0-9]*.[0-9]*.[0-9]*` glob. Worse, git does not agree with itself about what a bare name means when a tag and a branch share it. Against a fixture holding tag `v1.2.3` and branch `v1.2.3` at different commits: ``` git show-ref v1.2.3 2530fec refs/heads/v1.2.3 BRANCH CONTENT (must never be released) 7632c1f refs/tags/v1.2.3 TAG CONTENT (what a release must ship) git rev-parse v1.2.3 -> 7632c1f (the tag) git checkout --detach v1.2.3 -> 2530fec (the branch), with only: warning: refname 'v1.2.3' is ambiguous. ``` So the validation and the checkout can disagree about which commit the name refers to, and the disagreement surfaces as a warning rather than a failure. In a workflow that exists specifically to make a failed tag release recoverable, publishing branch contents as that tag's artifacts is the one outcome that would make the recovery path worse than useless. The fix makes the guarantee structural rather than a pattern match, in three parts: 1. `prepare` requires the name to exist in the repository's **tags collection**, which a branch cannot satisfy, and exports the commit sha it resolves to. Verified against this repo's API: `repos/rasterstate/fj/tags/v0.4.1` returns `commit.sha 62d4d7f6...`, while `repos/rasterstate/fj/tags/main` errors, so the endpoint does not fall back to branches. The lookup uses the same API base and token the `guard` job already uses. 2. Both `build` and `publish` check out `refs/tags/<tag>` instead of `<tag>`, so there is no ambiguity left for git to resolve in either direction. 3. Both then assert `git rev-parse HEAD` equals the sha `prepare` resolved. The namespace alone does not cover a tag that moves between `prepare` and a build, and this also makes the two jobs provably the same tree rather than two independent resolutions. Evidence for the fix, same fixture: the bare name checks out `2530fec` and the assertion refuses the build; `refs/tags/v1.2.3` checks out `7632c1f` and the assertion passes. The resolver itself was exercised against a stub tags API over ten inputs and rejected, among others, `v9.9.9` and `v0.4.0`: both are shape-valid, and `v0.4.0` is a name with no tag in this repository, which is exactly the class the old guard waved through. The shape checks stay. They still bound the charset for the tarball names, the rendered formula, and the release API, and `/` remains excluded so the tag cannot alter the API path it is interpolated into. Noting the pattern, since it is the same one twice: both times the guard was in the right place and operating on the wrong thing. First it validated a string after the shell had already expanded it; then it validated a string instead of pinning the ref. The correction in both cases was to make the check structural rather than textual.

Forseti review

No blocking findings from the lead reviewer.

No inline findings.

  • PR: rasterstate/fj#233
  • Head SHA: a03aa3559691
  • Review job: sha256:09217eb0db3dca93c2b5cf81de03785981579698832cfa04be8028c247ea3911
  • Provider pair: openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001
  • Blocking findings: 0
  • Inline findings: 0
  • Token source: GITHUB_TOKEN
  • Runner: 5c5e0f84c3d4
  • Run: https://rasterhub.com/rasterstate/fj/actions/runs/404
<!-- forseti:review {"version":2,"repo":"rasterstate/fj","pr":233,"head_sha":"a03aa3559691e77a6fd7cf8dbc2bc5a6bc9eec43","provider_pair":"openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001","policy_version":"stub-policy-v1","prompt_version":"prompt-v2","context_fingerprint":"fnv64:9eed838f2551a4cb","review_job_key":"sha256:09217eb0db3dca93c2b5cf81de03785981579698832cfa04be8028c247ea3911","base_sha":"5d4789713fc7e5e5317565cc01396275fe479f9c","role":"summary","status":"current"} --> ## Forseti review No blocking findings from the lead reviewer. _No inline findings._ - PR: `rasterstate/fj#233` - Head SHA: `a03aa3559691` - Review job: `sha256:09217eb0db3dca93c2b5cf81de03785981579698832cfa04be8028c247ea3911` - Provider pair: `openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001` - Blocking findings: `0` - Inline findings: `0` - Token source: `GITHUB_TOKEN` - Runner: `5c5e0f84c3d4` - Run: https://rasterhub.com/rasterstate/fj/actions/runs/404
forgejo-actions left a comment

Forseti review

No blocking findings from the lead reviewer.

No inline findings.

  • PR: rasterstate/fj#233
  • Head SHA: a03aa3559691
  • Review job: sha256:09217eb0db3dca93c2b5cf81de03785981579698832cfa04be8028c247ea3911
  • Provider pair: openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001
  • Blocking findings: 0
  • Inline findings: 0
  • Token source: GITHUB_TOKEN
  • Runner: 5c5e0f84c3d4
  • Run: https://rasterhub.com/rasterstate/fj/actions/runs/404
<!-- forseti:review {"version":2,"repo":"rasterstate/fj","pr":233,"head_sha":"a03aa3559691e77a6fd7cf8dbc2bc5a6bc9eec43","provider_pair":"openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001","policy_version":"stub-policy-v1","prompt_version":"prompt-v2","context_fingerprint":"fnv64:9eed838f2551a4cb","review_job_key":"sha256:09217eb0db3dca93c2b5cf81de03785981579698832cfa04be8028c247ea3911","base_sha":"5d4789713fc7e5e5317565cc01396275fe479f9c","role":"summary","status":"current"} --> ## Forseti review No blocking findings from the lead reviewer. _No inline findings._ - PR: `rasterstate/fj#233` - Head SHA: `a03aa3559691` - Review job: `sha256:09217eb0db3dca93c2b5cf81de03785981579698832cfa04be8028c247ea3911` - Provider pair: `openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001` - Blocking findings: `0` - Inline findings: `0` - Token source: `GITHUB_TOKEN` - Runner: `5c5e0f84c3d4` - Run: https://rasterhub.com/rasterstate/fj/actions/runs/404
stephen deleted branch fix/release-job-artifacts 2026-07-30 02:10:35 +00:00
stephen referenced this pull request from a commit 2026-07-30 03:06:06 +00:00
Sign in to join this conversation.
No description provided.