Support custom build workflows for platform-specific packages - #6891
Support custom build workflows for platform-specific packages#6891masenf wants to merge 4 commits into
Conversation
Packages whose artifacts cannot come from a single `uv build` — a matrix of
platform-specific wheels, say — can now hand their build to a workflow the
consuming repository owns:
[[tool.reflex-release.custom-build]]
packages = ["mypkg"]
workflow = "build_wheels.yml"
expect-artifacts = ["*.tar.gz", "*-macosx_*_arm64.whl"]
The generated publish.yml calls that workflow in place of its own build job
for those packages, passing the package, version, tag, build directory and
the artifact-name prefix to upload under.
publish.yml is restructured from build/publish/tag-and-release into
prepare/build/collect/publish/tag-and-release so that the artifact
verification, the post-build hook, the release notes and the checksum
manifest run in one place whichever job produced the files. The custom build
jobs sit between prepare and collect, so the whole matrix runs before the
approval gate, and collect tolerates a skipped build path but never a failed
one — a lost matrix leg stops the release instead of uploading a partial set.
The trust boundary is unchanged: the calling job grants only contents: read
and no secrets, and a called workflow cannot hold more privilege than its
caller grants, so a custom build is inside the same unprivileged boundary as
the built-in one. verify-dist checks every collected file is that package at
that version, and expect-artifacts additionally requires the set to be
complete, since a version can only be uploaded to PyPI once.
Also:
- reject custom-build on a pin-exact lockstep member, whose pyproject.toml
rewrite happens in a checkout the custom workflow never sees;
- run the dev-pin gate in prepare for custom-built packages, which never
reach the build job where it normally runs;
- fail `sync` (so `sync --check` on every PR) when a configured build
workflow is missing or declares no workflow_call trigger;
- pass DIST_DIR to post_build.sh and fix the README example, which used a
path the build never wrote to.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E1ujR3svpGxMwm5pBMrsXh
Towncrier fragments are named <pr-number>.<type>.md, and the issue_format turns that number into the changelog's link. Renamed from a placeholder to the actual pull request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E1ujR3svpGxMwm5pBMrsXh
Merging this PR will not alter performance
Comparing Footnotes
|
Greptile SummaryThe PR adds repository-owned custom build workflows for packages requiring platform-specific artifacts while retaining the existing approval and publishing boundary.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains within the eligible follow-up scope. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/reflex-release/src/reflex_release/config.py | Adds the custom-build configuration model, package/workflow validation, and artifact expectation lookup. |
| packages/reflex-release/src/reflex_release/scaffold.py | Generates custom reusable-workflow jobs and validates their declared workflow-call interface during synchronization. |
| packages/reflex-release/src/reflex_release/templates/workflows/publish.yml | Splits the release workflow into prepare, build, collect, publish, and release stages and integrates custom build artifacts. |
| packages/reflex-release/src/reflex_release/dist.py | Extends distribution verification to require configured filename patterns. |
| tests/units/reflex_release/test_scaffold.py | Adds coverage for generated custom-build jobs, workflow contracts, artifact collection, and stage dependencies. |
Reviews (3): Last reviewed commit: "Merge main into the custom build branch" | Re-trigger Greptile
There was a problem hiding this comment.
1 issue found and verified against the latest diff
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/scaffold.py">
<violation number="1" location="packages/reflex-release/src/reflex_release/scaffold.py:531">
P1: When a custom workflow declares `workflow_call` without the five contract inputs, `sync` accepts it but GitHub rejects the generated call at release time. Validate `workflow_call.inputs` and their required string types before accepting the workflow.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| f"does not exist. Create it with:\n\n{CUSTOM_BUILD_CONTRACT}" | ||
| ) | ||
| if not re.search( | ||
| r"^\s*workflow_call:", target.read_text(encoding="utf-8"), re.MULTILINE |
There was a problem hiding this comment.
P1: When a custom workflow declares workflow_call without the five contract inputs, sync accepts it but GitHub rejects the generated call at release time. Validate workflow_call.inputs and their required string types before accepting the workflow.
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/scaffold.py, line 531:
<comment>When a custom workflow declares `workflow_call` without the five contract inputs, `sync` accepts it but GitHub rejects the generated call at release time. Validate `workflow_call.inputs` and their required string types before accepting the workflow.</comment>
<file context>
@@ -343,6 +502,41 @@ def check_title_format(config: Config) -> None:
+ f"does not exist. Create it with:\n\n{CUSTOM_BUILD_CONTRACT}"
+ )
+ if not re.search(
+ r"^\s*workflow_call:", target.read_text(encoding="utf-8"), re.MULTILINE
+ ):
+ fail(
</file context>
Pin reflex-release at 945a87bd, which adds custom build workflows (reflex-dev/reflex#6891), and use it to end the publish.yml fork. xy's matrix moves to .github/workflows/build_release_artifacts.yml, a workflow this repository owns and the tool never touches, wired up by [[tool.reflex-release.custom-build]] packages = ["xy"] workflow = "build_release_artifacts.yml" publish.yml is now the tool's output, verbatim — as are the other three — so `reflex-release sync` regenerates all four and `sync --check`, which the generated changelog.yml runs on every pull request, is finally green. Upgrading the tool is a pin bump plus one command, with no patch to re-apply. expect-artifacts names all thirteen files a release must contain. A matrix leg that runs, succeeds and uploads nothing looks exactly like one never configured, and PyPI accepts a version once, so an incomplete set now stops the release before the approval instead of shipping a version some users cannot install. Two xy behaviors that lived in the forked publish.yml move into the build workflow, which is where they belong: - the tag-shape gate, as a `version-gate` job the three build jobs depend on, so it still runs before half an hour of cross-compilation; - the dry run, as that workflow's own workflow_dispatch. It has no publish path at all — the upload lives in publish.yml, which a dry run never calls — which is a stronger guarantee than the boolean gate it replaces. The action pins are the other half of taking the four verbatim: normalizing them would put every generated workflow permanently out of sync with the tool, so xy's one-SHA-per-action policy now applies to the workflows xy authors, build_release_artifacts.yml included. A test asserts the marker that scopes it still identifies exactly the four generated files. One regression, documented rather than hidden: deploy-docs-stg.yml no longer starts automatically after a release. Its `push: tags` trigger cannot see a tag pushed with GITHUB_TOKEN, and a generated publish.yml cannot dispatch it, so it is a manual run with the released tag until the pinned commit carries reflex-release's post-release hook.
- Validate that a custom build workflow declares all five contract inputs, not just a workflow_call trigger. GitHub rejects a call naming an undeclared input, so a renamed or forgotten input used to surface as a failed release; `sync --check` now makes it a red pull request. Read by indentation rather than with a YAML parser, which this tool deliberately does not carry on the release path — it is lenient, and what it misses GitHub still rejects before any job runs. - Restrict the workflow filename to a YAML-safe bare filename. It is interpolated into the generated `uses:` as a bare scalar, so a name carrying YAML structure produced altered workflow YAML instead of an error. - Give collect's checkout full history and tags. post_build.sh moved there from the build job, whose checkout has both, and a hook that inspects them has to keep working. Documented the one remaining difference: collect builds nothing, so the release tag is not applied locally. - Name the sub-table in type errors from [[lockstep]] and [[custom-build]] entries, which pointed at [tool.reflex-release] instead of the table the key actually lives in. Fixed in the shared helper, so the pre-existing lockstep case is covered too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E1ujR3svpGxMwm5pBMrsXh
There was a problem hiding this comment.
1 issue found across 7 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/scaffold.py">
<violation number="1" location="packages/reflex-release/src/reflex_release/scaffold.py:552">
P2: When a custom workflow adds an inline YAML comment after `workflow_call:` or `inputs:`, `sync` rejects the valid workflow as missing every required input. Allow an optional inline comment when matching block keys.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| Returns: | ||
| The index, or None when no line declares that key with a nested block. | ||
| """ | ||
| pattern = re.compile(rf"\s*{re.escape(key)}:$") |
There was a problem hiding this comment.
P2: When a custom workflow adds an inline YAML comment after workflow_call: or inputs:, sync rejects the valid workflow as missing every required input. Allow an optional inline comment when matching block keys.
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/scaffold.py, line 552:
<comment>When a custom workflow adds an inline YAML comment after `workflow_call:` or `inputs:`, `sync` rejects the valid workflow as missing every required input. Allow an optional inline comment when matching block keys.</comment>
<file context>
@@ -502,6 +505,95 @@ def check_title_format(config: Config) -> None:
+ Returns:
+ The index, or None when no line declares that key with a nested block.
+ """
+ pattern = re.compile(rf"\s*{re.escape(key)}:$")
+ return next(
+ (index for index, line in enumerate(lines) if pattern.fullmatch(line.rstrip())),
</file context>
| pattern = re.compile(rf"\s*{re.escape(key)}:$") | |
| pattern = re.compile(rf"\s*{re.escape(key)}:\s*(?:#.*)?$") |
#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
Type of change
Description
This PR adds support for delegating a package's build to a repository-owned workflow, enabling packages with platform-specific artifacts (e.g., compiled extensions requiring a matrix of OS-specific builds) to participate in the reflex-release pipeline.
Key changes
Configuration (
config.py):CustomBuilddataclass to represent a custom build workflow configuration with packages, workflow filename, and optional artifact expectationscustom-buildconfiguration table support with validationcustom_build_for(),custom_build_packages(),expect_artifacts()Workflow generation (
scaffold.py):buildjob into three stages:prepare: Validates the release request and emits version/tag/build-dir outputsbuild: Runsuv buildfor packages without custom workflowscollect: Gathers artifacts from all build paths, verifies them, and prepares for approvalpreparejob (they skip thebuildjob where it normally runs)publish.ymltemplate with placeholders for custom build integrationArtifact verification (
dist.py):verify_dist()to accept optionalexpectpatterns that must each match at least one built fileTesting (
test_scaffold.py,test_config.py,test_dist.py):write_custom_build()for test setupSecurity model
Custom build workflows inherit the calling job's
contents: readpermission and cannot request additional privileges. They run before the approval gate alongside the built-in build job, so:publishjob holding the PyPI credential runs no repository codeDocumentation
Updated
README.mdwith:collectjob's artifact verificationprepareandcollectstagesTest Plan
workflow_calltrigger, invalid configurations)https://claude.ai/code/session_01E1ujR3svpGxMwm5pBMrsXh