Files
github-action/tests/test_release.py
Tirsvad 27abd226c4
TirSystem/github-action: Publish wiki / publish-wiki (push) Skipped
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 15s
Add the release workflow and its script
Task: MIL-004#5
2026-10-10 23:01:30 +08:00

185 lines
5.8 KiB
Python

"""Offline tests for tools/release.py and .gitea/workflows/release.yml."""
import pytest
import yaml
from workflow_source import REPO_ROOT, load_script
release = load_script(REPO_ROOT / "tools" / "release.py", "release")
WORKFLOW = REPO_ROOT / ".gitea" / "workflows" / "release.yml"
SHA = "a" * 40
CONTEXT = "Validate workflows / validate (push)"
CHANGELOG = """# Changelog
## [Unreleased]
### Added
- Something later.
## [1.1.0] - 2026-11-01
### Added
- A workflow.
## [1.0.0] - 2026-10-20
### Added
- First release.
"""
class FakeGitea:
"""Answers the calls of the release script and records the writes."""
def __init__(self, tip=SHA, status="success", tags=(), context=CONTEXT):
self.tip = tip
self.status = status
self.tags = list(tags)
self.context = context
self.writes = []
def __call__(self, url, method="GET", headers=None, body=None):
path = url.split("?")[0]
if method != "GET":
self.writes.append((method, path.rsplit("/repos/o/r", 1)[-1], body))
return None
if path.endswith("/branches/main"):
return {"commit": {"id": self.tip}}
if path.endswith(f"/commits/{SHA}/status"):
if self.status is None:
return {"statuses": []}
return {"statuses": [{"context": self.context, "status": self.status}]}
if path.endswith("/tags"):
return [{"name": name} for name in self.tags] if "page=1" in url else []
raise AssertionError(f"unexpected request {method} {url}")
@pytest.fixture
def env(tmp_path, monkeypatch):
changelog = tmp_path / "CHANGELOG.md"
changelog.write_text(CHANGELOG, encoding="utf-8")
values = {
"GITEA_API_URL": "https://git.example/api/v1",
"SOURCE_REPOSITORY": "o/r",
"RELEASE_CREDENTIALS": "t" * 40,
"RELEASE_VERSION": "1.1.0",
"RELEASE_SHA": SHA,
"RELEASE_DRY_RUN": "false",
"CHANGELOG_FILE": str(changelog),
}
for key, value in values.items():
monkeypatch.setenv(key, value)
monkeypatch.delenv("RELEASE_BRANCH", raising=False)
monkeypatch.delenv("VALIDATION_CONTEXT", raising=False)
return monkeypatch
def run(gitea):
return release.main(request=gitea)
def test_green_commit_with_entry_creates_release_and_major_tag(env):
gitea = FakeGitea(tags=["v1.0.0", "v1"])
assert run(gitea) == 0
methods = [(m, p) for m, p, _ in gitea.writes]
assert methods == [("POST", "/releases"), ("DELETE", "/tags/v1"), ("POST", "/tags")]
release_body = gitea.writes[0][2]
assert release_body["tag_name"] == "v1.1.0"
assert release_body["target_commitish"] == SHA
assert "### Added" in release_body["body"] and "A workflow." in release_body["body"]
assert gitea.writes[2][2]["tag_name"] == "v1" and gitea.writes[2][2]["target"] == SHA
def test_first_release_creates_the_major_tag_without_deleting(env):
env.setenv("RELEASE_VERSION", "1.0.0")
gitea = FakeGitea(tags=[])
assert run(gitea) == 0
assert [(m, p) for m, p, _ in gitea.writes] == [("POST", "/releases"), ("POST", "/tags")]
def test_failed_validation_run_creates_no_tag(env):
gitea = FakeGitea(status="failure")
with pytest.raises(release.WorkflowError) as error:
run(gitea)
assert error.value.category == "validation"
assert gitea.writes == []
def test_missing_validation_run_creates_no_tag(env):
gitea = FakeGitea(status=None)
with pytest.raises(release.WorkflowError) as error:
run(gitea)
assert "No validation run" in str(error.value)
assert gitea.writes == []
def test_commit_that_is_not_the_tip_is_refused(env):
gitea = FakeGitea(tip="b" * 40)
with pytest.raises(release.WorkflowError) as error:
run(gitea)
assert "not the tip" in str(error.value)
assert gitea.writes == []
def test_version_without_changelog_entry_is_refused(env):
env.setenv("RELEASE_VERSION", "1.2.0")
gitea = FakeGitea()
with pytest.raises(release.WorkflowError) as error:
run(gitea)
assert error.value.category == "validation"
assert gitea.writes == []
def test_existing_tag_is_never_moved(env):
gitea = FakeGitea(tags=["v1.1.0", "v1.0.0", "v1"])
with pytest.raises(release.WorkflowError) as error:
run(gitea)
assert "already exists" in str(error.value)
assert gitea.writes == []
def test_older_patch_does_not_move_the_major_tag(env):
env.setenv("RELEASE_VERSION", "1.0.0")
gitea = FakeGitea(tags=["v1.1.0", "v1"])
assert run(gitea) == 0
assert [(m, p) for m, p, _ in gitea.writes] == [("POST", "/releases")]
def test_dry_run_is_the_default_and_writes_nothing(env, capsys):
env.delenv("RELEASE_DRY_RUN")
gitea = FakeGitea(tags=["v1.0.0", "v1"])
assert run(gitea) == 0
assert gitea.writes == []
assert "Dry run" in capsys.readouterr().out
@pytest.mark.parametrize("version", ["v1.0.0", "1.0", "1.0.0-rc1", ""])
def test_malformed_version_is_a_configuration_error(env, version):
env.setenv("RELEASE_VERSION", version)
with pytest.raises(release.WorkflowError) as error:
run(FakeGitea())
assert error.value.category == "configuration"
def test_missing_token_is_a_configuration_error(env):
env.setenv("RELEASE_CREDENTIALS", " ")
with pytest.raises(release.WorkflowError) as error:
run(FakeGitea())
assert error.value.category == "configuration"
def test_workflow_runs_the_file_and_defaults_to_a_dry_run():
data = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))
triggers = data.get("on", data.get(True))
inputs = triggers["workflow_dispatch"]["inputs"]
assert inputs["dry_run"]["default"] == "true"
job = data["jobs"]["release"]
assert job["permissions"] == {"contents": "write"}
runs = [step.get("run", "") for step in job["steps"]]
assert "python3 tools/release.py" in runs