Skip to content

Fix release workflow skipping publish when using custom builds - #6950

Merged
masenf merged 2 commits into
mainfrom
claude/github-actions-skip-propagation-t0t67d
Aug 26, 2026
Merged

Fix release workflow skipping publish when using custom builds#6950
masenf merged 2 commits into
mainfrom
claude/github-actions-skip-propagation-t0t67d

Conversation

@masenf

@masenf masenf commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Description

Fixes a critical bug in the release workflow where using a [[tool.reflex-release.custom-build]] entry would cause the publish step to silently skip without uploading packages, while still reporting success.

Root cause: When a custom build is configured, exactly one of the built-in build job and the custom-build job runs for any given package β€” the other is always skipped. GitHub's implicit success() status function (applied when no explicit if condition is set) evaluates over the entire transitive dependency closure rather than just direct dependencies. This caused the skipped build path to propagate through the collect job (which absorbs both paths) and reach the publish job, silently skipping it despite having packages to publish.

Solution:

  1. publish.yml: Added explicit if condition to the publish job that checks needs.collect.result == 'success' and !cancelled() alongside the existing skipped check. This prevents the implicit success() from propagating through skipped dependencies.

  2. publish.yml: Added explicit if condition to the tag-and-release job checking needs.publish.result == 'success' and !cancelled(). Using !failure() would be insufficient since a skipped publish is neither failed nor cancelled.

  3. release_from_changelog.yml: Enhanced the report step's validation logic to distinguish between legitimately skipped legs (when detect found no packages for them) and improperly skipped legs (when detect found packages but the leg didn't run). The script now:

    • Passes ANY and ANY_LAST outputs from the detect job to indicate whether each leg had work
    • Uses a check_leg() function that only accepts 'skipped' when there was no work to do
    • Reports an error if a leg was skipped despite having packages to publish
  4. test_scaffold.py: Added comprehensive test coverage:

    • test_the_gate_survives_the_build_path_that_is_always_skipped: Verifies the publish job has proper status checks
    • test_the_tag_is_pushed_only_after_a_successful_upload: Verifies tag-and-release uses success() not failure()
    • test_the_report_is_red_when_a_leg_with_work_did_not_publish: Parametrized test validating all combinations of leg states and work indicators

Testing

  • Added 3 new test functions with comprehensive parametrized coverage (8 test cases for the report validation)
  • Tests verify both the workflow YAML structure and the shell script logic
  • All tests pass with the fix in place

Checklist

  • Tests added for the bug fix
  • Changes follow the code style guidelines
  • No breaking changes introduced

https://claude.ai/code/session_011bhWhmMrSDwsuUK41vXQ4T

Review in cubic

Exactly one of the built-in `build` job and a `[[tool.reflex-release.custom-build]]`
job runs for a given package, so the other is always skipped. GitHub evaluates
the implicit `success()` a job gets when its `if` names no status function over
the whole transitive dependency closure rather than the direct `needs`, so that
skipped build reached `publish` straight through the `collect` written to absorb
it, and `tag-and-release` behind it: every build succeeded, the artifacts were
verified and checksummed, and the upload silently never happened.

Guard both jobs with an explicit `needs.<job>.result == 'success'`. Not
`!failure()` as `collect` uses β€” a skipped `publish` is neither failed nor
cancelled, so tolerating it would push the tag for a release that uploaded
nothing.

`release_from_changelog.yml`'s `report` job, the canonical failure signal for a
partial release, was blind to the same shape: it accepted any skipped leg, so a
batch that published nothing exited 0, and its "held back" diagnostic sat behind
a flag that case never set. It now accepts a skipped leg only when detection
found nothing for that leg to publish.

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

codspeed-hq Bot commented Aug 26, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

βœ… 27 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing claude/github-actions-skip-propagation-t0t67d (e56a150) with main (12d29c7)

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 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR prevents skipped build paths from suppressing package publication and tightens reporting when a release leg with detected work does not run.

  • Adds explicit status guards to package publication and release tagging.
  • Validates release-leg results against the package-detection outputs.
  • Adds workflow-structure and shell-behavior regression tests.
  • Documents the corrected custom-build release 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/templates/workflows/publish.yml Adds explicit dependency-result guards so skipped transitive build paths do not suppress publishing or allow tagging without a successful upload.
packages/reflex-release/src/reflex_release/templates/workflows/release_from_changelog.yml Makes the aggregate report distinguish legitimate no-work skips from skipped release legs that had packages to publish.
tests/units/reflex_release/test_scaffold.py Adds focused regression coverage for publish/tag conditions and report-step result validation.
packages/reflex-release/news/+publish-skip-propagation.bugfix.md Documents the custom-build skip-propagation bug and the corrected release behavior.

Reviews (2): Last reviewed commit: "test(reflex-release): run the report ste..." | Re-trigger Greptile

@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.

No issues found across 4 files

Re-trigger cubic

`bash` on PATH on a Windows runner is the WSL launcher, which exits 1 with
"Windows Subsystem for Linux has no installed distributions" β€” so the report
cases expecting a red exit passed for the wrong reason and the green ones
failed. Resolve Git for Windows' bash there instead (it ships beside the git
the runners already have) and skip when no POSIX bash exists at all, so the
worst case is a skip rather than a false failure. Inherit the environment
rather than building a bare one, which bash on Windows needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011bhWhmMrSDwsuUK41vXQ4T
@masenf
masenf merged commit e196d03 into main Aug 26, 2026
111 checks passed
@masenf
masenf deleted the claude/github-actions-skip-propagation-t0t67d branch August 26, 2026 18:44
masenf added a commit that referenced this pull request Aug 26, 2026
* fix(reflex-release): stop a skipped build path from skipping the upload

Exactly one of the built-in `build` job and a `[[tool.reflex-release.custom-build]]`
job runs for a given package, so the other is always skipped. GitHub evaluates
the implicit `success()` a job gets when its `if` names no status function over
the whole transitive dependency closure rather than the direct `needs`, so that
skipped build reached `publish` straight through the `collect` written to absorb
it, and `tag-and-release` behind it: every build succeeded, the artifacts were
verified and checksummed, and the upload silently never happened.

Guard both jobs with an explicit `needs.<job>.result == 'success'`. Not
`!failure()` as `collect` uses β€” a skipped `publish` is neither failed nor
cancelled, so tolerating it would push the tag for a release that uploaded
nothing.

`release_from_changelog.yml`'s `report` job, the canonical failure signal for a
partial release, was blind to the same shape: it accepted any skipped leg, so a
batch that published nothing exited 0, and its "held back" diagnostic sat behind
a flag that case never set. It now accepts a skipped leg only when detection
found nothing for that leg to publish.

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

* test(reflex-release): run the report step with a POSIX bash

`bash` on PATH on a Windows runner is the WSL launcher, which exits 1 with
"Windows Subsystem for Linux has no installed distributions" β€” so the report
cases expecting a red exit passed for the wrong reason and the green ones
failed. Resolve Git for Windows' bash there instead (it ships beside the git
the runners already have) and skip when no POSIX bash exists at all, so the
worst case is a skip rather than a false failure. Inherit the environment
rather than building a bare one, which bash on Windows needs.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
masenf pushed a commit that referenced this pull request Aug 27, 2026
Merge brings in two reflex-release changes that alter the templates:
dependency-pin lifting during materialize (#6889) and the skip-propagation
fix in publish.yml and release_from_changelog.yml (#6950). `sync --check`
flagged all three affected workflows as drifted, which is the mechanism
working β€” a template change on main that never reached this repository's own
workflows is exactly what it is there to catch. Regenerated; the uv and
Python pins and the dispatch checkboxes are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXbekKtPyfbENVnZdTRxKX
masenf added a commit that referenced this pull request Aug 27, 2026
…package (#6941)

* Replace the release workflows with the bundled reflex-release

The publish pipeline was a hand-maintained set of workflows over
scripts/release.py and .github/scripts/*. reflex-release, which lives in
this repository at packages/reflex-release, is that same pipeline packaged
and generalized, so the workflows now come from its templates instead.

Every reflex-specific behavior moves into [tool.reflex-release] in the
repo-root pyproject.toml: the reflex/reflex-base lockstep pair (one
version, exact pin rewritten at build time, reflex uploaded last), the
internal packages that patch-release on every push, the America/Los_Angeles
release timezone, and the packages exempt from the news-fragment check. The
.pyi check on the reflex wheel becomes the pipeline's post_build.sh hook,
and the dev-pin gate is now reflex-release's own check-dev-pins, which
reports identically to scripts/check_min_deps.py --check-dev-pins.

cli-command runs the copy in this repository straight from uv.lock rather
than a version published to PyPI:

    uv run --frozen --package reflex-release reflex-release

That pins the pipeline to one commit. The workflows are rendered from the
templates of the commit that contains them, so `sync --check` β€” which
changelog.yml now runs on every pull request β€” fails both on a workflow
edited by hand and on a template change that was never regenerated. The
two can no longer drift apart.

What changes in behavior, beyond the move:

- publish.yml splits validation, build and verification into separate
  unprivileged jobs and puts the SHA-256 manifest in front of the approver;
  the manifest is also attached to the GitHub release.
- changelog.yml also runs on pull requests targeting the publishing
  branches, and its release-branch exemption for version headings now
  requires the pull request to be authored by github-actions[bot].
- Dispatch release takes a comma-separated package list instead of one
  checkbox per package, which no longer fits GitHub's workflow_dispatch
  input limit.
- auto_release_internal.yml triggers on an internal package's src/ rather
  than its whole directory, matching what detection counts as its source,
  and diffs the whole pushed range instead of the last commit.

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

* Pin the release toolchain in reflex-release, restore the checkboxes

Four changes to the tool, and the regenerated workflows that follow from
them.

Pinned toolchain. New uv-version and python-version keys write the uv and
Python the generated workflows install verbatim into every setup-uv step,
in place of whatever that action resolves at run time. Both default to a
version reflex-release itself pins, so upgrading the tool moves the release
toolchain with it and `sync --check` reports that as drift until the
workflows are regenerated β€” the same signal a template change already
gives. A repository that wants its own cadence sets either key; "" leaves
that version to the setup action. Both are interpolated into a quoted YAML
scalar, so they are validated against a version-or-specifier pattern rather
than trusted.

The pins live in one rendered block instead of in each template, and a test
asserts every setup-uv step in every template carries the placeholder that
receives it: render() only fails on a placeholder it cannot substitute, so
a step added without one would otherwise silently install an unpinned uv.

Pinned build backend. reflex-release pins hatchling and
uv-dynamic-versioning exactly. Build requirements are resolved fresh rather
than locked, so a repository that vendors the tool β€” as this one now does,
running it out of uv.lock β€” no longer has the backend that builds its
release tooling move underneath it.

Checkbox limit. Ten was wrong: workflow_dispatch takes twenty inputs, and
this repository's own dispatch form has been running eighteen checkboxes.
Raised to nineteen packages plus the release action, which brings the
checkboxes back here β€” the comma-separated fallback was a regression, not a
fix.

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

* Regenerate the release workflows after merging main

Merge brings in two reflex-release changes that alter the templates:
dependency-pin lifting during materialize (#6889) and the skip-propagation
fix in publish.yml and release_from_changelog.yml (#6950). `sync --check`
flagged all three affected workflows as drifted, which is the mechanism
working β€” a template change on main that never reached this repository's own
workflows is exactly what it is there to catch. Regenerated; the uv and
Python pins and the dispatch checkboxes are unchanged.

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

* Add never-publish-packages; append new Config fields

Both from cubic's review of #6941.

never-publish-packages, for the packages a repository builds but never
releases. changelog-exempt-packages was the closest thing, and it only
waives the news-fragment requirement β€” it left integrations-docs with a
release checkbox and publishable by hand, which the checkbox form made
visible. A listed package now gets no checkbox, is never auto-selected, is
skipped by changelog detection even when it has a CHANGELOG.md, needs no
fragment, and is refused by prepare-publish, so the one remaining way to
reach it β€” typing it into the publish workflow β€” fails in the first
unprivileged job rather than at verify-dist after a build. Being
unreleasable it cannot also be a lockstep member, a custom-build package,
latest-release-package or internal; each is rejected when the configuration
loads. integrations-docs moves to the new key, which drops it from the
Dispatch release form.

Config's new fields move to the end of the dataclass. It is exported, so
the generated __init__ has a positional contract: uv_version and
python_version sat after cli_command, shifting every later argument for a
caller that does not pass everything by keyword. A test pins the historical
field order as a prefix so the next field added lands in the right place.

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

* Re-run the changelog check when pull request labels change

Ports #6916 into the reflex-release changelog template, so the fix applies
to every repository the tool scaffolds rather than to this one's copy of a
workflow that is now generated.

skip-changelog and changelog-version-edit waive parts of the check, so a
verdict is only valid for the label set it was computed under. Three ways
that broke: applying a label after the last push started no run at all,
removing one left the green run that label produced standing with the gate
silently open, and re-running a failed run replayed the original event
payload β€” where the label does not exist yet β€” so the check kept failing
until someone pushed again.

So: labeled/unlabeled join the trigger types, and the labels are read back
from the API into a step output the two guarded steps test, in a step that
runs before the checkout because it needs nothing but `gh`. The job itself
stays ungated: a job skipped by `if` reports its check as skipped, which
branch protection counts as passing, so a cheap no-op on an unrelated label
would overwrite a real failure with a green.

Regenerated reflex's own changelog.yml from the template. The jq program was
checked against the label sets it has to distinguish, running the generated
step itself against a stubbed `gh`.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
masenf added a commit that referenced this pull request Aug 28, 2026
* Materialize changelogs for reflex-release@0.1.0a1 (new-prerelease-minor)

* Materialize changelogs for reflex-release@0.1.0a2 (continued-prerelease)

* Materialize changelogs for reflex-release@0.1.0a3 (continued-prerelease)

* Fix release workflow skipping publish when using custom builds (#6950)

* fix(reflex-release): stop a skipped build path from skipping the upload

Exactly one of the built-in `build` job and a `[[tool.reflex-release.custom-build]]`
job runs for a given package, so the other is always skipped. GitHub evaluates
the implicit `success()` a job gets when its `if` names no status function over
the whole transitive dependency closure rather than the direct `needs`, so that
skipped build reached `publish` straight through the `collect` written to absorb
it, and `tag-and-release` behind it: every build succeeded, the artifacts were
verified and checksummed, and the upload silently never happened.

Guard both jobs with an explicit `needs.<job>.result == 'success'`. Not
`!failure()` as `collect` uses β€” a skipped `publish` is neither failed nor
cancelled, so tolerating it would push the tag for a release that uploaded
nothing.

`release_from_changelog.yml`'s `report` job, the canonical failure signal for a
partial release, was blind to the same shape: it accepted any skipped leg, so a
batch that published nothing exited 0, and its "held back" diagnostic sat behind
a flag that case never set. It now accepts a skipped leg only when detection
found nothing for that leg to publish.

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

* test(reflex-release): run the report step with a POSIX bash

`bash` on PATH on a Windows runner is the WSL launcher, which exits 1 with
"Windows Subsystem for Linux has no installed distributions" β€” so the report
cases expecting a red exit passed for the wrong reason and the green ones
failed. Resolve Git for Windows' bash there instead (it ships beside the git
the runners already have) and skip when no POSIX bash exists at all, so the
worst case is a skip rather than a false failure. Inherit the environment
rather than building a bare one, which bash on Windows needs.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Materialize changelogs for reflex-release@0.1.0a4 (continued-prerelease)

* Materialize changelogs for reflex-release@0.1.0 (release-from-prerelease)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Masen Furer <m_github@0x26.net>
Co-authored-by: Claude <noreply@anthropic.com>
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