Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions packages/reflex-release/src/reflex_release/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
22 changes: 13 additions & 9 deletions packages/reflex-release/src/reflex_release/scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ""


Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 31 additions & 1 deletion tests/units/reflex_release/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
80 changes: 69 additions & 11 deletions tests/units/reflex_release/test_scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,24 @@ 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.
_UV_SETUP = "uses: astral-sh/setup-uv@"
_UV_SETUP_PLACEHOLDER = "@@UV_SETUP_WITH@@"
#: 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 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.
Expand All @@ -71,13 +81,58 @@ 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, (
Comment thread
masenf marked this conversation as resolved.
Comment thread
masenf marked this conversation as resolved.
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_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 of the jobs that check the
repository out, then those of the jobs that do not.
"""
checked_out: list[dict] = []
bare: list[dict] = []
for job in yaml.safe_load(rendered)["jobs"].values():
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_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. 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 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"


def test_render_pins_uv_and_python(config: Config) -> None:
for name in CORE_WORKFLOWS:
rendered = render(name, config)
Expand Down Expand Up @@ -128,9 +183,12 @@ 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.
_, 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)

Expand Down
Loading