From 378dd159cec7166a59b5f08639a36d38e8aecc04 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 06:54:13 +0000 Subject: [PATCH 1/2] fix(reflex-release): stop the release run from annotating itself with noise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A release batch of ten packages posted 29 annotations, none of which told anyone anything: - 20 warnings, two per package, from the gated `publish` job. That job deliberately never checks the repository out — it holds the only OIDC privilege and runs nothing but `uv publish` on the artifact the approval covered — so setup-uv finds an empty working directory: its cache-dependency glob matches nothing ("The cache will never get invalidated") and its empty-workdir check warns. Neither is a problem to fix: the job installs no dependencies, so there is nothing to cache. Pass `enable-cache: false` and `ignore-empty-workdir: true` there, via a second render of the uv pin block for jobs without a checkout, so the pins stay in one place. - 9 notices, one per package, from `pin-lockstep` reporting that a package has no exact-pin lockstep siblings. That is the normal case for every package outside a lockstep group; a step that no-ops has nothing an approver needs shown at the top of the run. Log it. Same for `post-release` with no workflow configured. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HDhLwzkKAooYz8x4AB2Ygk --- .github/workflows/publish.yml | 7 +++ .../news/+quiet-publish-annotations.misc.md | 1 + .../src/reflex_release/commands.py | 4 +- .../src/reflex_release/scaffold.py | 22 ++++--- .../templates/workflows/publish.yml | 7 ++- tests/units/reflex_release/test_commands.py | 32 +++++++++- tests/units/reflex_release/test_scaffold.py | 61 ++++++++++++++++--- 7 files changed, 111 insertions(+), 23 deletions(-) create mode 100644 packages/reflex-release/news/+quiet-publish-annotations.misc.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cc1939c6b16..36807ad2865 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -365,11 +365,18 @@ jobs: fi echo "pypi environment protection rules: $rules (prevent_self_review: $self_review)" + # This job deliberately never checks the repository out, so setup-uv + # runs against an empty directory: its cache key would hash nothing and + # its empty-workdir check would warn, once each per released package. It + # installs no dependencies either — `uv publish` only uploads the files + # the approval covered — so there is nothing to cache. - name: Install uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: version: "0.12.5" python-version: "3.14.7" + enable-cache: "false" + ignore-empty-workdir: "true" - name: Download built artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/packages/reflex-release/news/+quiet-publish-annotations.misc.md b/packages/reflex-release/news/+quiet-publish-annotations.misc.md new file mode 100644 index 00000000000..841ded5630e --- /dev/null +++ b/packages/reflex-release/news/+quiet-publish-annotations.misc.md @@ -0,0 +1 @@ +Stop the gated publish job from warning about its (deliberately) empty workspace, and log the no-op lockstep pin without annotating the run. diff --git a/packages/reflex-release/src/reflex_release/commands.py b/packages/reflex-release/src/reflex_release/commands.py index 4412d052e05..9889accfd8f 100644 --- a/packages/reflex-release/src/reflex_release/commands.py +++ b/packages/reflex-release/src/reflex_release/commands.py @@ -615,7 +615,7 @@ def cmd_pin_lockstep(config: Config, package: str, version: str) -> None: config.require_known(package) targets = config.exact_pin_targets(package) if not targets: - notice(f"{package} has no exact-pin lockstep siblings; nothing to do.") + echo(f"{package} has no exact-pin lockstep siblings; nothing to do.") return pyproject = config.package_path(package) / "pyproject.toml" for target in targets: @@ -1174,7 +1174,7 @@ def cmd_post_release(config: Config, tag: str, package: str, version: str) -> No """ workflow = config.post_release_workflow if workflow is None: - notice(f"no {POST_RELEASE_WORKFLOW_KEY} is configured; nothing to dispatch.") + echo(f"no {POST_RELEASE_WORKFLOW_KEY} is configured; nothing to dispatch.") return # An unset environment variable reaches here as an empty string, and GitHub # accepts a dispatch carrying empty inputs — the run would go green having diff --git a/packages/reflex-release/src/reflex_release/scaffold.py b/packages/reflex-release/src/reflex_release/scaffold.py index 5a7484af430..178ccb74088 100644 --- a/packages/reflex-release/src/reflex_release/scaffold.py +++ b/packages/reflex-release/src/reflex_release/scaffold.py @@ -321,7 +321,7 @@ def _selects_package(packages: tuple[str, ...]) -> str: return f"contains(fromJson('{listing}'), inputs.package)" -def _uv_setup_block(config: Config) -> str: +def _uv_setup_block(config: Config, *, checkout: bool = True) -> str: """Render the ``with:`` block pinning the uv and Python the workflows use. Every generated workflow installs uv the same way, so the pins live in one @@ -331,19 +331,22 @@ def _uv_setup_block(config: Config) -> str: Args: config: The repository configuration. + checkout: Whether the job installing uv checked the repository out. A + job that did not runs in an empty working directory, where setup-uv + has nothing to key a cache on and warns twice per run about it — + noise on a job that installs no dependencies anyway. Returns: The block, indented under a ``uses: astral-sh/setup-uv`` step, or an - empty string when neither version is pinned. + empty string when there is nothing to pass. """ - pins = [ - f' {key}: "{value}"' - for key, value in ( - ("version", config.uv_version), - ("python-version", config.python_version), - ) - if value + settings = [ + ("version", config.uv_version), + ("python-version", config.python_version), ] + if not checkout: + settings += [("enable-cache", "false"), ("ignore-empty-workdir", "true")] + pins = [f' {key}: "{value}"' for key, value in settings if value] return "\n".join([" with:", *pins]) if pins else "" @@ -504,6 +507,7 @@ def render(name: str, config: Config) -> str: "@@HEADER@@": header, "@@CLI@@": cli, "@@UV_SETUP_WITH@@": _uv_setup_block(config), + "@@UV_SETUP_NO_CHECKOUT@@": _uv_setup_block(config, checkout=False), "@@MAIN_BRANCH@@": config.main_branch, "@@PRERELEASE_PREFIX@@": config.prerelease_branch_prefix, "@@HOTFIX_PREFIX@@": config.hotfix_branch_prefix, diff --git a/packages/reflex-release/src/reflex_release/templates/workflows/publish.yml b/packages/reflex-release/src/reflex_release/templates/workflows/publish.yml index 7da2e9e5a6e..8ce3bd17349 100644 --- a/packages/reflex-release/src/reflex_release/templates/workflows/publish.yml +++ b/packages/reflex-release/src/reflex_release/templates/workflows/publish.yml @@ -360,9 +360,14 @@ jobs: fi echo "pypi environment protection rules: $rules (prevent_self_review: $self_review)" + # This job deliberately never checks the repository out, so setup-uv + # runs against an empty directory: its cache key would hash nothing and + # its empty-workdir check would warn, once each per released package. It + # installs no dependencies either — `uv publish` only uploads the files + # the approval covered — so there is nothing to cache. - name: Install uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 -@@UV_SETUP_WITH@@ +@@UV_SETUP_NO_CHECKOUT@@ - name: Download built artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 diff --git a/tests/units/reflex_release/test_commands.py b/tests/units/reflex_release/test_commands.py index bbcdd7f5aed..afd72570a52 100644 --- a/tests/units/reflex_release/test_commands.py +++ b/tests/units/reflex_release/test_commands.py @@ -991,7 +991,37 @@ def test_post_release_without_a_configured_workflow_does_nothing( ) commands.cmd_post_release(config, "v1.2.3", "mypkg", "1.2.3") assert captured == [] - assert "no post-release-workflow is configured" in capsys.readouterr().out + out = capsys.readouterr().out + assert "no post-release-workflow is configured" in out + assert "::notice::" not in out + + +def test_pin_lockstep_pins_the_sibling_to_the_exact_version( + config: Config, repo: Path +) -> None: + write_lockstep(repo) + commands.cmd_pin_lockstep(load_config(repo), "mypkg", "1.2.3") + assert '"widget-core == 1.2.3"' in (repo / "pyproject.toml").read_text( + encoding="utf-8" + ) + + +def test_pin_lockstep_without_siblings_does_not_annotate_the_run( + config: Config, repo: Path, capsys: pytest.CaptureFixture +) -> None: + """A step that no-ops has nothing an approver needs to be shown. + + Every package outside an exact-pin lockstep group runs this step, so an + annotation here is one "nothing to do" line per package on the summary of + every release batch. + """ + commands.cmd_pin_lockstep(config, "mypkg", "1.2.3") + out = capsys.readouterr().out + assert "mypkg has no exact-pin lockstep siblings" in out + assert "::notice::" not in out + assert "widget-core >= 0.1.0" in (repo / "pyproject.toml").read_text( + encoding="utf-8" + ) def test_post_release_dispatches_the_workflow_on_the_tag( diff --git a/tests/units/reflex_release/test_scaffold.py b/tests/units/reflex_release/test_scaffold.py index 803f913cc63..76a1e3c5d9a 100644 --- a/tests/units/reflex_release/test_scaffold.py +++ b/tests/units/reflex_release/test_scaffold.py @@ -52,14 +52,14 @@ def test_render_substitutes_every_placeholder(config: Config) -> None: assert "Generated by reflex-release" in rendered -#: The step every generated workflow installs uv with, and the placeholder that -#: has to follow it so the configured pins reach it. +#: The step every generated workflow installs uv with, and the placeholders one +#: of which has to follow it so the configured pins reach it. _UV_SETUP = "uses: astral-sh/setup-uv@" -_UV_SETUP_PLACEHOLDER = "@@UV_SETUP_WITH@@" +_UV_SETUP_PLACEHOLDERS = ("@@UV_SETUP_WITH@@", "@@UV_SETUP_NO_CHECKOUT@@") def test_every_template_pins_the_uv_it_installs() -> None: - """Each setup-uv step must be followed by the pin placeholder. + """Each setup-uv step must be followed by a pin placeholder. render() only fails on a placeholder it could not substitute, so a step added to a template without one would silently install an unpinned uv. @@ -71,13 +71,49 @@ def test_every_template_pins_the_uv_it_installs() -> None: if _UV_SETUP not in line: continue found += 1 - assert lines[index + 1] == _UV_SETUP_PLACEHOLDER, ( - f"{template.name} line {index + 1} installs uv without the " - f"{_UV_SETUP_PLACEHOLDER} line that pins it" + assert lines[index + 1] in _UV_SETUP_PLACEHOLDERS, ( + f"{template.name} line {index + 1} installs uv without one of the " + f"{', '.join(_UV_SETUP_PLACEHOLDERS)} lines that pin it" ) assert found, "no template installs uv; the placeholder guard is checking nothing" +def _uv_steps_without_a_checkout(rendered: str) -> list[dict]: + """Return the setup-uv steps of every job that does not check the repo out. + + Args: + rendered: A rendered workflow. + + Returns: + The ``uses: astral-sh/setup-uv`` step mappings, in job order. + """ + steps: list[dict] = [] + for job in yaml.safe_load(rendered)["jobs"].values(): + job_steps = job.get("steps") or [] + if any("actions/checkout@" in step.get("uses", "") for step in job_steps): + continue + steps += [ + step for step in job_steps if "astral-sh/setup-uv@" in step.get("uses", "") + ] + return steps + + +def test_render_quiets_setup_uv_where_there_is_no_checkout(config: Config) -> None: + """A job with an empty workspace must not warn about it once per release. + + setup-uv hashes the working directory for its cache key and checks that the + directory is not empty; a job that installs uv without a checkout (the gated + publish job) trips both, for a cache it has no dependencies to fill. + """ + found = 0 + for name in CORE_WORKFLOWS: + for step in _uv_steps_without_a_checkout(render(name, config)): + found += 1 + assert step["with"]["enable-cache"] == "false", name + assert step["with"]["ignore-empty-workdir"] == "true", name + assert found, "no job installs uv without a checkout; this guard checks nothing" + + def test_render_pins_uv_and_python(config: Config) -> None: for name in CORE_WORKFLOWS: rendered = render(name, config) @@ -128,9 +164,14 @@ def test_render_omits_the_block_when_nothing_is_pinned(repo: Path) -> None: assert steps # The placeholder line goes away entirely rather than leaving a `with:` with # nothing under it, and takes its own newline with it rather than opening a - # gap in the step list. - for index in steps: - assert lines[index + 1].strip() != "with:" + # gap in the step list. The checkout-less job keeps its block: what that one + # passes silences setup-uv rather than pinning it. + quiet = _uv_steps_without_a_checkout(rendered) + assert sum(1 for index in steps if lines[index + 1].strip() == "with:") == len( + quiet + ) + for step in quiet: + assert set(step["with"]) == {"enable-cache", "ignore-empty-workdir"} assert "\n\n\n" not in rendered yaml.safe_load(rendered) From 49b481ae39459893c85d83bd60f9603484837e92 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 07:52:20 +0000 Subject: [PATCH 2/2] test(reflex-release): assert which uv pin block each job gets, both ways MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The broadened placeholder guard accepts either placeholder after any setup-uv step, and the semantic test only looked at jobs without a checkout — so a template change that put the quieting block on a checked-out job would turn that job's dependency cache off and no test would notice. Split the rendered setup-uv steps by whether their job checks out and assert both directions: a job with no checkout carries enable-cache false and ignore-empty-workdir true, a job that checked out carries neither. Cover every generated workflow rather than only the core ones, so the internal auto-release workflow is checked too. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HDhLwzkKAooYz8x4AB2Ygk --- tests/units/reflex_release/test_scaffold.py | 73 +++++++++++++-------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/tests/units/reflex_release/test_scaffold.py b/tests/units/reflex_release/test_scaffold.py index 76a1e3c5d9a..4f7b9db83fd 100644 --- a/tests/units/reflex_release/test_scaffold.py +++ b/tests/units/reflex_release/test_scaffold.py @@ -52,11 +52,21 @@ def test_render_substitutes_every_placeholder(config: Config) -> None: assert "Generated by reflex-release" in rendered -#: The step every generated workflow installs uv with, and the placeholders one -#: of which has to follow it so the configured pins reach it. -_UV_SETUP = "uses: astral-sh/setup-uv@" +#: The actions the generated workflows install uv and the repository with. +_UV_ACTION = "astral-sh/setup-uv@" +_CHECKOUT_ACTION = "actions/checkout@" + +#: The setup-uv step, and the placeholders one of which has to follow it so the +#: configured pins reach it. Which of the two belongs on a given step depends on +#: whether its job checked out, which test_render_quiets_setup_uv_only_where_ +#: there_is_no_checkout enforces against the rendered YAML. +_UV_SETUP = f"uses: {_UV_ACTION}" _UV_SETUP_PLACEHOLDERS = ("@@UV_SETUP_WITH@@", "@@UV_SETUP_NO_CHECKOUT@@") +#: What the checkout-less block passes on top of the pins, to keep setup-uv from +#: warning about a working directory that is empty on purpose. +_QUIET_SETUP = {"enable-cache": "false", "ignore-empty-workdir": "true"} + def test_every_template_pins_the_uv_it_installs() -> None: """Each setup-uv step must be followed by a pin placeholder. @@ -78,39 +88,48 @@ def test_every_template_pins_the_uv_it_installs() -> None: assert found, "no template installs uv; the placeholder guard is checking nothing" -def _uv_steps_without_a_checkout(rendered: str) -> list[dict]: - """Return the setup-uv steps of every job that does not check the repo out. +def _uv_steps_by_checkout(rendered: str) -> tuple[list[dict], list[dict]]: + """Split a rendered workflow's setup-uv steps by what their job checked out. Args: rendered: A rendered workflow. Returns: - The ``uses: astral-sh/setup-uv`` step mappings, in job order. + The ``uses: astral-sh/setup-uv`` step mappings of the jobs that check the + repository out, then those of the jobs that do not. """ - steps: list[dict] = [] + checked_out: list[dict] = [] + bare: list[dict] = [] for job in yaml.safe_load(rendered)["jobs"].values(): - job_steps = job.get("steps") or [] - if any("actions/checkout@" in step.get("uses", "") for step in job_steps): - continue - steps += [ - step for step in job_steps if "astral-sh/setup-uv@" in step.get("uses", "") - ] - return steps + steps = job.get("steps") or [] + group = ( + checked_out + if any(_CHECKOUT_ACTION in step.get("uses", "") for step in steps) + else bare + ) + group += [step for step in steps if _UV_ACTION in step.get("uses", "")] + return checked_out, bare -def test_render_quiets_setup_uv_where_there_is_no_checkout(config: Config) -> None: - """A job with an empty workspace must not warn about it once per release. +def test_render_quiets_setup_uv_only_where_there_is_no_checkout( + config: Config, +) -> None: + """The quieting block belongs on the jobs with an empty workspace, and only there. setup-uv hashes the working directory for its cache key and checks that the directory is not empty; a job that installs uv without a checkout (the gated - publish job) trips both, for a cache it has no dependencies to fill. + publish job) trips both, for a cache it has no dependencies to fill. A job + that did check out has files to hash and dependencies to install, so the same + block there would quietly turn a cache it wants off. """ found = 0 - for name in CORE_WORKFLOWS: - for step in _uv_steps_without_a_checkout(render(name, config)): - found += 1 - assert step["with"]["enable-cache"] == "false", name - assert step["with"]["ignore-empty-workdir"] == "true", name + for name in GENERATED_WORKFLOWS: + checked_out, bare = _uv_steps_by_checkout(render(name, config)) + found += len(bare) + for step in bare: + assert _QUIET_SETUP.items() <= step["with"].items(), name + for step in checked_out: + assert not _QUIET_SETUP.keys() & (step.get("with") or {}).keys(), name assert found, "no job installs uv without a checkout; this guard checks nothing" @@ -166,12 +185,10 @@ def test_render_omits_the_block_when_nothing_is_pinned(repo: Path) -> None: # nothing under it, and takes its own newline with it rather than opening a # gap in the step list. The checkout-less job keeps its block: what that one # passes silences setup-uv rather than pinning it. - quiet = _uv_steps_without_a_checkout(rendered) - assert sum(1 for index in steps if lines[index + 1].strip() == "with:") == len( - quiet - ) - for step in quiet: - assert set(step["with"]) == {"enable-cache", "ignore-empty-workdir"} + _, bare = _uv_steps_by_checkout(rendered) + assert sum(1 for index in steps if lines[index + 1].strip() == "with:") == len(bare) + for step in bare: + assert set(step["with"]) == set(_QUIET_SETUP) assert "\n\n\n" not in rendered yaml.safe_load(rendered)