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
1 change: 1 addition & 0 deletions .github/scripts/tests/test_cua_driver_release_wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def test_release_please_owns_driver_and_lume(self) -> None:
self.assertIn('"component": "cua-driver-rs"', config)
self.assertIn('"component": "lume"', config)
self.assertIn("5c625bfb5d1ff62eadeeb3772007f7f66fdcf071", workflow)
self.assertIn("validate_release_please_tags.py --target HEAD", workflow)
self.assertIn('-p cua-driver --precise "$DRIVER_VERSION"', workflow)
self.assertIn(
"gh pr list --state open --base main --limit 100 --json number",
Expand Down
83 changes: 83 additions & 0 deletions .github/scripts/tests/test_validate_release_please_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from __future__ import annotations

import importlib.util
from pathlib import Path
import subprocess

import pytest

SCRIPT_PATH = Path(__file__).resolve().parents[1] / "validate_release_please_tags.py"
SPEC = importlib.util.spec_from_file_location("validate_release_please_tags", SCRIPT_PATH)
assert SPEC is not None and SPEC.loader is not None
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
TagValidationError = MODULE.TagValidationError
package_tag = MODULE.package_tag
validate_tags = MODULE.validate_tags


CONFIG = {
"include-component-in-tag": True,
"include-v-in-tag": True,
"tag-separator": "-",
"packages": {
"libs/cua-driver": {
"component": "cua-driver-rs",
"release-type": "simple",
}
},
}
MANIFEST = {"libs/cua-driver": "0.18.0"}


def run_git(repo: Path, *args: str) -> str:
return subprocess.run(
["git", *args], cwd=repo, check=True, capture_output=True, text=True
).stdout.strip()


@pytest.fixture
def repo(tmp_path: Path) -> Path:
run_git(tmp_path, "init", "-b", "main")
run_git(tmp_path, "config", "user.name", "Release Test")
run_git(tmp_path, "config", "user.email", "release-test@example.com")
(tmp_path / "file").write_text("first\n")
run_git(tmp_path, "add", "file")
run_git(tmp_path, "commit", "-m", "first")
return tmp_path


def test_package_tag_uses_manifest_tag_options() -> None:
assert package_tag(CONFIG, CONFIG["packages"]["libs/cua-driver"], "0.18.0") == (
"cua-driver-rs-v0.18.0"
)


def test_allows_missing_tag_for_newly_merged_release(repo: Path) -> None:
assert validate_tags(repo_root=repo, config=CONFIG, manifest=MANIFEST, target="HEAD") == []


def test_accepts_manifest_tag_on_target_branch(repo: Path) -> None:
run_git(repo, "tag", "cua-driver-rs-v0.18.0")
(repo / "file").write_text("second\n")
run_git(repo, "commit", "-am", "second")

assert validate_tags(repo_root=repo, config=CONFIG, manifest=MANIFEST, target="HEAD") == [
"cua-driver-rs-v0.18.0"
]


def test_rejects_manifest_tag_outside_target_branch(repo: Path) -> None:
base = run_git(repo, "rev-parse", "HEAD")
run_git(repo, "switch", "-c", "release-metadata")
(repo / "metadata").write_text("notes\n")
run_git(repo, "add", "metadata")
run_git(repo, "commit", "-m", "metadata")
run_git(repo, "tag", "cua-driver-rs-v0.18.0")
run_git(repo, "switch", "main")
(repo / "file").write_text("main\n")
run_git(repo, "commit", "-am", "main")
assert run_git(repo, "merge-base", "HEAD", "release-metadata") == base

with pytest.raises(TagValidationError, match="is not an ancestor"):
validate_tags(repo_root=repo, config=CONFIG, manifest=MANIFEST, target="HEAD")
111 changes: 111 additions & 0 deletions .github/scripts/validate_release_please_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""Reject Release Please manifest tags that are not on the target branch."""

from __future__ import annotations

import argparse
import json
from pathlib import Path
import subprocess
from typing import Any, Mapping, Sequence


class TagValidationError(RuntimeError):
"""A released manifest version has an invalid Git tag."""


def package_tag(config: Mapping[str, Any], package: Mapping[str, Any], version: str) -> str:
def option(name: str, default: Any) -> Any:
return package.get(name, config.get(name, default))

component = package.get("component", package.get("package-name"))
if option("include-component-in-tag", False) and not component:
raise TagValidationError("component tag requested without a component or package name")

prefix = (
str(component) + str(option("tag-separator", "-"))
if option("include-component-in-tag", False)
else ""
)
version_prefix = "v" if option("include-v-in-tag", True) else ""
return f"{prefix}{version_prefix}{version}"


def git(*args: str, cwd: Path, check: bool = True) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", *args],
cwd=cwd,
check=check,
capture_output=True,
text=True,
)


def validate_tags(
*,
repo_root: Path,
config: Mapping[str, Any],
manifest: Mapping[str, Any],
target: str,
) -> list[str]:
packages = config.get("packages")
if not isinstance(packages, Mapping):
raise TagValidationError("release config must contain a packages object")

checked: list[str] = []
for path, package in packages.items():
if path not in manifest:
raise TagValidationError(f"release manifest does not contain {path!r}")
if not isinstance(package, Mapping):
raise TagValidationError(f"release package config for {path!r} must be an object")

tag = package_tag(config, package, str(manifest[path]))
tag_ref = f"refs/tags/{tag}^{{commit}}"
if git("rev-parse", "--verify", "--quiet", tag_ref, cwd=repo_root, check=False).returncode:
# Immediately after a release PR merges, its new manifest version has
# no tag yet. Release Please must be allowed to create that tag.
continue
if git(
"merge-base", "--is-ancestor", tag_ref, target, cwd=repo_root, check=False
).returncode:
tag_sha = git("rev-parse", tag_ref, cwd=repo_root).stdout.strip()
target_sha = git("rev-parse", target, cwd=repo_root).stdout.strip()
raise TagValidationError(
f"manifest tag {tag} ({tag_sha}) for {path} is not an ancestor of "
f"{target} ({target_sha}); refusing to generate release history"
)
checked.append(tag)
return checked


def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path.cwd())
parser.add_argument("--config", type=Path, default=Path("release-please-config.json"))
parser.add_argument("--manifest", type=Path, default=Path(".release-please-manifest.json"))
parser.add_argument("--target", default="HEAD")
args = parser.parse_args(argv)

try:
repo_root = args.repo_root.resolve()
config_path = args.config if args.config.is_absolute() else repo_root / args.config
manifest_path = args.manifest if args.manifest.is_absolute() else repo_root / args.manifest
checked = validate_tags(
repo_root=repo_root,
config=json.loads(config_path.read_text()),
manifest=json.loads(manifest_path.read_text()),
target=args.target,
)
except (OSError, ValueError, subprocess.CalledProcessError, TagValidationError) as error:
print(f"release tag validation error: {error}")
return 1

if checked:
print(f"release manifest tags are on {args.target}: {', '.join(checked)}")
else:
print("release manifest tags do not exist yet; Release Please may create them")
return 0


if __name__ == "__main__":
raise SystemExit(main())
3 changes: 3 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ jobs:
echo "::error::Select cua-driver-rs or lume when forcing a bump type."
exit 1

- name: Reject manifest tags outside main history
run: python3 .github/scripts/validate_release_please_tags.py --target HEAD

- name: Resolve targeted release request
id: request
if: >-
Expand Down
Loading