FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

ci(workflows): isolate semantic-release action outputs (#83) · pythonnative/pythonnative@b52499e · GitHub

Commit b52499e

Browse files
authored
ci(workflows): isolate semantic-release action outputs (#83)
1 parent b3680df commit b52499e

5 files changed

Lines changed: 119 additions & 5 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ jobs:
2626
permissions:
2727
contents: write
2828
outputs:
29-
tag: ${{ steps.select.outputs.tag }}
30-
commit: ${{ steps.select.outputs.commit }}
29+
# PSR writes its own `tag` output even when released=false. Forward
30+
# only outputs we explicitly set after selecting a source commit.
31+
tag: ${{ steps.select.outputs.release_tag }}
32+
commit: ${{ steps.select.outputs.release_commit }}
3133
steps:
3234
- uses: actions/checkout@v4
3335
with:
@@ -67,13 +69,13 @@ jobs:
6769
# Check the tag against package metadata without importing/building it.
6870
git show "$commit:pyproject.toml" > "$RUNNER_TEMP/release-project.toml"
6971
RELEASE_TAG="$tag" python -c 'import os, pathlib, tomllib; data = tomllib.loads((pathlib.Path(os.environ["RUNNER_TEMP"]) / "release-project.toml").read_text()); assert os.environ["RELEASE_TAG"] == "v" + data["project"]["version"]'
70-
echo "tag=$tag" >> "$GITHUB_OUTPUT"
71-
echo "commit=$commit" >> "$GITHUB_OUTPUT"
72+
echo "release_tag=$tag" >> "$GITHUB_OUTPUT"
73+
echo "release_commit=$commit" >> "$GITHUB_OUTPUT"
7274
7375
distributions:
7476
name: Build release distributions
7577
needs: release
76-
if: needs.release.outputs.tag != ''
78+
if: needs.release.outputs.tag != '' && needs.release.outputs.commit != ''
7779
uses: ./.github/workflows/wheels.yml
7880
with:
7981
source-ref: ${{ needs.release.outputs.commit }}

‎MANIFEST.in‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
global-exclude *.pyc *.pyo .DS_Store
22
include scripts/cibuildwheel.toml scripts/check-distributions.py scripts/release-assets.py
3+
include .github/workflows/release.yml
34
recursive-exclude src/pythonnative/native .build/* .gradle/* .cxx/* build/* .swiftpm/* .kotlin/*
45
prune src/pythonnative/native/android/build
56
prune src/pythonnative/native/android/.gradle

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dev = [
4444
"mypy>=1.10",
4545
"pytest>=8.0",
4646
"pytest-asyncio>=0.23",
47+
"PyYAML>=6.0",
4748
"Pillow>=10.0",
4849
# `pn deps` and `pn run ios` shell out to `sys.executable -m pip` to
4950
# resolve and vendor the app's [requirements].packages for the device

‎tests/test_release_workflow.py‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""Exercise the actual release-selection shell step and its job outputs."""
2+
3+
import os
4+
import re
5+
import shutil
6+
import subprocess
7+
import sys
8+
from pathlib import Path
9+
from typing import Any
10+
11+
import pytest
12+
import yaml
13+
14+
pytestmark = pytest.mark.skipif(os.name == "nt" or not shutil.which("bash"), reason="Release selection runs on Ubuntu")
15+
16+
17+
@pytest.fixture
18+
def release_repo(tmp_path: Path) -> Path:
19+
def git(*args: str) -> None:
20+
subprocess.run(["git", *args], cwd=tmp_path, check=True, capture_output=True)
21+
22+
git("init", "-b", "main")
23+
git("config", "user.name", "Release test")
24+
git("config", "user.email", "release-test@example.invalid")
25+
git("config", "commit.gpgsign", "false")
26+
git("config", "core.hooksPath", "/dev/null")
27+
(tmp_path / "pyproject.toml").write_text('[project]\nversion = "0.40.0"\n')
28+
git("add", "pyproject.toml")
29+
git("commit", "-m", "chore(release): v0.40.0")
30+
git("tag", "v0.40.0")
31+
git("update-ref", "refs/remotes/origin/main", "HEAD")
32+
git("commit", "--allow-empty", "-m", "build(workflows): repair publishing")
33+
34+
commands = tmp_path / "commands"
35+
commands.mkdir()
36+
(commands / "python").symlink_to(sys.executable)
37+
# Keep the test local: replace only external tools, retaining real Git and
38+
# the exact shell step from release.yml. PSR always appends its outputs,
39+
# including the existing tag when it reports released=false.
40+
stubs = {
41+
"uv": "#!/bin/sh\nexit 0\n",
42+
"gh": "#!/bin/sh\necho false\n",
43+
"semantic-release": f"#!{sys.executable}\n"
44+
+ """import os
45+
from pathlib import Path
46+
import subprocess
47+
new_release = os.environ['TEST_RELEASE_MODE'] == 'new'
48+
version = '0.40.1' if new_release else '0.40.0'
49+
if new_release:
50+
Path('pyproject.toml').write_text('[project]\\nversion = "0.40.1"\\n')
51+
subprocess.run(['git', 'commit', '-am', 'chore(release): v0.40.1'], check=True)
52+
subprocess.run(['git', 'tag', 'v0.40.1'], check=True)
53+
with open(os.environ['GITHUB_OUTPUT'], 'a') as output:
54+
output.write(f'released={str(new_release).lower()}\\nversion={version}\\ntag=v{version}\\n')
55+
""",
56+
}
57+
for name, content in stubs.items():
58+
path = commands / name
59+
path.write_text(content)
60+
path.chmod(0o755)
61+
return tmp_path
62+
63+
64+
def run_selection(repo: Path, mode: str, recovery_tag: str = "") -> tuple[dict[str, str], dict[str, str]]:
65+
workflow: dict[str, Any] = yaml.safe_load(
66+
(Path(__file__).resolve().parents[1] / ".github/workflows/release.yml").read_text()
67+
)
68+
job = workflow["jobs"]["release"]
69+
step = next(step for step in job["steps"] if step.get("id") == "select")
70+
output_file = repo / "outputs"
71+
output_file.touch()
72+
env = {
73+
**os.environ,
74+
"PATH": str(repo / "commands") + os.pathsep + os.environ["PATH"],
75+
"GITHUB_OUTPUT": str(output_file),
76+
"RUNNER_TEMP": str(repo),
77+
"RECOVERY_TAG": recovery_tag,
78+
"TEST_RELEASE_MODE": mode,
79+
}
80+
subprocess.run(["bash", "-e", "-o", "pipefail", "-c", step["run"]], cwd=repo, env=env, check=True)
81+
step_outputs = dict(line.split("=", 1) for line in output_file.read_text().splitlines())
82+
job_outputs = {}
83+
for name, expression in job["outputs"].items():
84+
match = re.fullmatch(r"\$\{\{ steps\.select\.outputs\.(\w+) \}\}", expression)
85+
assert match is not None
86+
job_outputs[name] = step_outputs.get(match[1], "")
87+
return step_outputs, job_outputs
88+
89+
90+
def test_no_release_does_not_forward_semantic_release_tag(release_repo: Path) -> None:
91+
step_outputs, job_outputs = run_selection(release_repo, "none")
92+
assert step_outputs["released"] == "false"
93+
assert step_outputs["tag"] == "v0.40.0"
94+
assert job_outputs == {"tag": "", "commit": ""}
95+
96+
97+
def test_new_release_selects_the_new_tagged_commit(release_repo: Path) -> None:
98+
_, job_outputs = run_selection(release_repo, "new")
99+
commit = subprocess.check_output(["git", "rev-parse", "v0.40.1^{commit}"], cwd=release_repo, text=True).strip()
100+
assert job_outputs == {"tag": "v0.40.1", "commit": commit}
101+
102+
103+
def test_recovery_selects_existing_source_instead_of_main(release_repo: Path) -> None:
104+
_, job_outputs = run_selection(release_repo, "none", "v0.40.0")
105+
commit = subprocess.check_output(["git", "rev-parse", "v0.40.0^{commit}"], cwd=release_repo, text=True).strip()
106+
head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=release_repo, text=True).strip()
107+
assert commit != head
108+
assert job_outputs == {"tag": "v0.40.0", "commit": commit}

‎uv.lock‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL