Latest Results
ENG-11157: add --min-instances/--max-instances to `reflex deploy` (#6884)
* ENG-11157: add --min-instances/--max-instances to reflex deploy
Adds first-class autoscaling controls to `reflex deploy`, backed by the
instance bounds route merged server side in flexgen #4744.
New `hosting.set_instance_bounds()` posts to
/api/v1/apps/{app_id}/instance_bounds with only the bounds the user
actually passed, so apps that don't override keep their platform
defaults. It runs after the app is resolved and validated but before the
export and the deployment submit: the deployment reads the bounds off the
app when it is created, and failing before the export means a rejected
bound doesn't cost a full build. Server details for the 400 (validation /
unsupported platform) and 409 (scale in progress) cases are surfaced
verbatim rather than pre-validated in the CLI.
Also adds regression coverage for the existing --vmtype passthrough,
pinning it from the CLI flag through to the `vm_type` submit field.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ENG-11157: pass --vmtype through to GCP deploys
The server has honored GCP sizing since flexgen#4744; this block predates
that and silently dropped the flag — the same gap the customer originally
reported. Only --region is still dropped (it comes from the connection).
Verified end to end against staging: the flag reaches the deploy submit,
the deployment records Single CPU Medium, and the Cloud Run revision rolls
out with cpu:1/memory:1024Mi.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: keep bounds adjacent to submit, append new params
Move set_instance_bounds() from before the export to immediately before
create_deployment(). Applying it earlier meant a failed export left the
new bounds on the app with no deployment to consume them, silently
changing what the *next* deployment scales to — and, for a non-zero
minimum, what the app costs to run. The residual window is now just the
submit itself, and if that is rejected the CLI says the bounds stuck
instead of letting the next deploy inherit them unannounced.
Append min_instances/max_instances to the end of deploy()'s signature
rather than grouping them next to vmtype, so positional callers keep
binding hostname and everything after it to the same parameters.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Address review: warn on every failed-submit path, fix export fixture
The "bounds outlived the deploy" warning only fired when create_deployment
returned a failure string. It raises on transport errors, and Ctrl-C during
the submit is a third exit, so both left the bounds changed silently. Move
the warning into a _warn_if_bounds_outlive_deploy context manager wrapping
the submit, so every path out of it reports.
mock_export_import_error_fn took a fixed 6 arguments while deploy() calls
export_fn with 7 on reflex > 0.7.6, so it raised TypeError, not the
ImportError its name promises. Both tests using it still passed, for the
wrong reason. Take *args so the intended path is the one exercised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Address review: hedge when the bounds response is lost
set_instance_bounds raises on a transport failure, which happens before
the warning context around the submit is entered. A response lost after
the server applied the write therefore left billable state changed with
no message at all -- the one gap in "a failed deploy always says what
stuck".
Handle it at the call site rather than in the helper: hosting.py's
convention is that transport errors propagate, and deploy() is what knows
this particular call has persistent, billable side effects. The message
hedges both ways, since a dropped connection says nothing about whether
the write landed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Number the changelog fragments for #6884
Also add the missing hosting-cli bugfix fragment for the --vmtype
passthrough on Google Cloud deploys, which shipped in 5a70527 without one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> ENG-10966: Add reflex-release package: changelog-driven release automation (#6868)
* Extract the changelog-driven release pipeline into reflex-release
Adds packages/reflex-release, a standalone, repository-agnostic version of
the release automation currently hard-wired into this repo's scripts/ and
.github/: towncrier news fragments, the changelog-as-source-of-truth
publish detection, and the human-gated PyPI upload. Other repositories
(reflex-enterprise, xy) can adopt the same flow with `uvx reflex-release
init`.
Everything the pipeline needs to know about a repository is declared in
one [tool.reflex-release] table: root package and/or sub-package
directory, branch policy, release timezone, lockstep groups that must
release together at a single version (generalizing the reflex/reflex-base
pair and its exact pin), internal packages that release without a
changelog, and the source paths that require a news fragment. A
single-package repository needs only root-package.
The GitHub Actions workflows are scaffolded into the consuming repository
rather than referenced across repositories, because PyPI trusted
publishing validates the OIDC job_workflow_ref claim against the
repository that owns the workflow file. `reflex-release sync` regenerates
them from the configuration and `sync --check` fails CI on drift, so
upgrading stays a version bump plus one command. The seven shell scripts
the current pipeline shells out to are folded into the CLI, leaving only
four (or five) generated YAML files in a consumer repo; repository-specific
artifact checks move to an optional post_build.sh hook.
The README documents installing the workflows, adding towncrier and news/
directories to a repo that has neither, growing into sub-packages,
tag-derived versions, the GitHub settings that must be configured by hand,
and adopting the flow in a repository that already publishes.
This repo's own release pipeline is untouched; only the new package is
registered with it so it can be selected in Dispatch release and Publish.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Honor towncrier's directory and filename settings
The fragment directory and changelog filename were hardcoded as news/ and
CHANGELOG.md, so a repository that configures towncrier differently would
have its pending fragments silently missed by auto-selection and its
changelog written where the publish detection never looks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Address review: tag prefix, lockstep entries, gh output
- tag-prefix (was root-tag-prefix) now applies to sub-packages as well:
the root package is tagged <tag-prefix><version> and a sub-package
<package>-<tag-prefix><version>, so a repo that tags bare 1.2.3 gets
widget-core-1.2.3 rather than an inconsistent widget-core-v1.2.3.
- Document and pin down the lockstep empty entry. A member dragged along
by a sibling's release already gets towncrier's "No significant
changes." section — even with no news directory at all — but nothing
proved it and the README read as if it were manual work. Adds a
regression test covering plan -> materialize -> detect for a partner
with no fragments, and points the fail-closed error at the re-dispatch
that fixes it.
- Restore gh output in job logs. Actions (release create, workflow run,
pr edit) now stream straight to the log behind an echoed command line
instead of being captured and discarded; only the two calls whose
stdout is consumed (pr create) or whose failure is the normal path
(the release-exists probe) stay captured.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Generate package checkboxes and auto-wire the drift check
The Dispatch release form gets one checkbox per package again, generated
from the configuration at sync time instead of hand-maintained. A lockstep
group renders as a single checkbox covering every member, since selecting
one always widens to the whole group anyway; the checkbox contributes all
its members to the selection.
Selection is carried by a folded PACKAGES scalar of
`${{ inputs.<pkg> && '<packages>' || '' }}` expressions, so an unchecked
box contributes an empty string and checking nothing still means
auto-select. No new protocol between the workflow and the CLI: the
existing selection parser already splits on whitespace and commas.
workflow_dispatch accepts at most ten inputs, so dispatch-package-inputs
defaults to "auto" — checkboxes while they fit, a comma-separated text
field beyond that — and can be forced either way. Package names that
collide once normalized to input identifiers fail at sync time rather
than producing an invalid workflow, and the rendered YAML of every
workflow is now parsed in tests (the folded scalar has to be indented
past its key, which is exactly the kind of break that only shows up when
Actions rejects the file).
Because the checkbox list is generated, adding a package now changes
dispatch_release.yml. The generated changelog workflow therefore runs
`sync --check` on every pull request, so drift — from a new package or a
stale pinned version — is a red PR rather than a surprise at release
time.
Also spells out in the README and the scaffold docstring why the
workflows are copied rather than referenced across repositories: trusted
publishing pins the publish workflow to the consuming repository,
release_from_changelog's `./` call to publish.yml resolves against the
repository holding the calling file, and triggers must be declared
locally regardless — leaving only two job bodies as shareable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Address PR review: heading guard, publish gates, detection ranges
Security:
- The changelog heading guard exempted any branch whose name starts with
the release prefix, and a branch name is attacker-selectable. The
exemption now additionally requires the pull request to be authored by
github-actions[bot], which is how the Dispatch release workflow opens
it and which a contributor cannot forge. The label escape hatch already
needed triage rights.
Correctness:
- prepare-publish auto patch-bumped any package without a changelog, not
just internal ones, so a manual dispatch could publish a version the
changelog never described. It now keys off internal-packages, as its
docstring always claimed.
- The dev-pin gate ran before pin-lockstep, so a publish-last member with
a temporary dev floor on its sibling failed on metadata the build was
about to rewrite. It now runs after the rewrite and judges what ships.
- detect-internal diffed HEAD~1..HEAD while the workflow's paths filter
matches the whole push, so an internal package changed in any but the
last commit was silently skipped. It now diffs the push event's range
and falls back when that base is unreachable.
- Internal root packages never matched: their paths do not start with
"./". Added Config.path_prefix, and the generated trigger uses the
root's source dirs (failing loudly if they are unconfigured).
- towncrier's title_format was honored when writing headings but the
parser could only read version-led ones, so a custom format would
strand every release. sync now round-trips the configured format
through the parser and rejects what it cannot read back.
- Lockstep validation compared list lengths, so a duplicated publish-last
entry could make every member publish last; it compares sets now.
- release-post rejected alphas but accepted beta/rc/dev headings.
- latest-release-package = "" is documented as disabling the Latest
badge but was rejected as an unknown package.
- pin-exact missed PEP 503 equivalent spellings (widget.core).
- ls-remote ran without credentials, so the prerelease branch-existence
check always reported "missing" in private repositories.
- The release commit used `commit -a`, which could sweep unrelated
tracked edits into it; it now stages exactly the changelogs and the
news directories towncrier consumed.
- sync now removes auto_release_internal.yml once no package is internal,
instead of leaving a generated workflow firing on every push.
- An explicit empty root-source-dirs / package-source-subdirs was
overridden by the layout guess.
Also: scalar configuration keys are validated as strings at load time
(a non-string release-timezone crashed inside zoneinfo), reflex_release
joins the coverage sources, and the README's stale root-tag-prefix
reference is corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Harden the release pipeline for cross-repository deployment
Audit of the generated workflows against the usual GitHub Actions
failure modes found the mechanics sound — no expression is interpolated
into a shell script (inputs travel through env:), every checkout sets
persist-credentials: false, no workflow uses pull_request_target, and
the only job holding id-token runs no repository code. The weak points
were in what an adopting repository can configure.
- The environment gate proved reviewers exist but not that they cannot
approve their own deployment. GitHub allows self-review unless it is
turned off, which reduces the two-person rule to a confirmation
dialog. The publish job now fails when the API reports self-review is
allowed, and warns when it cannot tell.
- An empty branch prefix made str.startswith() match everything: setting
hotfix-branch-prefix = "" let *any* branch publish final versions, and
widened the release workflow's push trigger to "**". Empty prefixes,
and prefixes the main branch itself matches, are now rejected at
configuration load.
- init warns when the workflows end up unpinned, and its checklist now
leads with the two settings that make the approval load-bearing:
Prevent self-review, and naming the pypi environment in the PyPI
trusted publisher (without it, any job in publish.yml can mint an
upload token regardless of the gate).
The README gains a Security model section covering the trust boundaries
between the unprivileged build and the credential-holding upload, what
the approval actually covers (and what the checksum manifest does not —
it travels inside the artifact, so it is an integrity check, not a
defense against a compromised build), the ways to weaken the pipeline —
missing branch ruleset, trusted publisher without the environment,
self-review, internal-packages, an unpinned tool, a self-consistent
edit to publish.yml — and the supply-chain position of resolving the
tool from PyPI at run time, with a locked alternative.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Make the self-review policy configurable, permissive by default
The previous commit made a prevented-self-review environment mandatory.
That is the right default for an untrusted reviewer list, but it is a
policy choice, not a property of the pipeline: when the environment's
reviewers are already a small trusted group, requiring a second person
for a routine release buys little and costs availability.
allow-self-review (default true) exposes the choice. True keeps GitHub's
own behavior and the publish job simply records in the log that
self-review is permitted; false restores the assertion, so the setting
cannot silently regress on a repository that depends on it. Either way
the upload still needs an explicit, attributed approval from someone on
the environment's reviewer list, and the reviewer list itself remains
the control worth auditing.
The README documents it as a trade-off rather than a defect: what you
keep under both postures, what you give up under the permissive one, and
when to pick the strict one. The init checklist now leads with "that
list is who can release" and only mentions Prevent self-review for
repositories that opted into asserting it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Address review: detection scope, commit scope, config validation
- detect-internal matched every path for a root internal package: the
root's directory prefix is empty, so a push touching only a sibling
auto-released it. Both detection and the generated paths filter now use
the package's configured source prefixes, so the trigger and the
detection agree instead of one over-matching the other.
- The all-zero base a branch-creating push reports fell back to diffing
only the final commit, which is the failure the surrounding comment
says to avoid. It now treats every tracked file as changed, matching
what the paths filter saw; an unreachable non-zero base keeps the
last-commit fallback.
- The release commit staged every package's news directory, which in a
local materialize could sweep in a fragment someone was drafting.
towncrier already stages the deletions it makes, so staging the
changelogs alone is both narrower and sufficient — covered by a test
that runs a real materialize and asserts the consumed fragment's
deletion is in the commit.
- Configuration: an empty main-branch is rejected (nothing could trigger
or be targeted); a branch prefix nested under the main branch is
rejected (git cannot create a ref below an existing one); a non-string
latest-release-package is rejected instead of silently disabling the
Latest marking, while the documented empty-string opt-out still works.
- check_title_format converts a malformed format string into the same
guidance instead of a KeyError/ValueError traceback, since the
pull-request drift guard runs it on every PR.
Documentation: the news fragment and README both claimed the
allow-self-review = false assertion fails when the setting is missing.
It does not — it fails when it can see self-review is permitted and warns
when GitHub does not report the setting. Both now say so, and the tag
ruleset advice follows the configured tag-prefix rather than hardcoding
`v*`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Rename reflex-release news fragments to PR 6868
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Link the dispatch results and simplify root release titles
The Dispatch release job summary named what it produced but never linked
it: finding the pull request it opened, or the branch it pushed, meant
navigating there by hand. Both are now markdown links in the summary —
the PR as #<number>, the prerelease branch plus a filtered link to that
branch's release_from_changelog runs — and both are emitted as notice
annotations, which surface on the run page itself rather than only in
the summary tab. Outside Actions there is no repository URL to build
from, so the links degrade to the plain text they were before.
GitHub releases for the root package are titled with their tag (v0.2.1)
instead of <package>@<version>: the root package is the repository, so
the tag already names the release unambiguously. Sub-packages keep the
package name, which is what distinguishes their releases.
Applied to reflex's own dispatch_release and publish scripts too, so the
extracted package and the pipeline it was extracted from stay aligned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Read and write every file as UTF-8
The package relied on the platform's preferred encoding for all of its
text I/O. On any system where that is not UTF-8 — Windows, where it is
cp1252, and the ASCII locale the Windows failure reproduces under —
reading a workflow template raised UnicodeDecodeError and writing a
changelog or pull request body raised UnicodeEncodeError. The templates
and the generated PR body both contain non-ASCII text, so sync, init and
the release commands were broken there outright, not just in tests.
Every read_text/write_text/open on a text file in the package and its
tests now names encoding="utf-8", which is what all of these formats
are: towncrier's changelogs and fragments, TOML by specification, and
the files GitHub Actions reads back for $GITHUB_OUTPUT and
$GITHUB_STEP_SUMMARY.
Reproduce the failure on Linux with:
LC_ALL=C PYTHONUTF8=0 PYTHONCOERCECLOCALE=0 uv run pytest tests/units/reflex_release
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Address review: strict self-review, commit scope, title_format
- allow-self-review = false now fails on anything short of a proven
prevent_self_review: true. An environment the API does not report the
setting for is indistinguishable from one that permits self-review, and
strict mode is opt-in, so a warning there is not a two-person rule. The
error names the escape hatch for deployments that cannot report it.
- The release commit stages only the changelogs of the packages in the
release rather than every changelog in the repository, so a worktree
carrying an unrelated changelog edit cannot have it swept in. Config
lost existing_changelogs(), which nothing calls now.
- towncrier's title_format = false writes sections with no version
heading. The pipeline finds versions by reading those headings, so that
configuration would have materialized changelogs no release could ever
be detected from — silently, forever. It is now rejected where the
setting is read, as is a non-string title_format, and the heading probe
additionally catches AttributeError/TypeError.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Bind published artifacts to what was approved
Three gaps found reviewing the publish path against its own promise —
that nothing reaches PyPI without a human knowing exactly what it is.
verify-dist checked the version but never the distribution name. Every
package in a repository publishes through the same trusted-publishing
identity, and lockstep siblings deliberately share a version, so the
version alone does not identify what was built: a build that produced a
different distribution would have uploaded it under this package's
approval, and nothing downstream would have noticed. It now checks the
PEP 503-normalized name from the artifact's core metadata against the
package's [project] name.
The approver could not see what they were approving. The SHA256SUMS
manifest existed but only inside the artifact, so the environment
approval named a package and a version and nothing about the bytes. The
manifest is now written into the build job's summary, which is the page
a reviewer lands on from the approval prompt.
Nothing recorded a published version's contents durably. SHA256SUMS
lived only as a workflow artifact, which expires; after that there was
no record tying a release to the bytes on PyPI. It is now attached to
the GitHub release, so works against a
downloaded wheel indefinitely.
Also: the changelog guard that rejects hand-written version headings ran
only on pull requests into the main branch, while a merged heading is a
publish trigger on any branch release_from_changelog watches — hotfix
branches publish final versions without touching main. It now runs on
pull requests into every branch that can publish, asserted against the
release workflow's own trigger list.
The README's approval section documents all of it, including why PyPI
attestations are deliberately not generated: it would put a third-party
action inside the one job holding the OIDC credential.
Applied to reflex's own scripts/release.py and workflows too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Address review: manifest, base ref, heading guard, tests
- cmd_verify_dist gained a required PACKAGE, which the existing
test_release tests did not set, so both verify-dist tests errored
before reaching what they test. They now pass the package and assert
the PEP 503 spelling difference (reflex_base metadata vs reflex-base
config), plus a new case for the wrong-distribution rejection.
- The SHA256SUMS attached to a release listed dist/-prefixed paths and
the release notes, so `sha256sum -c` against downloaded release assets
reported missing files rather than verifying them — the README claimed
otherwise. The manifest is now written from inside dist/ with bare
filenames covering exactly what goes to PyPI, and the publish job
verifies it from there. Asserted against both call sites.
- reflex's changelog workflow diffed against origin/main for the
affected-package scan and the towncrier check, while now triggering on
pull requests into r/pre-** and r/hotfix/**. A hotfix train diverges
from main, so that folded in the train's already-released source
changes whose fragments are gone, failing legitimate pull requests.
All three steps now use the pull request's own base ref.
- reflex's changelog workflow also still skipped the heading guard on
any branch named release/*, the attacker-selectable bypass the
template fixed two rounds ago. It now requires the github-actions[bot]
author too.
- README: verify-dist runs after the build, not before it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Collapse the reflex-release news fragments into one
Twelve fragments described the review rounds that built one unreleased
package, not twelve things a reader needs. The package has never been
published, so its "fixes" have no released behavior to have changed:
they are just how the first version came out. One feature entry names
the pipeline, what init scaffolds, what the configuration covers and
what gates an upload, and points at the package README for the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
* Revert reflex's own release pipeline to main
The changes to reflex's workflows and scripts/release.py were parity
edits — keeping the extracted package and the pipeline it came from in
step. That is the wrong shape for this pull request: the next step is
replacing the current implementation with reflex-release outright, so
carrying improvements into code that is about to be deleted makes this
diff harder to review for no lasting benefit. Each one lives in the
package's templates, which is where it will land.
What stays is only what publishing reflex-release needs: the package as
a workspace member and dev dependency, its src on the pyright path and
in the coverage sources, pyyaml for the workflow-rendering tests, and
the package registered in the three places the existing pipeline
enumerates packages by hand (the publish choice list, the dispatch
checkbox, and detect.sh).
Reverted: verify-dist's distribution-name check, the checksum manifest
changes, the dispatch summary links, the root release title, the
changelog workflow's trigger/base-ref/bot-author changes, and the
test_release.py updates that supported them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLG
---------
Co-authored-by: Claude <noreply@anthropic.com> Revert reflex's own release pipeline to main
The changes to reflex's workflows and scripts/release.py were parity
edits — keeping the extracted package and the pipeline it came from in
step. That is the wrong shape for this pull request: the next step is
replacing the current implementation with reflex-release outright, so
carrying improvements into code that is about to be deleted makes this
diff harder to review for no lasting benefit. Each one lives in the
package's templates, which is where it will land.
What stays is only what publishing reflex-release needs: the package as
a workspace member and dev dependency, its src on the pyright path and
in the coverage sources, pyyaml for the workflow-rendering tests, and
the package registered in the three places the existing pipeline
enumerates packages by hand (the publish choice list, the dispatch
checkbox, and detect.sh).
Reverted: verify-dist's distribution-name check, the checksum manifest
changes, the dispatch summary links, the root release title, the
changelog workflow's trigger/base-ref/bot-author changes, and the
test_release.py updates that supported them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnY8e5SHmgXG64jsv2ekLGclaude/reflex-deploy-workflow-pkg-jsb589 Latest Branches
+1%
eng-11157-deploy-instance-bounds -1%
claude/reflex-deploy-workflow-pkg-jsb589 0%
carlos/update-search-breadcrumbs © 2026 CodSpeed Technology