Skip to content

reflex-release: add a post-release workflow hook - #6888

Merged
masenf merged 3 commits into
mainfrom
claude/reflex-release-post-release-workflow-hluw0c
Aug 15, 2026
Merged

reflex-release: add a post-release workflow hook#6888
masenf merged 3 commits into
mainfrom
claude/reflex-release-post-release-workflow-hluw0c

Conversation

@masenf

@masenf masenf commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

What this does

Adds post-release-workflow to [tool.reflex-release]. It names a workflow of the consuming repository's own, and publish.yml dispatches it once per published tag, after the upload, the tag and the GitHub release all exist — the hook for whatever has to follow a release: publishing docs, refreshing an image, notifying a downstream repository.

[tool.reflex-release]
post-release-workflow = "docs_publish.yml"

The dispatch runs on the tag, so the workflow sees exactly the tree that was published, and it is handed the three facts about the release:

# .github/workflows/docs_publish.yml
on:
  workflow_dispatch:
    inputs:
      tag: { description: "The published tag", required: true, type: string }
      package: { description: "The published package", required: true, type: string }
      version: { description: "The published version", required: true, type: string }

All three inputs are required because GitHub rejects a dispatch carrying inputs the target workflow does not declare.

Design notes

  • The step is only scaffolded when the setting is present. So is the actions: write grant it needs, in publish.yml and in both workflows that call it (release_from_changelog.yml, auto_release_internal.yml). A repository that runs nothing after a release keeps actions: read and gets no dead step. render() gained a small general rule for this: a placeholder alone on a line is dropped with its line when its value is empty.
  • Naming a generated workflow is rejected by sync. Handing a published tag back to the release pipeline would either re-enter it or fail on inputs it does not declare.
  • A failed dispatch fails the run, loudly. It is the last thing a release does, so it never leaves a half-published version — but a typo in the workflow name is something you want to hear about, and the error names the tag whose follow-up did not start.
  • Adding or removing the setting changes the generated workflows, so it needs a reflex-release sync. The existing PR drift check catches a repository that forgets.

New reflex-release post-release command, wired through env: like every other step.

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

uv run pytest tests/units/reflex_release — 256 passed (10 new, covering config parsing, both rendering paths, the permission widening in all three workflows, the sync guard, and the dispatch/failure behavior). ruff check, ruff format --check and pyright clean.

Split out of a combined branch alongside #6889; the two features are independent, and whichever lands second will need a trivial rebase on README.md, commands.py and test_commands.py.

`post-release-workflow` in `[tool.reflex-release]` names a workflow of the
consuming repository's own. `publish.yml` dispatches it once per published tag,
after the upload, the tag and the GitHub release all exist — the hook for
whatever has to follow a release: publishing docs, refreshing an image,
notifying a downstream repository.

The dispatch runs on the tag, so the workflow sees exactly the tree that was
published, and it is handed `tag`, `package` and `version` as
workflow_dispatch inputs. All three are required because GitHub rejects a
dispatch carrying inputs the target workflow does not declare.

The step and the `actions: write` grant it needs — in `publish.yml` and in both
workflows that call it — are rendered only when the setting is present, so a
repository that runs nothing after a release keeps the narrower permissions.
Naming a workflow this tool generates is rejected by `sync`, since handing a
published tag back to the release pipeline would either re-enter it or fail on
inputs it does not declare.

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

greptile-apps Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds an optional post-release workflow dispatch after publishing each tag.

  • Adds configuration parsing and a post-release CLI command.
  • Conditionally scaffolds the dispatch step and required GitHub Actions permissions.
  • Rejects generated workflows by filename or display name and adds release-path tests and documentation.

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/config.py Adds optional post-release workflow configuration and shared dispatch-contract constants.
packages/reflex-release/src/reflex_release/commands.py Implements validation and dispatch of the configured workflow on the published tag.
packages/reflex-release/src/reflex_release/scaffold.py Conditionally renders the dispatch step and permissions and rejects generated workflow identifiers.
packages/reflex-release/src/reflex_release/templates/workflows/publish.yml Adds placeholders for conditional actions permission and the final post-release step.
tests/units/reflex_release/test_scaffold.py Covers conditional rendering, permission propagation, generated-workflow rejection, and dispatch-contract rendering.

