Latest Results
Replace the outdated-dependency shell filters with a configured script (#7037)
* Replace the outdated-dependency shell filters with a configured script
The check lived as two inline shell blocks in
check_outdated_dependencies.yml, so it could not be run outside CI, and the
ignored package names were baked into grep patterns over the rendered report.
`scripts/check_outdated_deps.py {backend,frontend}` now does the work and the
workflow is two one-line steps. Held names move to `[tool.check-outdated-deps]`
in the root pyproject.toml, so changing what is ignored is a config edit.
Matching is on parsed package names rather than a regex over the table, which
removes the anchoring the grep needed to avoid swallowing siblings: a bare
`tailwindcss` entry cannot match `tailwindcss-animated`, and a `plotly.js`
entry cannot match `plotly.js-locales` or `react-plotly.js`. Entries ending in
`*` opt into prefix matching for the cases that need it -- `@chakra-ui/*` for
the scope, `ag-grid*` for the three packages that release in lockstep, which
the shell filter covered with both `ag-grid` and `ag-grid-react`.
Two config keys rather than one, because the two reasons a package is ignored
want different treatment:
- `*_held` -- blocked on work we own. A held package that is already at its
latest version means the blocker is gone, so the entry should be deleted:
that errors, which keeps the list from accumulating dead entries.
- `*_pinned` -- pinned by policy or owned by another repo (pyright, ruff, the
reflex-enterprise packages, the ag-grid family). Reaching latest says nothing
about whether the entry can go, so staleness is not checked.
Both lists start out as the filters main already had, all of which are
`*_pinned`; `*_held` is where blocker-driven entries land.
Two details the parser has to get right, both verified against a real bun
1.4.0 install rather than assumed:
- bun right-pads the package column, so the name needs trimming out of the
cell rather than matching a fixed-width field.
- bun annotates rows outside `dependencies` in that cell -- `tailwindcss
(dev)`, `react (peer)`, `clsx (optional)`. The annotation is stripped before
matching; npm names cannot contain spaces, so the suffix is unambiguous.
Correspondingly, the declared-name set used for staleness covers all four
sections bun reports on, not just dependencies and devDependencies.
Note this job only triggers on release/** pushes and workflow_dispatch, so it
does not run on this pull request. That is part of why running it locally
matters; `uv run python scripts/check_outdated_deps.py backend` and
`--web-dir <app>/.web` for the frontend side both work against a checkout.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKQ4ARC2vkE5iKoULyaczz
* Fix codespell: unparseable -> unparsable
Caught by the pre-commit job, which runs codespell on top of ruff. I had run
ruff locally but not the full `uv run pre-commit run --all-files`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKQ4ARC2vkE5iKoULyaczz
---------
Co-authored-by: Claude <noreply@anthropic.com> Add prerelease-test skill for release QA campaigns (#7001)
* Add prerelease-test skill for release QA campaigns
Captures the workflow used to validate the 0.9.9 release train as a
repeatable skill, so the same analysis can be kicked off before any
release.
SKILL.md lays out the phases: discover what shipped from the
pre-release branch changelogs and confirm every package published to
PyPI, de-risk with a smoke test, fan out feature clusters that build
real apps and drive them in a browser, upgrade-test reflex-examples
apps from the previous stable, regression-test reflex-enterprise demos,
audit wheel/sdist packaging, then triage into fix-before-release vs
file-as-issue. Testing installs only published packages, baselines
against the previous stable to tell regressions from pre-existing bugs,
and verifies each claimed issue with an independent reproducer before
reporting it.
references/ holds the brief handed to each test agent (hard rules,
environment traps, known-benign console noise), cluster decomposition
guidance, the Workflow explore/verify pipeline with schemas and a port
map, and report templates.
scripts/ bundles three helpers, each validated against the released
0.9.9 train:
- check_release_versions.py: changelog versions -> PyPI publication
check (16 packages, all published)
- audit_pyi.py: stubs present in both wheel and sdist, byte-identical,
no foreign stubs, counts vs pyi_hashes.json (121 stubs, clean)
- drive_app.py: Chromium driver with console/network capture, a small
JSON action language and benign-noise filtering
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Format the python snippet in the agent brief
ruff-format runs on markdown in pre-commit, so embedded python blocks
are formatted too; the import/assert one-liner becomes two statements.
Reproduced locally with the locked ruff 0.16.3, which now reports the
whole skill directory as already formatted.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Address cubic review findings
All eight were valid. check_release_versions: publication status is now
tri-state so a network failure is indeterminate (exit 2) rather than a
false 'unpublished' blocker, and never-publish-packages from the release
config are excluded from the scan. audit_pyi: the missing-artifact early
return omitted 'expected' and crashed main() with KeyError, and a
package absent from the manifest is now held to zero stubs instead of
going unchecked. drive_app: a server that never came up raised out of
page.goto before the report was written; startup failures are captured
and exit 1. orchestration: a dead verifier no longer leaves verdict:null
in verified_issues. SKILL.md: the documented invocation omitted --ref and
bare scripts/... paths collide with the repo's own scripts/ directory.
Re-validated after the changes: discovery finds 16 published packages,
the documented Phase 5 pipe audits 121 stubs clean, and the driver's
clean, action-failure and dead-server paths all behave as documented.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Address the second review round
Four further valid findings, distinct from the first round:
- drive_app: inline action JSON longer than the filesystem's filename
limit made Path(raw).exists() raise OSError before parsing, so the
documented inline form failed for any non-trivial script (verified: a
1109-byte payload raised 'File name too long' before, runs now).
- drive_app: the report was read immediately after the last action, so
anything it triggered could land after the browser closed.
- audit_pyi: the import root came from a dist-name heuristic; it now
comes from the stub manifest, which is authoritative and — unlike
deriving it from the artifact — keeps the foreign-stub check
meaningful.
- scripts: dropped bare python3 shebangs for PEP 723 metadata and
'uv run --script', matching scripts/check_min_deps.py and CLAUDE.md.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Keep indeterminate packages out of the packaging audit
--specs feeds a downloader, so a package whose PyPI check could not complete
was turning a controlled indeterminate result into a traceback one stage later.
Emit only confirmed-published rows, and report the skipped ones on stderr so a
short list is never mistaken for a complete one.
Guard the download in audit() too, so a package that cannot be fetched for any
other reason fails on its own row instead of aborting the whole audit.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Report unchecked packages instead of dropping them
A changelog with no version heading was skipped silently, so the one package
whose publication went unverified was also the one the report never mentioned.
Record it as indeterminate, which keeps it out of --specs and exits 2.
Also treat a fully yanked release as not installable, note partial yanks in the
detail column, and give the packaging cluster the same repo-root-relative script
path the rest of the skill uses.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Stop the packaging pipeline from hiding an unchecked package
Documenting phase 5 as a shell pipe meant the audit's exit status replaced the
discovery script's, so a package dropped for an incomplete PyPI check took the
audit's PASS with it. Document it as two commands instead.
Parse --actions before launching the browser: doing it after spent a launch and
a page load on a typo and left a traceback where the report should be. A list
that is not a list now says so too.
Say plainly in the orchestration example that dead explorers are kept as
markers, since filter(Boolean) does not drop them and should not.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Bound artifact downloads and survive a corrupt one
urlretrieve honours only the global socket timeout, which is unset, so a stalled
PyPI transfer would hang the whole audit instead of failing one row. Stream the
download through urlopen with a timeout.
A corrupt wheel or sdist raised out of audit() and took the rest of the train
with it, the same way an unreachable package used to. Handle it per package.
Every exit from audit() now goes through one record builder, so the reporting
loop can no longer meet a record that is missing a key it reads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Skip the packages whose changelog is not a release version
Internal packages are patch-released on every push to main rather than through
the changelog, so a changelog heading in one names a version that was never cut
and would be checked against PyPI as a false blocker. Exclude them alongside the
never-published ones, which the release config already groups the same way.
Never audit a yanked artifact when a live one of the same kind exists: it is not
what a user installs, so the audit would be reporting on the wrong file.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Separate a package that could not be audited from a broken one
An unreachable PyPI or an unreadable archive was being recorded as a packaging
defect, so a healthy package would fail the release on infrastructure trouble.
Track those as unchecked instead and exit 2, the same missing-versus-unknown
split check_release_versions.py already makes, and exit 1 only for a defect
observed in artifacts that were actually read.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Account for the work a failed action already started
A failing action skipped the settle wait because nothing had been recorded as
performed, so a click that reached the server before its assertion failed took
the resulting console error and failed request with it. Waiting is exactly what
that case needs: the response it triggered is often the whole finding.
Give a dead verifier its own marker too. Dropping it left a hole that read the
same as an issue the fan-out cap never sent to a verifier, which is the failure
the explorer marker two lines below exists to prevent.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Stop the agent preamble from overpromising ports
The shared preamble handed every agent a 20-port range, but the map gives a
verify agent only 4, so a verifier following the bootstrap verbatim would reach
into its sibling's range — the collision the map exists to prevent. Take the
ends from the table instead of a fixed span.
Say which venv the checkout-shadowing guard should name. It is the brief's most
important instruction and it was the one placeholder with three plausible fills.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Keep three more failure paths from ending the run
A 200 response that is not PyPI's shape was read as a confirmed missing
release: only urls present and empty means that, so anything else is now
indeterminate rather than a false blocker.
A truncated gzip sdist raises EOFError, which is neither OSError nor TarError,
so the audit aborted on exactly the interrupted download the surrounding
handler was written for.
A page an action crashes or closes made the settle and the screenshot raise
outside every guard, so the run ended in a traceback with no report at all --
the report being the one thing the caller was waiting for.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Never leave a partial artifact where the next run will reuse it
A download that died partway wrote its bytes straight to the final path, and
--keep skips whatever already exists, so every later run reused the truncated
file. The EOFError handling added last commit made that worse: it tells you to
re-run to tell a bad download from a bad artifact, and re-running could not
clear the bad download. Write to .part and rename only once the copy completes.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Gate the audit on discovery, and stop two more raises escaping
Two commands on separate lines never gated anything: the audit ran whatever
discovery returned, so the truncated-list PASS the prose claims to prevent
still happened. Chain them with && so a non-zero discovery skips the audit.
A urls list whose entries are not PyPI's raised out of pypi_status instead of
reporting one bad check, and a corrupted deflate stream raises zlib.error,
which subclasses only Exception and so missed the archive handler entirely.
Name exceptions module-qualified unless they are builtins: zlib.error is called
just "error", and "could not read artifacts: error" tells the reader nothing.
Report an unknown --ref rather than letting CalledProcessError print a stack
about subprocess internals over what is almost always a typo.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Guard the destinations too, not just the writes
The screenshot's mkdir sat outside the handler added for the screenshot itself,
so a destination that cannot be created still ended the run in a traceback --
the same hole the handler was there to close, one line above it. The report
write had no guard at all: losing the file should not also lose the findings,
so the summary now prints either way.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Stop an unreadable page from reporting a clean run
Reading the title and body was guarded, but the failure only reached the body
text, so a page that died after a successful load and actions produced
RESULT: clean and exit 0. A QA driver that says clean when it inspected nothing
is the one result it must never produce, so the read failure is now recorded
and counts against the run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Fix the pipe left in the audit script's own usage
SKILL.md moved to && so a failed discovery cannot hand its dropped packages to
a passing audit, but this script still documented the pipe it warns about, so
anyone reading the script instead of the skill would copy the masking form.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Fail the run when a requested report cannot be written
The guard added two commits ago printed REPORT NOT WRITTEN and carried on, so a
healthy app with an unwritable --report path still printed RESULT: clean and
exited 0. A caller that asked for a report and got none has no result to trust,
and automation would have read that as a pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Make the documented commands runnable and reject bad actions early
Both usage docstrings showed bare script invocations, but these scripts have no
shebang and no exec bit by design -- the PEP 723 metadata is there so uv builds
the environment. Copying either documented line failed. Show uv run --script,
the same form the skill's canonical command uses.
A list whose items are not single-key objects passed validation and failed
later inside the action loop, spending a browser launch and a page load on a
typo the early exit was added to catch.
A yanked flag that is present but not a boolean no longer reads as live: absent
still means not yanked, but a value that is not PyPI's cannot be interpreted.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
* Exclude the root changelog when the root package is excluded
never-publish-packages and internal-packages name packages, and reflex-release
resolves those names against all_packages(), which puts the root package in the
same namespace as the directories under packages-dir. Its config validation
accepts root-package in either list on exactly those terms.
changelog_paths() appended the root CHANGELOG.md unconditionally, so a repo whose
root package never publishes (or patch-releases internally) would have its root
changelog checked against PyPI and reported as a false release blocker, and
emitted into --specs for the artifact audit to fetch.
Turn the exclusion set into changelog paths, mapping the root package name to
CHANGELOG.md, so the root is excluded on the same terms as any sub-package.
Verified against synthetic refs that add "reflex" to each list: before, 13
changelogs with the root included in both cases; after, 12 with the root
dropped. The real config is unchanged — 13 packages, byte-identical output.
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
--------- fix(assets): link shared assets atomically instead of check-then-act (#7039)
* fix(assets): link shared assets atomically instead of check-then-act
`asset(shared=True)` created the symlink into `assets/external/` with a
sequence that was racy at every step: the `exists()`/`is_symlink()` guard,
the `unlink()` in the `FileExistsError` handler, and the retry `symlink_to()`
after it. Concurrent compiles into one working directory — pytest-xdist
workers, parallel builds, containers on a shared bind mount — lost those
races and aborted the compile with `FileNotFoundError` from the `unlink()`,
or with an unhandled `FileExistsError` from the retry.
Build the link under a unique temporary name in the destination directory
and `os.replace()` it into place, which atomically overwrites whatever the
loser of the race left behind and needs no retry. Errors still propagate,
so `asset()` cannot return a path with no symlink behind it.
The old guard is dropped: `exists()` follows the link, so a destination
pointing at some other existing file made it skip rather than repoint. The
replacement fast path compares `readlink()` against the intended target,
keeping the "no needless re-creation for file watchers" property while
actually converging on the right target.
The `FileExistsError` comment attributed this to docker bind mounts; that
is one cause, but the general one is concurrency, with no container involved.
* chore: name the news fragment for PR #7039
* docs: tighten comments and the news fragment
* fix(assets): bound the staged symlink name so long asset names still link
* fix(assets): retry the staged rename when Windows denies a concurrent replace
* fix(assets): cut the staged name prefix by encoded bytes, not characters
* fix(assets): treat an unresolvable destination as needing replacement
* fix(assets): stage the link under a name that does not derive from the asset
* refactor(assets): guard only the destination when checking an existing link
* test(assets): compare the untouched link after resolution for Windows Latest Branches
0%
benedikt-bartscher:disable-implicit-event-handlers 0%
claude/outdated-dependencies-release-t26evx +5%
ProgrammerPlus1998:httpxodus/httpx2-migration © 2026 CodSpeed Technology