-
Notifications
You must be signed in to change notification settings - Fork 0
fix(release): pin release tooling to workflow ref so backfilled tags find scripts/release/* #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -151,6 +151,24 @@ jobs: | |||||||||
| ref: ${{ needs.resolve-ref.outputs.sha }} | ||||||||||
| persist-credentials: false | ||||||||||
|
|
||||||||||
| # Release TOOLING is pinned to the WORKFLOW's own ref (github.sha -- | ||||||||||
| # always a commit on main this run executes from), never to the tag | ||||||||||
| # being verified. A tag can predate any scripts/release/* helper this | ||||||||||
| # workflow relies on (that is exactly how the v2.1.0 backfill failed -- | ||||||||||
| # scripts/release/assert_version.py did not exist yet at that tag), so | ||||||||||
| # the helpers are checked out into their own path, separate from the | ||||||||||
| # tag's tree, and invoked from there. The package/code under test stays | ||||||||||
| # pinned to the tag's resolved (and ancestry-verified) sha above -- | ||||||||||
| # only the tooling that INSPECTS that tree floats to main. | ||||||||||
| - name: Check out release tooling from the workflow's own ref | ||||||||||
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | ||||||||||
| with: | ||||||||||
| ref: ${{ github.sha }} | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Pin both tooling checkouts to For a tag-push run,
Proposed fix- ref: ${{ github.sha }}
+ ref: ${{ github.workflow_sha }}📝 Committable suggestion
Suggested change
📍 Affects 1 file
🤖 Prompt for AI AgentsThere was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: For the Prompt for AI agents
Suggested change
|
||||||||||
| sparse-checkout: scripts/release | ||||||||||
| sparse-checkout-cone-mode: false | ||||||||||
| persist-credentials: false | ||||||||||
| path: .release-tooling | ||||||||||
|
|
||||||||||
| - name: Derive version from the resolved tag | ||||||||||
| id: version | ||||||||||
| env: | ||||||||||
|
|
@@ -168,7 +186,7 @@ jobs: | |||||||||
| pip install build twine | ||||||||||
|
|
||||||||||
| - name: Assert tag == pyproject.toml version == wave_sdk.__version__ | ||||||||||
| run: python3 scripts/release/assert_version.py "${{ needs.resolve-ref.outputs.tag }}" | ||||||||||
| run: python3 .release-tooling/scripts/release/assert_version.py "${{ needs.resolve-ref.outputs.tag }}" | ||||||||||
|
|
||||||||||
| - name: pytest (full suite, the checked-out tag) | ||||||||||
| run: python -m pytest -q | ||||||||||
|
|
@@ -203,6 +221,19 @@ jobs: | |||||||||
| ref: ${{ needs.resolve-ref.outputs.sha }} | ||||||||||
| persist-credentials: false | ||||||||||
|
|
||||||||||
| # See the matching step in the `verify` job above: release tooling is | ||||||||||
| # pinned to the workflow's own ref (github.sha), never to the tag being | ||||||||||
| # published, so a backfilled tag from before scripts/release/* existed | ||||||||||
| # still finds the helper it needs. | ||||||||||
| - name: Check out release tooling from the workflow's own ref | ||||||||||
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | ||||||||||
| with: | ||||||||||
| ref: ${{ github.sha }} | ||||||||||
| sparse-checkout: scripts/release | ||||||||||
| sparse-checkout-cone-mode: false | ||||||||||
| persist-credentials: false | ||||||||||
| path: .release-tooling | ||||||||||
|
|
||||||||||
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | ||||||||||
| with: | ||||||||||
| python-version: "3.12" | ||||||||||
|
|
@@ -216,7 +247,7 @@ jobs: | |||||||||
| id: pypi_check | ||||||||||
| run: | | ||||||||||
| set -e | ||||||||||
| RESULT=$(python3 scripts/release/pypi_version_exists.py "${{ needs.verify.outputs.version }}") | ||||||||||
| RESULT=$(python3 .release-tooling/scripts/release/pypi_version_exists.py "${{ needs.verify.outputs.version }}") | ||||||||||
| echo "exists=$RESULT" >> "$GITHUB_OUTPUT" | ||||||||||
|
|
||||||||||
| - name: Publish to PyPI (Trusted Publishing, OIDC, PEP 740 attestations) | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,27 +8,72 @@ | |||||||||||||||||||||||||||||
| and it is intentionally a plain script (not inlined YAML) so it can be run | ||||||||||||||||||||||||||||||
| and unit-tested locally without pushing a tag first. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Usage: python3 scripts/release/assert_version.py v2.1.0 | ||||||||||||||||||||||||||||||
| Exit 0 if it matches, 1 with a clear message if it does not. | ||||||||||||||||||||||||||||||
| `--repo-root` (default: the current working directory, matching the pattern | ||||||||||||||||||||||||||||||
| already used by scripts/release/check_drift.py) is where pyproject.toml and | ||||||||||||||||||||||||||||||
| the `wave_sdk` package are read from -- deliberately NOT derived from this | ||||||||||||||||||||||||||||||
| script's own file location (`Path(__file__)`). `release.yml` checks out the | ||||||||||||||||||||||||||||||
| release TOOLING (this script) from the workflow's own ref into a separate | ||||||||||||||||||||||||||||||
| `.release-tooling/` path, while the CODE being asserted about is checked out | ||||||||||||||||||||||||||||||
| from the (possibly much older) tag being released -- the two trees are not | ||||||||||||||||||||||||||||||
| siblings once the tooling floats ahead of a backfilled tag, so this script | ||||||||||||||||||||||||||||||
| must never assume "my own directory" is anywhere near the code it inspects. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Usage: | ||||||||||||||||||||||||||||||
| python3 scripts/release/assert_version.py v2.1.0 | ||||||||||||||||||||||||||||||
| python3 .release-tooling/scripts/release/assert_version.py v2.1.0 --repo-root . | ||||||||||||||||||||||||||||||
| python3 scripts/release/assert_version.py v2.1.0 --repo-root /path/to/other/checkout | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Exit 0 if it matches, 1 with a clear message if it does not, 2 on bad usage. | ||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import argparse | ||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import tomllib | ||||||||||||||||||||||||||||||
| try: # tomllib is stdlib from 3.11; `tomli` is a dev dependency below that | ||||||||||||||||||||||||||||||
| # (this repo's pyproject.toml pins `tomli>=2.0.0; python_version < '3.11'`, | ||||||||||||||||||||||||||||||
| # matching the same fallback already used in tests/test_packaging.py and | ||||||||||||||||||||||||||||||
| # scripts/release/check_drift.py -- requires-python here is >=3.9, and the | ||||||||||||||||||||||||||||||
| # `pytest (py3.9)` CI matrix leg runs this script as a subprocess). | ||||||||||||||||||||||||||||||
| import tomllib | ||||||||||||||||||||||||||||||
| except ModuleNotFoundError: # pragma: no cover - exercised on 3.9/3.10 only | ||||||||||||||||||||||||||||||
| import tomli as tomllib | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| def main(argv: list[str]) -> int: | ||||||||||||||||||||||||||||||
| if len(argv) != 2: | ||||||||||||||||||||||||||||||
| print(f"usage: {argv[0]} <tag e.g. v2.1.0>", file=sys.stderr) | ||||||||||||||||||||||||||||||
| return 2 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| tag = argv[1] | ||||||||||||||||||||||||||||||
| parser = argparse.ArgumentParser( | ||||||||||||||||||||||||||||||
| prog=Path(argv[0]).name if argv else "assert_version.py", | ||||||||||||||||||||||||||||||
| description=__doc__, | ||||||||||||||||||||||||||||||
| formatter_class=argparse.RawDescriptionHelpFormatter, | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| parser.add_argument("tag", help="tag to check, e.g. v2.1.0") | ||||||||||||||||||||||||||||||
| parser.add_argument( | ||||||||||||||||||||||||||||||
| "--repo-root", | ||||||||||||||||||||||||||||||
| default=".", | ||||||||||||||||||||||||||||||
| help=( | ||||||||||||||||||||||||||||||
| "path to the sdk-python checkout whose pyproject.toml/wave_sdk this " | ||||||||||||||||||||||||||||||
| "validates (default: current working directory -- NOT this script's " | ||||||||||||||||||||||||||||||
| "own file location, which may live in a separate sparse checkout " | ||||||||||||||||||||||||||||||
| "pinned to a different ref than the code being verified)" | ||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||
| args = parser.parse_args(argv[1:]) | ||||||||||||||||||||||||||||||
| except SystemExit as exc: | ||||||||||||||||||||||||||||||
| # argparse already printed a usage message; normalize the exit code | ||||||||||||||||||||||||||||||
| # to this script's documented "bad usage" code (2) either way. | ||||||||||||||||||||||||||||||
| return exc.code if isinstance(exc.code, int) else 2 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| tag = args.tag | ||||||||||||||||||||||||||||||
| tag_version = tag[1:] if tag.startswith("v") else tag | ||||||||||||||||||||||||||||||
| repo_root = Path(__file__).resolve().parents[2] | ||||||||||||||||||||||||||||||
| repo_root = Path(args.repo_root).resolve() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| pyproject_path = repo_root / "pyproject.toml" | ||||||||||||||||||||||||||||||
| if not pyproject_path.is_file(): | ||||||||||||||||||||||||||||||
| print(f"error: {pyproject_path} does not exist -- wrong --repo-root?", file=sys.stderr) | ||||||||||||||||||||||||||||||
| return 2 | ||||||||||||||||||||||||||||||
|
Comment on lines
+74
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||
| data = tomllib.loads(pyproject_path.read_text()) | ||||||||||||||||||||||||||||||
| pyproject_version = data["project"]["version"] | ||||||||||||||||||||||||||||||
|
Comment on lines
+74
to
78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Handle malformed TOML and missing Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -37,6 +82,7 @@ def main(argv: list[str]) -> int: | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| dunder_version = wave_sdk.__version__ | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| print(f"repo root : {repo_root}") | ||||||||||||||||||||||||||||||
| print(f"tag : {tag} (version {tag_version})") | ||||||||||||||||||||||||||||||
| print(f"pyproject.toml : {pyproject_version}") | ||||||||||||||||||||||||||||||
| print(f"wave_sdk.__version__ : {dunder_version}") | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| """Unit tests for scripts/release/*.py -- run as subprocesses so a stale | ||
| `import wave_sdk` from this test process never leaks into (or masks a bug in) | ||
| the script's own import. | ||
|
|
||
| The specific regression under test: `.github/workflows/release.yml` checks | ||
| out release TOOLING (this script) from the workflow's own ref into a | ||
| separate `.release-tooling/` directory, while the CODE it inspects is | ||
| checked out from a (possibly much older) release tag into the workspace | ||
| root. `scripts/release/assert_version.py` must resolve `pyproject.toml` and | ||
| import `wave_sdk` relative to `--repo-root` (default: cwd) -- NEVER relative | ||
| to its own file location (`Path(__file__)`) -- or it silently breaks the | ||
| moment it is invoked from anywhere other than the tree it is meant to | ||
| inspect. This is exactly how the v2.1.0 backfill failed: the tag's tree | ||
| predated scripts/release/ entirely, so the workflow's `verify` job could not | ||
| even find the script at the in-tree path, let alone run it against the | ||
| wrong tree. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| import subprocess | ||
| import sys | ||
| import textwrap | ||
| from pathlib import Path | ||
|
|
||
| REPO_ROOT = Path(__file__).resolve().parent.parent | ||
| ASSERT_VERSION = REPO_ROOT / "scripts" / "release" / "assert_version.py" | ||
|
|
||
|
|
||
| def _write_fake_checkout(tmp_path: Path, version: str) -> Path: | ||
| """Build a minimal standalone checkout with its own pyproject.toml + wave_sdk.""" | ||
| checkout = tmp_path / "fake-checkout" | ||
| (checkout / "wave_sdk").mkdir(parents=True) | ||
| (checkout / "pyproject.toml").write_text( | ||
| textwrap.dedent( | ||
| f"""\ | ||
| [project] | ||
| name = "wave-sdk" | ||
| version = "{version}" | ||
| """ | ||
| ) | ||
| ) | ||
| (checkout / "wave_sdk" / "__init__.py").write_text(f'__version__ = "{version}"\n') | ||
| return checkout | ||
|
|
||
|
|
||
| def _run_assert_version(*args: str, cwd: Path) -> subprocess.CompletedProcess: | ||
| return subprocess.run( | ||
| [sys.executable, str(ASSERT_VERSION), *args], | ||
| cwd=cwd, | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30, | ||
| ) | ||
|
|
||
|
|
||
| def test_resolves_repo_root_from_cwd_not_from_its_own_file_location(tmp_path): | ||
| """The core regression guard: invoke the script from a directory that is | ||
| NOT anywhere near its own file location (mirrors `.release-tooling/scripts/ | ||
| release/assert_version.py` being run against an unrelated tag checkout), | ||
| relying only on cwd defaulting `--repo-root`. | ||
| """ | ||
| checkout = _write_fake_checkout(tmp_path, "9.9.9") | ||
|
|
||
| result = _run_assert_version("v9.9.9", cwd=checkout) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: This new subprocess test runs on Python 3.9, where the script's direct Assessment: 🟠 Prompt for AI Agent 🤖This is a comment left during a code review.
**Path:** tests/test_release_scripts.py
**Line:** 64:64
**Comment:**
*Import Error: This new subprocess test runs on Python 3.9, where the script's direct `tomllib` import raises `ModuleNotFoundError` despite the project supporting that version.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix |
||
|
|
||
| assert result.returncode == 0, result.stderr | ||
| assert "OK: tag, pyproject.toml, and wave_sdk.__version__ all agree" in result.stdout | ||
| assert str(checkout) in result.stdout # confirms it read the fake checkout, not the real repo | ||
|
|
||
|
|
||
| def test_explicit_repo_root_overrides_cwd(tmp_path): | ||
| """`--repo-root` must work even when invoked from a completely different cwd | ||
| (e.g. a workflow step whose default working-directory is the tag checkout, | ||
| but the tooling script lives under `.release-tooling/`).""" | ||
| checkout = _write_fake_checkout(tmp_path, "1.2.3") | ||
|
|
||
| result = _run_assert_version("v1.2.3", "--repo-root", str(checkout), cwd=tmp_path) | ||
|
|
||
| assert result.returncode == 0, result.stderr | ||
| assert "OK: tag, pyproject.toml, and wave_sdk.__version__ all agree" in result.stdout | ||
|
|
||
|
|
||
| def test_fails_loud_on_version_mismatch(tmp_path): | ||
| checkout = _write_fake_checkout(tmp_path, "1.0.0") | ||
|
|
||
| result = _run_assert_version("v2.0.0", cwd=checkout) | ||
|
|
||
| assert result.returncode == 1 | ||
| assert "VERSION MISMATCH" in result.stderr | ||
| assert "tag 2.0.0 != pyproject.toml 1.0.0" in result.stderr | ||
|
|
||
|
|
||
| def test_fails_with_usage_code_on_missing_tag_argument(tmp_path): | ||
| checkout = _write_fake_checkout(tmp_path, "1.0.0") | ||
|
|
||
| result = _run_assert_version(cwd=checkout) | ||
|
|
||
| assert result.returncode == 2 | ||
|
|
||
|
|
||
| def test_fails_clearly_on_wrong_repo_root(tmp_path): | ||
| """A --repo-root that doesn't contain pyproject.toml must fail loud (exit 2), | ||
| not crash with an unhandled traceback or silently read the wrong tree.""" | ||
| empty_dir = tmp_path / "empty" | ||
| empty_dir.mkdir() | ||
|
|
||
| result = _run_assert_version("v1.0.0", "--repo-root", str(empty_dir), cwd=tmp_path) | ||
|
|
||
| assert result.returncode == 2 | ||
| assert "does not exist" in result.stderr | ||
|
|
||
|
|
||
| def test_real_repo_checkout_passes_when_invoked_from_a_different_cwd(tmp_path): | ||
| """Regression check against the ACTUAL repo: running the script with the | ||
| real repo root passed via --repo-root, from an unrelated cwd, must still | ||
| resolve pyproject.toml/wave_sdk from --repo-root, not from cwd or from | ||
| the script's own directory. | ||
| """ | ||
| result = _run_assert_version( | ||
| f"v{_current_repo_version()}", | ||
| "--repo-root", | ||
| str(REPO_ROOT), | ||
| cwd=tmp_path, | ||
| ) | ||
|
|
||
| assert result.returncode == 0, result.stderr | ||
|
|
||
|
|
||
| def _current_repo_version() -> str: | ||
| try: | ||
| import tomllib | ||
| except ModuleNotFoundError: # pragma: no cover - exercised on 3.9/3.10 only | ||
| import tomli as tomllib | ||
|
|
||
| data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text()) | ||
| return data["project"]["version"] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion:
github.shais the tag commit on tag-push runs, so backfilled tags predatingscripts/releasestill produce an empty tooling checkout and fail. [logic error]Assessment: 🟠
Major· 🔁Occurrence: SometimesPrompt for AI Agent 🤖