Reviews (3): Last reviewed commit: "Merge branch 'main' into claude/reflex-r..." | Re-trigger Greptile

Comment thread packages/reflex-release/src/reflex_release/scaffold.py Outdated
Comment thread packages/reflex-release/src/reflex_release/config.py Outdated
@codspeed-hq

codspeed-hq Bot commented Aug 14, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing claude/reflex-release-post-release-workflow-hluw0c (598ebe8) with main (fddcb6b)

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.

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

4 issues found across 12 files

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="tests/units/reflex_release/test_scaffold.py">

<violation number="1" location="tests/units/reflex_release/test_scaffold.py:593">
P3: The `with_post_release` helper added here is byte-for-byte identical to the one added in tests/units/reflex_release/test_commands.py (lines 895-913). Both edit pyproject.toml to inject `post-release-workflow` and reload the config. Since both test modules share the same `repo` fixture, hoist this helper into tests/units/reflex_release/conftest.py so the two test files don't each maintain a copy of the same setup.</violation>
</file>

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

<violation number="1" location="packages/reflex-release/src/reflex_release/scaffold.py:384">
P2: When `post-release-workflow` names `auto_release_internal.yml` after internal packages are removed, this guard accepts the generated workflow because it is no longer in `managed_workflows(config)`. `sync` may delete the file or leave no receiver, so the post-release step fails on every release. Reject all generated names, including `OPTIONAL_WORKFLOWS`.</violation>

<violation number="2" location="packages/reflex-release/src/reflex_release/scaffold.py:384">
P2: check_post_release_workflow only rejects post-release-workflow values that match a generated workflow's filename (via managed_workflows). Since `gh workflow run` also resolves workflows by their display name (the `name:` field in the YAML), a user could configure the display name of a generated workflow (e.g. "Publish to PyPI") and this guard would miss it, causing the dispatch to hit the generated workflow with incompatible inputs or re-enter the release pipeline. Consider also checking against the generated workflows' display names, not just filenames.</violation>
</file>

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

<violation number="1" location="packages/reflex-release/src/reflex_release/commands.py:1003">
P3: `cmd_post_release` dispatches `tag`, `package`, and `version` straight into `gh workflow run` without validating any of them. The CLI defaults to the empty string when the `TAG`/`PACKAGE`/`VERSION` env vars are unset, so a manual/misconfigured invocation sends `--field package=` and `--field version=`. `gh workflow run` returns 0 once the dispatch is accepted, so the release run does not fail loudly and the downstream workflow receives empty facts — contradicting the failure-handling design. The sibling publish commands validate inputs (`cmd_prepare_publish` calls `require_known(package)` and validates the version). Validate that the three values are non-empty (and `require_known(package)`) before dispatching.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/reflex-release/src/reflex_release/scaffold.py Outdated
Comment thread packages/reflex-release/src/reflex_release/scaffold.py Outdated
Comment thread tests/units/reflex_release/test_scaffold.py Outdated
Comment thread packages/reflex-release/src/reflex_release/commands.py
Close two holes in the guard that rejects naming a generated workflow as the
post-release hook. `gh workflow run` resolves a workflow by file name *or* by
display name, so "Publish to PyPI" walked straight past a filename-only check;
and the guard consulted `managed_workflows`, which drops
auto_release_internal.yml once a repository has no internal packages — exactly
when `sync` deletes the file, leaving the dispatch with no receiver at all. The
check now covers every workflow this tool can generate, under both names.

Validate the dispatch payload. The CLI defaults TAG/PACKAGE/VERSION to the empty
string when the environment variables are unset, and GitHub accepts a dispatch
carrying empty inputs, so a misconfigured invocation went green having told the
downstream workflow nothing. All three are now required, and the package is
checked against the repository the way the sibling publish commands do.

Name the hook's contract once. The config key and the three dispatch input names
were repeated across config parsing, the dispatch payload, the scaffolded step's
comment and its failure message; they now come from POST_RELEASE_WORKFLOW_KEY and
POST_RELEASE_INPUTS in config.py, so adding an input cannot leave the generated
workflow and the payload disagreeing.

Hoist the identical `with_post_release` test helper out of test_commands.py and
test_scaffold.py into conftest.py as `set_post_release_workflow`, alongside the
`write_lockstep` helper it mirrors.

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

masenf commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

All five review points were valid and are addressed in f1fc2a5.

Guard bypasses (P1 display names, P2 OPTIONAL_WORKFLOWS) — both real, and the same root cause: the guard compared against the wrong set. It now compares against generated_workflow_names(), which is every workflow this tool can generate (CORE_WORKFLOWS + OPTIONAL_WORKFLOWS, not the currently-managed subset) under both the file name and the name: the template declares, read from the templates so the two cannot drift. The auto_release_internal.yml case was the worse of the two: it slipped through precisely when sync deletes the file, so the dispatch would have had no receiver at all.

Empty dispatch payload (P3) — real. --field package= is accepted, gh exits 0, and the run goes green having told the downstream workflow nothing, which contradicts the loud-failure design. All three values are now required and the package is checked with require_known, as cmd_prepare_publish does.

Duplicated identifiers (P2) — taken in the production code: POST_RELEASE_WORKFLOW_KEY and POST_RELEASE_INPUTS in config.py now feed the config parsing, the dispatch payload, the scaffolded step's comment and its failure message. That last one was the actual drift risk — the generated comment told users to declare inputs named in prose, unconnected to the list the payload was built from.

One deliberate exception: the tests still spell the key and the input names out literally. test_post_release_dispatches_the_workflow_on_the_tag asserts the exact gh workflow run argv, and the conftest helper writes the literal TOML key. Deriving those from the same constants would make them pass whatever the constants said — the duplication there is the check.

Duplicated test helper (P3) — hoisted to conftest.py as set_post_release_workflow, next to the write_lockstep helper it mirrors.

Tests are now 265 passing (19 new, including the four bypass names as a parametrized case and the three empty-input cases); ruff check, ruff format --check and pyright clean.


Generated by Claude Code

@masenf
masenf merged commit 092aa4f into main Aug 15, 2026
116 of 117 checks passed
@masenf
masenf deleted the claude/reflex-release-post-release-workflow-hluw0c branch August 15, 2026 00:25
masenf pushed a commit that referenced this pull request Aug 15, 2026
#6888 (post-release workflow hook) landed on main and touches the same
generated workflow, so three conflicts needed more than a textual union:

- POST_RELEASE_STEP read `needs.build.outputs.tag`/`version`. This branch
  splits the old build job into prepare/build/collect, and tag-and-release
  now needs [prepare, publish] — so the dispatch reads those facts from
  prepare, the job that computes them. Retargeted, and its test with it.

- main's render() gained "a placeholder alone on a line is an optional
  block: an empty value removes the line". @@DEFAULT_BUILD_GUARD@@ sat at
  the end of the build job's `if`, so an empty value would have eaten the
  newline and folded `runs-on:` into the expression. The guard is now a
  line of its own, and the two optional job/step blocks let the template
  carry their blank separator instead of embedding a trailing newline.

- check_custom_build_workflows rejected naming a generated workflow via
  managed_workflows(), which has the hole main documents for the same guard:
  it drops auto_release_internal.yml exactly when a repository has no
  internal packages. Switched to main's GENERATED_WORKFLOWS, and made the
  custom-build table label public so the two guards name it from one place.

Verified all four combinations of the two features render, parse as YAML
and round-trip through `sync --check`, with a test covering both at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E1ujR3svpGxMwm5pBMrsXh
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