diff --git a/README.md b/README.md index 8bf6a02..0084053 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,11 @@ caller's `pylock.toml`. `nab-git-commit` is a temporary input of `refresh-release-lock`, `validate-news`, `check-tagged-source`, `prepare-towncrier-release.yaml`, `upgrade-release-tools.yaml`, and `validate-python-release.yaml`. It is empty by -default. When set, it must be a full 40-character commit SHA, and Nab is installed from -`git+https://github.com/notatallshaw/nab@` instead of the release. That commit gets no cooldown: `exclude-newer` -applies only to its dependencies. It exists until a Nab release carries the needed fix and will be removed then. +default. When set, it must be a full 40-character commit SHA, and the whole Nab workspace is installed from +`git+https://github.com/notatallshaw/nab@` instead of the release: `nab` from the root, and `nab-index`, +`nab-markersets`, `nab-project`, `nab-provider`, and `nab-resolver` from their `#subdirectory=` paths, because the root +pins them to an unreleased version. That commit gets no cooldown: `exclude-newer` applies only to its dependencies. It +exists until a Nab release carries the needed fix and will be removed then. ## Check pull request news @@ -178,10 +180,12 @@ configurations, which no guard checks. `prepare-towncrier` and `validate-release anywhere, a `.pre-commit-config.yml`, and a `.pre-commit-config.yaml` below the root before they run `prek`. The shared prepare and tagged validator run `refresh-release-lock` in `check` mode before release checks or publishing. -`check-tagged-source` bundles the tagged checks for a custom tag publisher: it requires `GITHUB_REF` to be the tag of -the checkout on `main` and checks the locks. It runs no hooks. It takes the secondary lock inputs below and needs a -full-history checkout. For a secondary Nab lock, pass its path, selected groups, selection type, and matrix inputs to -both workflows. A stale primary or secondary lock stops the release before artifacts are built. +`check-tagged-source` bundles the tagged checks for a custom tag publisher: on a tag ref it requires the tag to name +both `GITHUB_SHA` and the checkout, on `main`, and checks the locks. Any other event, such as a `workflow_dispatch` from +`main` with a `tag` input, must check out the commit that `inputs.tag` names, on `main`. It runs no hooks. It takes the +secondary lock inputs below and needs a full-history checkout. For a secondary Nab lock, pass its path, selected groups, +selection type, and matrix inputs to both workflows. A stale primary or secondary lock stops the release before +artifacts are built. For example, a tagged source job that also publishes from a PGO lock checks both files before installing build tools: diff --git a/changelog.d/+dispatch-tag-check.bugfix.md b/changelog.d/+dispatch-tag-check.bugfix.md new file mode 100644 index 0000000..26536b4 --- /dev/null +++ b/changelog.d/+dispatch-tag-check.bugfix.md @@ -0,0 +1,3 @@ +Let `check-tagged-source` accept a `workflow_dispatch` or other non-tag run, such as one from `main` with a `tag` input, +when the checkout is the commit that tag names and that commit is on `main`. Tag runs still require the tag, +`GITHUB_SHA`, and the checkout to match. diff --git a/changelog.d/+nab-workspace.bugfix.md b/changelog.d/+nab-workspace.bugfix.md new file mode 100644 index 0000000..0413461 --- /dev/null +++ b/changelog.d/+nab-workspace.bugfix.md @@ -0,0 +1,2 @@ +Install the whole Nab workspace from the `nab-git-commit` commit: `nab` and its `nab-index`, `nab-markersets`, +`nab-project`, `nab-provider`, and `nab-resolver` sub-packages, which the root pins to an unreleased version. diff --git a/check-tagged-source/action.yml b/check-tagged-source/action.yml index 234505d..b2ad79d 100644 --- a/check-tagged-source/action.yml +++ b/check-tagged-source/action.yml @@ -34,13 +34,18 @@ runs: - name: Verify the exact release tag shell: bash env: - TAG: ${{ github.ref_name }} + TAG: ${{ github.event.inputs.tag || github.ref_name }} run: | set -euo pipefail - test "$GITHUB_REF" = "refs/tags/$TAG" git check-ref-format "refs/tags/$TAG" - test "$(git rev-parse "refs/tags/$TAG^{commit}")" = "$GITHUB_SHA" - test "$GITHUB_SHA" = "$(git rev-parse HEAD)" + commit=$(git rev-parse "refs/tags/$TAG^{commit}") + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + test "$GITHUB_REF" = "refs/tags/$TAG" + test "$commit" = "$GITHUB_SHA" + test "$GITHUB_SHA" = "$(git rev-parse HEAD)" + else + test "$commit" = "$(git rev-parse HEAD)" + fi git merge-base --is-ancestor HEAD origin/main - uses: $/refresh-release-lock with: diff --git a/refresh-release-lock/action.yml b/refresh-release-lock/action.yml index 58c3927..bd47065 100644 --- a/refresh-release-lock/action.yml +++ b/refresh-release-lock/action.yml @@ -68,9 +68,15 @@ runs: NAB_GIT_COMMIT: ${{ inputs.nab-git-commit }} run: | set -euo pipefail - requirement="nab>=0.0.22" - if [[ -n "$NAB_GIT_COMMIT" ]]; then requirement=git+https://github.com/notatallshaw/nab@$NAB_GIT_COMMIT; fi - "$GITHUB_ACTION_PATH/../tools/install_live_tools.sh" "$RUNNER_TEMP/lock-tools" "$requirement" + requirements=("nab>=0.0.22") + if [[ -n "$NAB_GIT_COMMIT" ]]; then + source=git+https://github.com/notatallshaw/nab@$NAB_GIT_COMMIT + requirements=("nab @ $source") + for package in nab-index nab-markersets nab-project nab-provider nab-resolver; do + requirements+=("$package @ $source#subdirectory=$package") + done + fi + "$GITHUB_ACTION_PATH/../tools/install_live_tools.sh" "$RUNNER_TEMP/lock-tools" "${requirements[@]}" - name: Resolve all project groups and extras shell: bash env: diff --git a/tests/refresh_release_lock/test_refresh.py b/tests/refresh_release_lock/test_refresh.py index 3decc32..436e14c 100644 --- a/tests/refresh_release_lock/test_refresh.py +++ b/tests/refresh_release_lock/test_refresh.py @@ -225,15 +225,30 @@ def test_refresh_rejects_invalid_nab_commit(tmp_path: Path, action_scripts: Path assert "nab-git-commit must be a full 40-character commit SHA" in result.stderr +_NAB_COMMIT: Final = "0123456789abcdef0123456789abcdef01234567" +_NAB_SOURCE: Final = f"git+https://github.com/notatallshaw/nab@{_NAB_COMMIT}" + + @pytest.mark.parametrize( - ("commit", "requirement"), + ("commit", "requirements"), [ - pytest.param("", "nab>=0.0.22", id="release"), - pytest.param("0123456789abcdef0123456789abcdef01234567", None, id="git-commit"), + pytest.param("", ["nab>=0.0.22"], id="release"), + pytest.param( + _NAB_COMMIT, + [ + f"nab @ {_NAB_SOURCE}", + f"nab-index @ {_NAB_SOURCE}#subdirectory=nab-index", + f"nab-markersets @ {_NAB_SOURCE}#subdirectory=nab-markersets", + f"nab-project @ {_NAB_SOURCE}#subdirectory=nab-project", + f"nab-provider @ {_NAB_SOURCE}#subdirectory=nab-provider", + f"nab-resolver @ {_NAB_SOURCE}#subdirectory=nab-resolver", + ], + id="git-commit-workspace", + ), ], ) def test_refresh_installs_selected_nab( - tmp_path: Path, action_scripts: Path, commit: str, requirement: str | None + tmp_path: Path, action_scripts: Path, commit: str, requirements: list[str] ) -> None: installer = tmp_path / "tools/install_live_tools.sh" installer.parent.mkdir() @@ -251,10 +266,7 @@ def test_refresh_installs_selected_nab( ) assert result.returncode == 0, result.stderr - assert (tmp_path / "installed").read_text().splitlines() == [ - f"{tmp_path}/lock-tools", - requirement or f"git+https://github.com/notatallshaw/nab@{commit}", - ] + assert (tmp_path / "installed").read_text().splitlines() == [f"{tmp_path}/lock-tools", *requirements] @pytest.mark.parametrize( diff --git a/tests/validate_python_release/test_tagged.py b/tests/validate_python_release/test_tagged.py index a325176..c04dddf 100644 --- a/tests/validate_python_release/test_tagged.py +++ b/tests/validate_python_release/test_tagged.py @@ -120,7 +120,6 @@ def test_validator_rejects_backend_hash_mismatch( [ pytest.param("lightweight", "refs/tags/0.2.0", True, id="lightweight-tag"), pytest.param("annotated", "refs/tags/0.2.0", True, id="annotated-tag"), - pytest.param("lightweight", "refs/heads/0.2.0", False, id="branch-event-on-tagged-commit"), pytest.param("branch", "refs/tags/0.2.0", False, id="same-name-branch"), pytest.param("off-main", "refs/tags/0.2.0", False, id="tagged-off-main-commit"), ], @@ -163,6 +162,55 @@ def test_validator_requires_exact_tag( assert (result.returncode == 0) is accepted, result.stderr +@pytest.mark.parametrize( + ("location", "accepted"), + [ + pytest.param("check-tagged-source/action.yml", True, id="source-check"), + pytest.param("validate-python-release:build", False, id="build"), + pytest.param("release:source", False, id="self-release"), + ], +) +def test_validator_branch_event_on_tagged_commit(repository: Path, location: str, *, accepted: bool) -> None: + run(repository, "git", "tag", "0.2.0") + + result = _run_tag_check(repository, location, "refs/heads/main", run(repository, "git", "rev-parse", "HEAD").stdout) + + assert (result.returncode == 0) is accepted, result.stderr + + +def test_source_check_rejects_dispatch_off_the_tag(repository: Path) -> None: + run(repository, "git", "tag", "0.2.0") + run(repository, "git", "commit", "--allow-empty", "-m", "Later change") + run(repository, "git", "push", "origin", "main") + + result = _run_tag_check( + repository, + "check-tagged-source/action.yml", + "refs/heads/main", + run(repository, "git", "rev-parse", "HEAD").stdout, + ) + + assert result.returncode != 0 + + +def test_source_check_reads_the_dispatched_tag() -> None: + assert ( + _tag_check("check-tagged-source/action.yml")["env"]["TAG"] + == "${{ github.event.inputs.tag || github.ref_name }}" + ) + + +def _run_tag_check(repository: Path, location: str, event_ref: str, event_sha: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["bash", "-c", _tag_check(location)["run"]], + cwd=repository, + env=os.environ | {"GITHUB_REF": event_ref, "TAG": "0.2.0", "GITHUB_SHA": event_sha.strip()}, + text=True, + capture_output=True, + check=False, + ) + + @pytest.mark.parametrize( ("tag", "filenames", "accepted"), [ @@ -225,11 +273,11 @@ def test_validator_requires_tag_at_the_event_commit(repository: Path, location: assert result.returncode != 0 -def _tag_check(location: str) -> dict[str, str]: +def _tag_check(location: str) -> dict[str, Any]: root = Path(__file__).resolve().parents[2] if location.endswith(".yml"): steps = yaml.safe_load((root / location).read_text())["runs"]["steps"] else: workflow, job = location.split(":") steps = yaml.safe_load((root / ".github/workflows" / f"{workflow}.yaml").read_text())["jobs"][job]["steps"] - return cast("dict[str, str]", next(step for step in steps if step.get("name") == "Verify the exact release tag")) + return cast("dict[str, Any]", next(step for step in steps if step.get("name") == "Verify the exact release tag"))