Skip to content

ci(release): validate agents before publishing binary - #6986

Merged
ralphbean merged 2 commits into
mainfrom
ci/preflight-release-before-publish
Sep 4, 2026
Merged

ci(release): validate agents before publishing binary#6986
ralphbean merged 2 commits into
mainfrom
ci/preflight-release-before-publish

Conversation

@ralphbean

Copy link
Copy Markdown
Member

Releasing fullsend currently publishes the binary before the agents functional tests finish. That leaves a window where users can receive the new binary while the matching agents tag is still missing.

This changes the release dependency graph so that:

  • the release tag is verified against the triggering commit;
  • agents functional tests run against that release tag first;
  • GoReleaser publishes only after validation passes;
  • the matching agents tag is created last.

The functional tests already build fullsend from the fullsend_ref input, so they validate the unreleased source tag rather than a previous published binary.

Validation:

  • actionlint .github/workflows/release.yml
  • make lint

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Validate agents before publishing release binaries

🐞 Bug fix ⚙️ Configuration changes 🕐 10-20 Minutes

Grey Divider

AI Description

• Gates binary publication on agents functional tests against the triggering release tag.
• Rejects release tags that no longer resolve to the triggering commit.
• Creates the matching agents tag only after validation and publication succeed.
Diagram

graph TD
  T["Release Tag"] --> R["Verify and Resolve"] --> V["Agent Tests"] --> P["Publish Binary"] --> A["Tag Agents"]
Loading
High-Level Assessment

The revised dependency graph is the appropriate approach because it validates the exact tagged source before any user-facing artifact is published. Keeping agent validation after GoReleaser was considered but preserves the inconsistent-release window this change is intended to remove.

Files changed (1) +21 / -2

Bug fix (1) +21 / -2
release.ymlGate release publication on tag and agents validation +21/-2

Gate release publication on tag and agents validation

• Reorders the release jobs so tag verification and agents functional tests complete before GoReleaser publishes binaries. Adds a GitHub API check that resolves lightweight or annotated tags and fails when the tag target differs from the triggering commit; the agents tag remains the final release step.

.github/workflows/release.yml

@ralphbean
ralphbean enabled auto-merge September 3, 2026 19:23
Preflight agents against the release tag.

Guard the release tag against unexpected movement.

Signed-off-by: Ralph Bean <rbean@redhat.com>
@ralphbean
ralphbean force-pushed the ci/preflight-release-before-publish branch from b3b80c6 to 8766e4d Compare September 3, 2026 19:24
@qodo-code-review

qodo-code-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Tag verification is not pinned ✓ Resolved 🐞 Bug ≡ Correctness
Description
resolve-agents verifies the tag against github.sha, but validate-agents later checks out the
mutable github.ref_name; moving the tag after verification lets the gate test a different commit.
This defeats the new guarantee that agents validate the exact source commit being released.
Code

.github/workflows/release.yml[R103-104]

+          EXPECTED_SHA: ${{ github.sha }}
+          TAG: ${{ github.ref_name }}
Relevance

●●● Strong

Recent workflow findings favor pinning checkouts and validation to immutable SHAs for release
correctness.

PR-#2534
PR-#2106

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new check compares the current tag resolution with the immutable event SHA, but it neither
exports nor passes that verified SHA onward. The reusable validation call instead receives the tag
name at lines 74-76, so its later checkout can resolve a changed target.

.github/workflows/release.yml[74-76]
.github/workflows/release.yml[100-117]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The release tag is verified against `github.sha`, but the agents validation workflow is subsequently given the mutable tag name. A tag move between verification and checkout can make validation test a different commit.

## Issue Context
The verification runs in `resolve-agents`, while `validate-agents` executes later and receives `github.ref_name` as `fullsend_ref`.

## Fix Focus Areas
- .github/workflows/release.yml[74-76]
- .github/workflows/release.yml[100-117]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Release runbook contradicts workflow ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The dependency change makes agents validation run before GoReleaser and prevents publication when
validation fails, while the release skill still says validation runs afterward, the binary still
ships, and Slack reports the failure. Operators following post-flight instructions will therefore
diagnose failed preflight releases incorrectly.
Code

.github/workflows/release.yml[14]

+    needs: [validate-agents, resolve-agents]
Relevance

●●● Strong

Recent release-runbook findings were accepted when documentation contradicted workflow behavior.

PR-#1968
PR-#1709

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The workflow now places validate-agents before release, whereas both operational documents
explicitly state that validation occurs after GoReleaser and that a validation failure leaves the
release published with a Slack notification.

.github/workflows/release.yml[13-14]
skills/cutting-releases/SKILL.md[182-190]
skills/cutting-releases/post-flight.md[33-52]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The release workflow now validates agents before publication, but the release skill and post-flight runbook still document the previous post-publication behavior.

## Issue Context
With `release` depending on `validate-agents`, a validation failure skips GoReleaser rather than allowing the fullsend release to ship. The existing Slack condition also does not describe this preflight-failure path.

## Fix Focus Areas
- .github/workflows/release.yml[13-14]
- skills/cutting-releases/SKILL.md[182-190]
- skills/cutting-releases/post-flight.md[33-52]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 65 rules
Review mode: ⚖️ Balanced: This modifies a release CI workflow’s dependency graph, tag verification, and publication ordering, creating meaningful release and security-sensitive behavior risk that warrants a complete review.

Grey Divider

Tip of the day
💡 Did you know, you can route each action level your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ralphbean

Copy link
Copy Markdown
Member Author

/fs-review

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 3, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:42 PM UTC · Completed 8:01 PM UTC

Commit: 8766e4d · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $3.62

@fullsend-ai-review fullsend-ai-review Bot added the risk/moderate PR risk: moderate label Sep 3, 2026
@fullsend-ai-review

Copy link
Copy Markdown

Risk Assessment: moderate (2/5)

Details

Single CI workflow file with one protected path and CI workflow flag active, but small change size; high multi-author contention (8 authors in 90d) and recent fix/revert history (4 commits) elevate Tier 2, producing a moderate composite.

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

High

  • [protected-path] .github/workflows/release.yml — This PR modifies .github/workflows/release.yml, which is under the protected path .github/. The PR has no linked issue authorizing changes to governance/infrastructure files. Human approval is required for protected-path changes.

Low

  • [workflow command injection (defense-in-depth)] .github/workflows/release.yml:114 — The new ::error:: workflow command interpolates ${TAG} and ${TAG_SHA} without sanitizing :: sequences. This is inconsistent with the defensive sanitization pattern already used at line 154 (${GATE_SHA//::/} / ${RESOLVED_SHA//::/}). In practice the risk is near-zero (git forbids : in ref names and SHAs are hex), but consistency with the existing defensive pattern is worth maintaining.
    Remediation: Apply :: sanitization: echo "::error::Release tag ${TAG//::/} resolves to ${TAG_SHA//::/}, expected ${EXPECTED_SHA//::/}"

  • [error-handling-idiom] .github/workflows/release.yml:107 — The "Verify release tag" step does not validate the format of TAG_SHA after API retrieval and potential annotated-tag dereference, unlike the "Resolve agents main" step which validates SHA format with ^[a-f0-9]{40}$. While set -euo pipefail ensures API failures abort, an explicit format check would produce a more diagnostic error message on unexpected API responses.
    Remediation: Add explicit format validation of TAG_SHA after the annotated-tag dereference branch, matching the pattern in "Resolve agents main".


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

Comment thread .github/workflows/release.yml Outdated
Comment thread .github/workflows/release.yml Outdated

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings from a two-model review pass (Codex gpt-5.6-sol + Grok 4.6), verified against the pinned callee before posting.

Posting one finding inline. The tag-verification TOCTOU and the release-runbook contradiction are both already covered by the existing review comments on this PR, so I have not duplicated them.

For reference, the lower-severity items I did not post: the notify-agents-sync-failure if: now has two unreachable clauses (release can no longer be success when validate-agents/resolve-agents fail); release now also blocks on resolve-agents' cross-repo "Resolve agents main" API call, so an unrelated agents-repo blip stops the binary (splitting the tag-verify into its own job would fix both that and the added serialisation); runner-image.yml / sandbox-images.yml still publish GHCR images ungated on the same tag push; and annotated tags are peeled only one level (fails closed).

One thing worth recording since it came up and is not a problem: a reviewer flagged that github.sha might be the tag-object SHA on an annotated-tag push, which would make the new check fail every release. That was checked against this repo's history and is not the case — v0.40.0, v0.39.0 and v0.38.0 are all annotated, and in each the Release run's head_sha is the peeled commit (v0.40.0: tag object 389315ee, peeled 2a103497, run head_sha 2a103497). The one-sided peel in the new step is correct. The job.workflow_sha may be a tag object SHA precedent elsewhere in this repo refers to a different context value.

Comment thread .github/workflows/release.yml Outdated
Moving agents validation ahead of GoReleaser makes publication depend
on that gate, which raises the bar for what the gate has to guarantee.
Three gaps and their fixes:

The tag is verified only before validation. validate-agents clones
fullsend by tag name — the called workflow rejects anything but `main`
or a `v*` ref and uses `git clone --branch`, so it cannot be pinned to
a SHA from this side. A tag moved during the ~45 minutes of functional
tests would therefore leave the gate validating one commit while
GoReleaser publishes another. Add a `recheck-tag` job between the gate
and `release` that verifies the tag again, so publication requires the
tag to have been stable across the whole gate. It is a job rather than
a step in `release` so that a tag moved mid-gate stays a
pre-publication failure with its own notification. Both verify jobs
check out `github.sha` rather than the tag, so the guard never runs a
copy of itself fetched through the ref it is checking.

validate-agents can report success having run no tests. In the pinned
callee an empty E2E_GCP_WIF_PROVIDER makes its "Check for secrets" step
log a warning, skip GCP auth and every test step, and still exit 0; the
roll-up then reports success. Before this change that only cost the
agents tag, but it now ships an unvalidated binary. Fail resolve-agents
when the secret is missing rather than degrading silently.

A blocked release is silent on Slack. notify-agents-sync-failure fires
only when `release` succeeded, and `release` can no longer succeed when
validate-agents or resolve-agents fail — so those two clauses became
unreachable and the pre-publication path lost its notification. Split
the two cases: notify-release-blocked for failures before publishing,
notify-agents-sync-failure for the one failure that can still happen
after. Both now fail loudly on a missing SLACK_WEBHOOK_URL instead of
on a bare curl error, matching notify-vouch-slack.yml.

The tag check lives in scripts/verify-release-tag.sh so both jobs run
the same code. It peels nested annotated tags rather than one level,
shape-checks each SHA before it goes back into an API path, requires
the resolved object to be a commit, and sanitises `::` in its workflow
commands — the two low-severity review findings. `release` also gains
the `timeout-minutes` every other job in the file already has (20, on a
job that has historically taken ~2).

The cutting-releases skill and its runbooks still described the old
order, telling the cutter that a validation failure ships the binary.
Rewritten to match, including what to do when the release is blocked
(re-run the failed jobs; never move the tag).

Validation:
- `bash scripts/verify-release-tag-test.sh` (13 cases)
- `actionlint .github/workflows/release.yml`
- `shellcheck scripts/verify-release-tag*.sh`
- `make lint`

Assisted-by: Claude (fix), Grok (review), Codex (review)
Signed-off-by: Wayne Sun <gsun@redhat.com>
@waynesun09

Copy link
Copy Markdown
Member

@ralphbean I took this over and pushed bf59bd358 on top of your commit — it addresses the two open bot findings, the two lows, and the MEDIUM I posted, plus two things a follow-up Grok 4.6 pass turned up. Your commit is untouched. Summary of what changed and why:

Tag verification now brackets the gate. The qodo HIGH is real but its suggested fix is not available: at the pinned callee beed20e7, functional-tests.yml rejects any fullsend_ref that is not main or ^v[0-9] (:258-263) and clones with git clone --branch (:268-275), so a SHA cannot be passed from this side. Instead there is a new recheck-tag job between validate-agents and release: the tag must resolve to github.sha both before validation starts and after it finishes, so a tag moved during the ~45-minute test window fails the release rather than publishing a binary the gate never saw. Pinning the gate itself to a SHA needs an agents-repo change and stays a follow-up.

The gate could pass having run zero tests. The callee's "Check for secrets" step (:339-346) logs a warning, skips GCP auth and every test step, and exits 0 when E2E_GCP_WIF_PROVIDER is empty — success, no coverage. That used to cost only the agents tag; with publication gated on it, it would ship an unvalidated binary. resolve-agents now fails loudly if the secret is missing.

A blocked release was silent on Slack. notify-agents-sync-failure fires only when release succeeded, and release can no longer succeed when validate-agents/resolve-agents fail — so two of its three clauses became unreachable and the pre-publication path lost its notification entirely. Split into notify-release-blocked (nothing shipped) and notify-agents-sync-failure (shipped, agents tag missing). Both now fail explicitly on a missing SLACK_WEBHOOK_URL instead of on a bare curl error.

Supporting changes. The check lives in scripts/verify-release-tag.sh so both jobs run one implementation; it peels nested annotated tags instead of one level, shape-checks each SHA before it goes back into an API path, requires the final object to be a commit, and sanitises :: — which retires both LOW findings. 13 test cases in scripts/verify-release-tag-test.sh, wired into make script-test. Both verify jobs check out github.sha rather than the tag, so the guard can't be handed a rewritten copy of itself through the ref it is checking. release gains timeout-minutes: 20 (it has historically taken ~2), which every other job in the file already had.

Docs. SKILL.md, post-flight.md and pre-flight.md all described the old order — post-flight in particular told the cutter that a validation failure still ships the binary. Rewritten, including what to do when a release is blocked: re-run the failed jobs, never re-tag (re-tagging moves the tag, which the new verification would then reject).

One thing worth recording as not a problem, since a reviewer raised it: github.sha on an annotated-tag push is the peeled commit, not the tag object, so comparing it to the peeled SHA is correct. Re-verified against v0.38.0 (14a7aab72db46c4d), v0.39.0 (698bce9cd9e8bec7) and v0.40.0 (389315ee2a103497) — in all three the Release run's head_sha is the peeled commit.

Validated locally: bash scripts/verify-release-tag-test.sh (13/13), actionlint .github/workflows/release.yml, shellcheck scripts/verify-release-tag*.sh, make lint — all clean. Reviewed with Codex gpt-5.6-sol and Grok 4.6; all five review threads replied to and resolved.

Happy to back any of this out if you'd rather keep the PR minimal and file the hardening separately.

@fullsend-ai-review

fullsend-ai-review Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 7:23 PM UTC · Ended 8:05 PM UTC

Commit: bf59bd3 · View workflow run →

@ralphbean
ralphbean added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 403540d Sep 4, 2026
70 of 73 checks passed
@ralphbean
ralphbean deleted the ci/preflight-release-before-publish branch September 4, 2026 19:48
@fullsend-ai-retro

fullsend-ai-retro Bot commented Sep 4, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 7:50 PM UTC · Completed 8:01 PM UTC

Commit: bf59bd3 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high · Cost: $3.55

@fullsend-ai-retro

Copy link
Copy Markdown

Retro: PR #6986 — ci(release): validate agents before publishing binary

PR #6986 reordered the fullsend-ai/fullsend release dependency graph so agents functional tests must pass before GoReleaser publishes the binary. Three review sources caught legitimate issues: qodo (tag TOCTOU), fullsend-ai-review ($3.62, claude-opus-4-6 at high effort — command injection, SHA validation, protected-path governance), and waynesun09 (vacuous test gate via multi-model review with Codex + Grok). waynesun09 then pushed a comprehensive hardening commit (+419/−31 across 7 files) addressing all findings. The PR was merged ~26 hours after creation.

Autonomy readiness

Not a candidate for increased autonomy. The most impactful finding — that the validate-agents gate could pass without running any tests when E2E_GCP_WIF_PROVIDER is empty — was caught only by human multi-model review. This required cross-workflow semantic analysis: following the uses: reference to the called reusable workflow and understanding its fail-open behavior. The review agent's findings were legitimate but lower-impact.

Process observations

  1. Auto-triggered reviews failed twice — runs 33796035624 and 33796227825 both failed at "Run review agent" on pull_request_target events. The manual /fs-review trigger (33797827106) succeeded 18 minutes later. This forced the author to intervene manually.
  2. Merged while re-review in progress — waynesun09's fix commit triggered a re-review at 19:23 UTC; the PR was merged at 19:48 UTC with the review agent still running (33910775994). The second review never completed or posted results.
  3. Stale approval — rh-hemartin's approval was on commit 8766e4d, not the fix commit bf59bd3 (+419/−31 across 7 files).

Evidence for existing issues (no new proposals)

@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ❌ Failure (validation failed after 2 iteration(s)) · Started 7:23 PM UTC · Completed 8:05 PM UTC

Commit: bf59bd3 · View workflow run →

Runtime: claude · Model: opus → claude-opus-4-6 · Effort: high

@waynesun09

Copy link
Copy Markdown
Member

Follow-ups from this PR are now filed:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk/moderate PR risk: moderate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants