Skip to content

Verify GitHub release before handing to post-release workflow - #6927

Open
masenf wants to merge 5 commits into
mainfrom
claude/reflex-release-post-workflow-verify-6jpylq
Open

Verify GitHub release before handing to post-release workflow#6927
masenf wants to merge 5 commits into
mainfrom
claude/reflex-release-post-workflow-verify-6jpylq

Conversation

@masenf

@masenf masenf commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • This change requires a documentation update

Description

create-release now reads the GitHub release back from GitHub instead of trusting gh's exit status. This ensures the tag handed to the post-release-workflow is one that was verified to be complete and correctly configured.

Why this matters: gh release create can succeed even if the asset upload fails, leaving a release without its checksum manifest. The next step (post-release workflow) publishes docs and images against the release, so it must be complete and correctly flagged. This change verifies:

  1. The release exists on the expected tag
  2. The release is published (not a draft)
  3. The release is flagged correctly (prerelease status matches what was requested)
  4. The release is titled correctly
  5. The checksum manifest is attached at the correct size (if one was built)

Re-run behavior: A re-run of a release whose tag already has a release verifies it the same way. A missing or stale manifest (from an attempt that died during asset upload) is the one partial state a re-run finishes by itself — it uploads the manifest again. A release that is drafted, flagged, or titled differently is one this run did not make, and it stops the job for a human to inspect rather than being passed off as this version's release.

Changes

  • Added helper functions to read and validate GitHub releases:

    • _release_view(): reads release metadata from GitHub
    • _metadata_problems(): validates release matches requested configuration
    • _checksum_asset_problem(): validates checksum manifest is attached correctly
    • _accept_existing_release(): handles re-run scenarios with existing releases
  • Updated cmd_create_release() to:

    • Check for existing releases before creating
    • Read back the created release from GitHub
    • Validate the release is complete and correctly configured
    • Provide clear error messages when validation fails
  • Added comprehensive test coverage for all validation scenarios:

    • Successful release creation and verification
    • Handling of missing/unparseable release metadata
    • Rejection of releases with incorrect metadata
    • Rejection of incomplete checksum assets
    • Safe re-run when release already matches
    • Finishing incomplete uploads from failed attempts
    • Rejection of stale releases not made by this run
  • Updated README documentation to explain the verification process and re-run behavior

Test Plan

All new functionality is covered by 11 new test cases in tests/units/reflex_release/test_commands.py:

  • test_create_release_reports_the_verified_release
  • test_create_release_fails_when_the_release_cannot_be_read_back
  • test_create_release_rejects_unparseable_release_metadata
  • test_create_release_rejects_a_release_that_is_not_the_one_asked_for (4 parameterized cases)
  • test_create_release_rejects_an_incomplete_checksum_asset (3 parameterized cases)
  • test_create_release_skips_a_matching_existing_release
  • test_create_release_attaches_a_manifest_an_earlier_attempt_left_off
  • test_create_release_fails_when_attaching_the_manifest_does_not_take
  • test_create_release_refuses_to_reuse_a_stale_release (3 parameterized cases)
  • test_create_release_reuses_a_release_without_a_local_manifest

Existing tests continue to pass with the updated release_args fixture, which now simulates the full release lifecycle including verification.

https://claude.ai/code/session_01KRAGNRmLGo2g59foKazSb9

Review in cubic

create-release took gh's exit status, and the mere existence of a release
named for the tag, as proof the release was there. gh can succeed on a
release whose asset upload did not, and any release on the tag -- a draft,
one flagged or titled differently, one left partial by an earlier attempt --
was accepted and handed straight to the post-release workflow, which
publishes docs and images against it.

The release is now read back from GitHub after it is created: it has to be
on that tag, published rather than drafted, flagged and titled as this run
asked, and carrying the SHA256SUMS manifest at the size that was built.
A re-run over a tag that already has a release verifies it the same way; a
missing or stale manifest is attached again (the one partial state a re-run
can finish by itself), and a release this run did not make stops the job
for a human instead of being passed off as this version's release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KRAGNRmLGo2g59foKazSb9
@masenf
masenf requested a review from a team as a code owner August 21, 2026 20:25
@codspeed-hq

codspeed-hq Bot commented Aug 21, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 27 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing claude/reflex-release-post-workflow-verify-6jpylq (9e3828a) with main (d86f167)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR verifies GitHub release metadata and checksum assets before allowing post-release processing, including safer handling of reruns and failed release reads.

  • Adds structured release read-back and metadata/asset validation.
  • Distinguishes absent releases from general GitHub CLI failures.
  • Repairs missing or stale checksum manifests on otherwise matching releases.
  • Adds unit coverage and documents release verification behavior.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/reflex-release/src/reflex_release/commands.py Adds release read-back, metadata validation, checksum verification, and rerun handling.
packages/reflex-release/src/reflex_release/gitutil.py Adds a GitHub CLI capture helper preserving exit status, stdout, and stderr.
tests/units/reflex_release/test_commands.py Expands release-command coverage across creation, read failures, malformed metadata, asset validation, and reruns.
packages/reflex-release/README.md Documents verified release creation and safe rerun behavior.

Reviews (5): Last reviewed commit: "fix(release): require the assets gh repo..." | Re-trigger Greptile

Comment thread packages/reflex-release/src/reflex_release/commands.py Outdated
Comment thread packages/reflex-release/src/reflex_release/commands.py Outdated
The verification read used gh_output(..., check=False), which flattens
every failure to an empty string: a transient API error, an expired token
or a rate limit read exactly like "this tag has no release". That made a
re-run try to create a release over one that already exists, and reported
a release gh had just created as missing.

gh_capture returns the exit status, stdout and stderr, so the read can
classify: gh saying the release is not there means absent, anything else
is a failure to read and says so, naming what gh reported. A phrasing the
not-found markers miss would stop every ordinary release, so a bare 404
counts as absent too -- the same job pushed this tag through the same gh,
so a not-found on the releases endpoint is about the release.

The requested field list is now the one place the fields are named, and a
payload missing any of them fails loudly instead of reading as a mismatch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KRAGNRmLGo2g59foKazSb9
Comment thread packages/reflex-release/src/reflex_release/commands.py
…sent

Matching gh's stderr widely enough to survive a phrasing change means a
bare 404 counts as "no release" -- but a 404 can also come from a
repository that is missing, renamed or beyond the token's reach, and that
says nothing about the release.

A not-found is now believed only once the repository itself reads back:
one extra gh call, on the path where a release is about to be created
anyway. An unreadable repository fails the read instead, so a release is
never created over one that may already exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KRAGNRmLGo2g59foKazSb9
Comment thread packages/reflex-release/src/reflex_release/commands.py

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/reflex-release/src/reflex_release/commands.py">

<violation number="1" location="packages/reflex-release/src/reflex_release/commands.py:975">
P2: When `gh release view` returns a 404 for an existing draft or otherwise hidden release, a successful `gh repo view` makes `_reads_as_absent` treat the release as absent. `cmd_create_release` then attempts `gh release create` for the existing tag instead of rejecting or completing the existing release. Use a draft-capable release listing or ID lookup before treating the tag as absent.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/reflex-release/src/reflex_release/commands.py
Comment thread packages/reflex-release/src/reflex_release/commands.py
# created anyway: if the repository reads back, the not-found was about the
# release rather than about reaching GitHub.
returncode, _, _ = gh_capture(["repo", "view", "--json", "name"], config.root)
return returncode == 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When gh release view returns a 404 for an existing draft or otherwise hidden release, a successful gh repo view makes _reads_as_absent treat the release as absent. cmd_create_release then attempts gh release create for the existing tag instead of rejecting or completing the existing release. Use a draft-capable release listing or ID lookup before treating the tag as absent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-release/src/reflex_release/commands.py, line 975:

<comment>When `gh release view` returns a 404 for an existing draft or otherwise hidden release, a successful `gh repo view` makes `_reads_as_absent` treat the release as absent. `cmd_create_release` then attempts `gh release create` for the existing tag instead of rejecting or completing the existing release. Use a draft-capable release listing or ID lookup before treating the tag as absent.</comment>

<file context>
@@ -939,6 +948,33 @@ def cmd_push_tag(config: Config, tag: str) -> None:
+    # created anyway: if the repository reads back, the not-found was about the
+    # release rather than about reaching GitHub.
+    returncode, _, _ = gh_capture(["repo", "view", "--json", "name"], config.root)
+    return returncode == 0
+
+
</file context>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not doing this one; the other two from this review are fixed in a11089e.

You're right that a listing sees drafts where the tag endpoint cannot — gh release view goes to get-a-release-by-tag, which returns published releases only. But the consequence is milder than "attempts gh release create ... instead of rejecting or completing the existing release": drafts don't reserve a tag, so the create succeeds, and the release it creates is then read back and verified against the tag, draft flag, title and checksum manifest before anything is dispatched. What's left behind is a stale draft someone else made — untidy, not a bad release, and not something this PR regressed (before it, the same 404 went to create with no verification at all). A draft that is visible is already rejected by the isDraft check this PR adds.

A gh release list scan would catch the invisible case, at the cost of another call on the path every ordinary release takes plus a pagination window that can't be made airtight. That's a behavioral addition worth its own change, so I've flagged it to the PR author rather than folding it in here. Greptile raised the same point on r3833580461 and landed in the same place.


Generated by Claude Code

json.loads accepts more than the object gh documents: a bare null or a
list would reach the field check and raise TypeError, and a null assets
field would raise on iteration. A traceback out of a job that has already
published a version says nothing about what went wrong.

The shape is now established once, where the payload enters -- an object,
carrying every field that was requested, with assets as a list -- so each
miss is a diagnostic failure and everything downstream can read the
payload as the release it claims to be.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KRAGNRmLGo2g59foKazSb9

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/reflex-release/src/reflex_release/commands.py Outdated
The boundary check established that assets is a list but not what the
list holds, so a null or a bare string among them still reached
asset.get() and raised. Every entry now has to be an object for the
payload to be accepted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KRAGNRmLGo2g59foKazSb9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants