The release is built on the maintainer's x86_64 Void host, not in CI. The bongbetic-ci runner is ARM64 and cannot build the glibc x86_64 package. A tag push no longer starts the xbps workflow: it is manual dispatch only, and its header says why. - build-release.sh release refuses an uncommitted or untracked checkout before any container starts. It writes BUILT_FROM (the tag and the commit it was built from), which nothing recorded before. - publish-release.sh TAG --out-dir DIR publishes a built out-dir to the Gitea release API in the workflow's order. It refuses unless origin's tag peels to the BUILT_FROM commit, the sdist digest matches, and the client key is the committed one. --dry-run runs every check and prints the planned calls without sending one. The token comes from GITEA_TOKEN or the tea login and is never printed. - Tests run both scripts offline against a local bare origin and a stateful Gitea stand-in. - docs/RELEASE_PROCESS.md and packaging/void/README.md describe the local sequence: merge, annotated tag by ref, clean build, dry-run, publish, checksum.
577 lines
23 KiB
Python
577 lines
23 KiB
Python
"""Offline guards for publishing a built Void release (spec #16; packaging/void/publish-release.sh).
|
|
|
|
``publish-release.sh`` publishes an out-dir that ``build-release.sh release``
|
|
already built and verified. These tests run the real script in a scratch git
|
|
checkout whose ``origin`` is a local bare repository, so the tag gate and the
|
|
``ls-remote`` comparison are exercised for real. The Gitea API is the stateful
|
|
``curl`` stand-in from ``test_xbps_release_workflow.py`` (``FakeGitea``): every
|
|
request, its order and its authentication are recorded, and nothing leaves the
|
|
machine. The release itself is never run.
|
|
|
|
The expected values come from the documented rules (the tag must be on origin
|
|
at the built commit, the repodata goes last, the token never leaves the header
|
|
file), not from the script.
|
|
"""
|
|
|
|
import hashlib
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
import pytest
|
|
from test_xbps_release_workflow import FakeGitea
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
VOID = REPO_ROOT / "packaging" / "void"
|
|
BUILD_RELEASE = VOID / "build-release.sh"
|
|
PUBLISH_RELEASE = VOID / "publish-release.sh"
|
|
SMOKE_SH = VOID / "smoke.sh"
|
|
PINS = VOID / "tool_checksums.txt"
|
|
PLIST = VOID / "repository-key.plist"
|
|
TEMPLATE = VOID / "srcpkgs" / "voculinux" / "template"
|
|
|
|
TAG = "v0.19.0"
|
|
XBPS = "0.19.0"
|
|
SDIST = "voculinux-0.19.0.tar.gz"
|
|
HOST = "https://git.bongbetic.com"
|
|
API = f"{HOST}/api/v1/repos/xavierk/voculinux"
|
|
TOKEN = "gitea-token-from-the-environment"
|
|
TEA_TOKEN = "tea-token-from-the-config-file"
|
|
FINGERPRINT = "fa:d1:00:32:ce:b1:b9:fd:73:05:05:76:75:54:37:25"
|
|
|
|
GIT_ENV = {
|
|
"GIT_CONFIG_GLOBAL": os.devnull,
|
|
"GIT_CONFIG_NOSYSTEM": "1",
|
|
"GIT_AUTHOR_NAME": "fixture",
|
|
"GIT_AUTHOR_EMAIL": "fixture@example.invalid",
|
|
"GIT_COMMITTER_NAME": "fixture",
|
|
"GIT_COMMITTER_EMAIL": "fixture@example.invalid",
|
|
"GIT_TERMINAL_PROMPT": "0",
|
|
}
|
|
|
|
|
|
def _git(cwd: Path, *args: str) -> str:
|
|
result = subprocess.run(
|
|
["git", *args],
|
|
cwd=cwd,
|
|
env={**os.environ, **GIT_ENV},
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
timeout=60,
|
|
)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def _smoke_packages() -> List[str]:
|
|
match = re.search(r'^PKGS="([^"]+)"', SMOKE_SH.read_text(encoding="utf-8"), re.M)
|
|
assert match, "smoke.sh must declare its ordered PKGS list"
|
|
return match.group(1).split()
|
|
|
|
|
|
class Release:
|
|
"""A scratch checkout with an origin, a built out-dir, and a fake Gitea on PATH."""
|
|
|
|
def __init__(self, root: Path) -> None:
|
|
self.root = root
|
|
self.tree = root / "checkout"
|
|
self.origin = root / "origin.git"
|
|
self.out = root / "publish"
|
|
self.home = root / "home"
|
|
self.tmp = root / "tmp"
|
|
for directory in (self.home, self.tmp):
|
|
directory.mkdir()
|
|
self._write_tree()
|
|
_git(root, "init", "-q", "--bare", str(self.origin))
|
|
_git(self.tree, "remote", "add", "origin", str(self.origin))
|
|
self.gitea = FakeGitea(root)
|
|
|
|
def _write_tree(self) -> None:
|
|
pkg = self.tree / "packaging" / "void"
|
|
pkg.mkdir(parents=True)
|
|
for source in (BUILD_RELEASE, PUBLISH_RELEASE, SMOKE_SH, PINS, PLIST):
|
|
shutil.copy2(source, pkg / source.name)
|
|
(pkg / "srcpkgs" / "voculinux").mkdir(parents=True)
|
|
text = TEMPLATE.read_text(encoding="utf-8")
|
|
text = re.sub(r"^version=.*$", f"version={XBPS}", text, flags=re.M)
|
|
text = re.sub(r"^_tag=.*$", f"_tag={TAG}", text, flags=re.M)
|
|
(pkg / "srcpkgs" / "voculinux" / "template").write_text(text, encoding="utf-8")
|
|
src = self.tree / "src" / "vocalinux"
|
|
src.mkdir(parents=True)
|
|
(src / "version.py").write_text('__version__ = "0.19.0"\n', encoding="utf-8")
|
|
_git(self.tree, "init", "-q")
|
|
_git(self.tree, "add", "-A")
|
|
_git(self.tree, "commit", "-q", "-m", "release fixture")
|
|
|
|
def commit(self, message: str) -> str:
|
|
(self.tree / "CHANGELOG.fixture").write_text(message + "\n", encoding="utf-8")
|
|
_git(self.tree, "add", "-A")
|
|
_git(self.tree, "commit", "-q", "-m", message)
|
|
return _git(self.tree, "rev-parse", "HEAD")
|
|
|
|
def head(self) -> str:
|
|
return _git(self.tree, "rev-parse", "HEAD")
|
|
|
|
def push_tag(self, tag: str = TAG, annotated: bool = True, at: Optional[str] = None) -> None:
|
|
target = at or "HEAD"
|
|
if annotated:
|
|
_git(self.tree, "tag", "-a", tag, target, "-m", f"Voculinux {tag}")
|
|
else:
|
|
_git(self.tree, "tag", tag, target)
|
|
_git(self.tree, "push", "-q", "origin", f"refs/tags/{tag}")
|
|
|
|
def stage(self, built_commit: str, tag: str = TAG) -> None:
|
|
"""Lay out publish/ the way build-release.sh writes it, for ``built_commit``."""
|
|
sdist = self.out / "sdist"
|
|
repo = self.out / "xbps" / "repo"
|
|
keys = self.out / "xbps" / "keys"
|
|
for directory in (sdist, repo, keys):
|
|
directory.mkdir(parents=True)
|
|
payload = b"canonical sdist bytes"
|
|
(sdist / SDIST).write_bytes(payload)
|
|
(self.out / "SDIST_SHA256").write_text(hashlib.sha256(payload).hexdigest() + "\n")
|
|
for name in _smoke_packages():
|
|
pkg = f"{name}-{XBPS}_1"
|
|
(repo / f"{pkg}.x86_64.xbps").write_bytes(b"package " + pkg.encode())
|
|
(repo / f"{pkg}.x86_64.xbps.sig2").write_bytes(b"signature " + pkg.encode())
|
|
(repo / "x86_64-repodata").write_bytes(b"signed index")
|
|
(keys / "FINGERPRINT").write_text(
|
|
f"signed-by: Voculinux <xbps@voculinux.com>\nfingerprint: {FINGERPRINT}\n",
|
|
encoding="utf-8",
|
|
)
|
|
shutil.copy2(PLIST, keys / f"{FINGERPRINT}.plist")
|
|
(self.out / "BUILT_FROM").write_text(f"tag={tag}\ncommit={built_commit}\n")
|
|
|
|
def staged_names(self) -> List[str]:
|
|
return sorted(
|
|
p.name
|
|
for sub in ("sdist", "xbps/repo", "xbps/keys")
|
|
for p in (self.out / sub).iterdir()
|
|
)
|
|
|
|
def run(
|
|
self,
|
|
*args: str,
|
|
tag: str = TAG,
|
|
token: Optional[str] = TOKEN,
|
|
tea_config: Optional[Path] = None,
|
|
fail: str = "",
|
|
) -> subprocess.CompletedProcess:
|
|
env = {
|
|
"PATH": f"{self.gitea.bin}{os.pathsep}{os.environ['PATH']}",
|
|
"HOME": str(self.home),
|
|
"TMPDIR": str(self.tmp),
|
|
"GITEA_HOST": HOST,
|
|
"GITEA_API": API,
|
|
"TEA_CONFIG": str(tea_config or self.home / "no-tea-config.yml"),
|
|
"FAKE_GITEA_STATE": str(self.gitea.state_file),
|
|
"FAKE_GITEA_BASE": API,
|
|
"FAKE_GITEA_FAIL": fail,
|
|
**GIT_ENV,
|
|
}
|
|
if token is not None:
|
|
env["GITEA_TOKEN"] = token
|
|
return subprocess.run(
|
|
["bash", str(self.tree / "packaging" / "void" / "publish-release.sh"), tag, *args],
|
|
cwd=self.tree,
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
check=False,
|
|
)
|
|
|
|
def publish(self, *extra: str, **kwargs) -> subprocess.CompletedProcess:
|
|
return self.run("--out-dir", str(self.out), *extra, **kwargs)
|
|
|
|
|
|
@pytest.fixture()
|
|
def release(tmp_path: Path) -> Release:
|
|
if shutil.which("git") is None or shutil.which("bash") is None:
|
|
pytest.skip("git and bash are required")
|
|
return Release(tmp_path)
|
|
|
|
|
|
def _published(release: Release) -> Release:
|
|
"""A tree whose origin holds v0.19.0 at HEAD, and a staged out-dir for HEAD."""
|
|
release.push_tag()
|
|
release.stage(release.head())
|
|
return release
|
|
|
|
|
|
def _uploads(gitea: FakeGitea, tag: str) -> List[str]:
|
|
return gitea.uploads(tag)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The gates: nothing is published unless origin's tag is the built commit
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize("annotated", [True, False], ids=["annotated", "lightweight"])
|
|
def test_a_tag_at_the_built_commit_publishes_both_releases(
|
|
release: Release, annotated: bool
|
|
) -> None:
|
|
release.push_tag(annotated=annotated)
|
|
release.stage(release.head())
|
|
result = release.publish()
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
assert "gates passed" in result.stdout
|
|
expected = release.staged_names()
|
|
assert SDIST in expected
|
|
for tag in (TAG, "latest"):
|
|
assert sorted(release.gitea.asset_names(tag)) == expected, tag
|
|
uploaded = _uploads(release.gitea, tag)
|
|
assert sorted(uploaded) == expected, f"{tag}: {uploaded}"
|
|
assert uploaded[-1] == "x86_64-repodata", "the index goes last, after its packages"
|
|
|
|
|
|
def test_a_tag_that_is_not_on_origin_is_refused(release: Release) -> None:
|
|
release.stage(release.head())
|
|
result = release.publish()
|
|
assert result.returncode != 0
|
|
assert f"tag {TAG} is not on origin" in result.stderr
|
|
assert not release.gitea.state["log"], "no request may be made for an unpublished tag"
|
|
|
|
|
|
def test_a_tag_that_only_shares_a_prefix_with_the_release_is_refused(release: Release) -> None:
|
|
"""origin holds v0.19.0x; the release is v0.19.0. A prefix match must not pass."""
|
|
_git(release.tree, "tag", "-a", "v0.19.0x", "-m", "other", "HEAD")
|
|
_git(release.tree, "push", "-q", "origin", "refs/tags/v0.19.0x")
|
|
release.stage(release.head())
|
|
result = release.publish()
|
|
assert result.returncode != 0
|
|
assert f"tag {TAG} is not on origin" in result.stderr
|
|
assert not release.gitea.state["log"]
|
|
|
|
|
|
def test_a_tag_on_another_commit_is_refused(release: Release) -> None:
|
|
built = release.head()
|
|
release.stage(built)
|
|
later = release.commit("a commit after the build")
|
|
release.push_tag(at=later)
|
|
result = release.publish()
|
|
assert result.returncode != 0
|
|
assert f"points at {later}, but" in result.stderr
|
|
assert f"built from {built}" in result.stderr
|
|
assert not release.gitea.state["log"]
|
|
|
|
|
|
def test_an_out_dir_without_a_build_record_is_refused(release: Release) -> None:
|
|
_published(release)
|
|
(release.out / "BUILT_FROM").unlink()
|
|
result = release.publish()
|
|
assert result.returncode != 0
|
|
assert "no BUILT_FROM record" in result.stderr
|
|
assert not release.gitea.state["log"]
|
|
|
|
|
|
def test_an_out_dir_built_for_another_tag_is_refused(release: Release) -> None:
|
|
release.push_tag()
|
|
release.stage(release.head(), tag="v0.18.5")
|
|
result = release.publish()
|
|
assert result.returncode != 0
|
|
assert "was built for 'v0.18.5'" in result.stderr
|
|
assert not release.gitea.state["log"]
|
|
|
|
|
|
def test_a_sdist_that_does_not_match_its_digest_is_refused(release: Release) -> None:
|
|
_published(release)
|
|
(release.out / "sdist" / SDIST).write_bytes(b"tampered after the build")
|
|
result = release.publish()
|
|
assert result.returncode != 0
|
|
assert "does not match SDIST_SHA256" in result.stderr
|
|
assert not release.gitea.state["log"]
|
|
|
|
|
|
def test_a_client_key_other_than_the_committed_plist_is_refused(release: Release) -> None:
|
|
_published(release)
|
|
(release.out / "xbps" / "keys" / f"{FINGERPRINT}.plist").write_text("<plist/>\n")
|
|
result = release.publish()
|
|
assert result.returncode != 0
|
|
assert "not the committed packaging/void/repository-key.plist" in result.stderr
|
|
assert not release.gitea.state["log"]
|
|
|
|
|
|
def test_a_repository_without_its_index_is_refused(release: Release) -> None:
|
|
_published(release)
|
|
(release.out / "xbps" / "repo" / "x86_64-repodata").unlink()
|
|
result = release.publish()
|
|
assert result.returncode != 0
|
|
assert "has no repository index" in result.stderr
|
|
assert not release.gitea.state["log"]
|
|
|
|
|
|
def test_a_tag_that_fails_the_tag_gate_is_refused(release: Release) -> None:
|
|
"""v0.19.0-beta is not the release: the tag gate refuses it before origin is read."""
|
|
_published(release)
|
|
result = release.publish(tag="v0.19.0-beta")
|
|
assert result.returncode != 0
|
|
assert "fails the tag gate" in result.stderr
|
|
assert not release.gitea.state["log"]
|
|
|
|
|
|
def test_usage_errors_exit_2(release: Release) -> None:
|
|
"""No --out-dir is a usage error, and so is a call with no tag at all."""
|
|
no_out_dir = release.run()
|
|
assert no_out_dir.returncode == 2, no_out_dir.stderr
|
|
assert "usage: publish-release.sh" in no_out_dir.stderr
|
|
bare = subprocess.run(
|
|
["bash", str(release.tree / "packaging" / "void" / "publish-release.sh")],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
assert bare.returncode == 2, bare.stderr
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dry run: every gate, the planned calls, and no request at all
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_a_dry_run_plans_every_call_and_makes_none(release: Release) -> None:
|
|
_published(release)
|
|
result = release.publish("--dry-run")
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
assert "dry run" in result.stdout
|
|
assert release.gitea.state["log"] == [], "a dry run must not reach the API"
|
|
lines = result.stdout.splitlines()
|
|
tag_section = lines.index(f"tag release {TAG} (commit {release.head()}):")
|
|
latest_section = lines.index("latest release (rolling, the registration URL serves it):")
|
|
assert tag_section < latest_section
|
|
for start, end in ((tag_section, latest_section), (latest_section, len(lines))):
|
|
uploads = [line for line in lines[start:end] if "/assets?name=" in line]
|
|
assert uploads, "a section plans its uploads"
|
|
assert uploads[-1].endswith("attachment=xbps/repo/x86_64-repodata"), uploads[-1]
|
|
assert f"POST {API}/releases only when {TAG} has no release" in result.stdout
|
|
|
|
|
|
def test_a_dry_run_never_prints_or_reads_a_token(release: Release) -> None:
|
|
_published(release)
|
|
result = release.publish("--dry-run", token=TOKEN)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
assert TOKEN not in result.stdout + result.stderr
|
|
assert "Authorization: token <redacted>" in result.stdout
|
|
|
|
|
|
def test_a_dry_run_needs_no_token_at_all(release: Release) -> None:
|
|
_published(release)
|
|
result = release.publish("--dry-run", token=None)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
|
|
|
|
def test_a_dry_run_still_refuses_a_tag_that_is_not_on_origin(release: Release) -> None:
|
|
release.stage(release.head())
|
|
result = release.publish("--dry-run")
|
|
assert result.returncode != 0
|
|
assert "is not on origin" in result.stderr
|
|
assert not release.gitea.state["log"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The publish itself, against the stateful stand-in
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_latest_release_is_created_at_the_built_commit(release: Release) -> None:
|
|
_published(release)
|
|
assert release.publish().returncode == 0
|
|
built = release.head()
|
|
latest = release.gitea.release("latest")
|
|
assert latest["target_commitish"] == built
|
|
tag_release = release.gitea.release(TAG)
|
|
assert tag_release["target_commitish"] == built
|
|
assert not tag_release["draft"] and not latest["draft"]
|
|
assert TAG in latest["body"], "latest says which tag it was built from"
|
|
assert built in latest["body"]
|
|
|
|
|
|
def test_every_request_is_to_the_fork_api_and_carries_the_token(release: Release) -> None:
|
|
_published(release)
|
|
assert release.publish().returncode == 0
|
|
log = release.gitea.state["log"]
|
|
assert log
|
|
for entry in log:
|
|
assert entry["url"].startswith(API + "/"), entry["url"]
|
|
assert entry["auth"] == [f"Authorization: token {TOKEN}"], entry
|
|
posts = [e["data"]["tag_name"] for e in log if e["url"] == f"{API}/releases"]
|
|
assert posts == [TAG, "latest"], "the tag release is filled before latest is touched"
|
|
|
|
|
|
def test_the_notes_state_the_sdist_digest_exactly(release: Release) -> None:
|
|
_published(release)
|
|
assert release.publish().returncode == 0
|
|
digest = (release.out / "SDIST_SHA256").read_text(encoding="utf-8").strip()
|
|
assert f"sdist sha256: {digest}" in release.gitea.release("latest")["body"]
|
|
assert FINGERPRINT in release.gitea.release(TAG)["body"]
|
|
|
|
|
|
def test_a_rerun_replaces_same_named_assets_on_the_tag_release(release: Release) -> None:
|
|
_published(release)
|
|
release.gitea.seed_release(TAG, [SDIST, "x86_64-repodata"])
|
|
before = {a["name"]: a["id"] for a in release.gitea.release(TAG)["assets"]}
|
|
assert release.publish().returncode == 0
|
|
after = {a["name"]: a["id"] for a in release.gitea.release(TAG)["assets"]}
|
|
assert after[SDIST] != before[SDIST]
|
|
assert after["x86_64-repodata"] != before["x86_64-repodata"]
|
|
assert sorted(after) == release.staged_names()
|
|
|
|
|
|
def test_latest_replaces_every_asset_it_already_held(release: Release) -> None:
|
|
_published(release)
|
|
release.gitea.seed_release("latest", ["voculinux-0.18.5_1.x86_64.xbps", "stale.txt"])
|
|
assert release.publish().returncode == 0
|
|
assert sorted(release.gitea.asset_names("latest")) == release.staged_names()
|
|
assert (
|
|
len([r for r in release.gitea.state["releases"].values() if r["tag_name"] == "latest"]) == 1
|
|
)
|
|
|
|
|
|
def test_uploads_come_only_from_the_staged_set(release: Release) -> None:
|
|
_published(release)
|
|
assert release.publish().returncode == 0
|
|
staged = set(release.staged_names())
|
|
for tag in (TAG, "latest"):
|
|
assert set(_uploads(release.gitea, tag)) == staged
|
|
for entry in release.gitea.state["log"]:
|
|
for form in entry["forms"]:
|
|
path = re.match(r"attachment=@(.*?);", form).group(1)
|
|
assert path.startswith(("sdist/", "xbps/")), f"not from the staged set: {path}"
|
|
|
|
|
|
def test_the_colon_in_the_fingerprint_name_is_percent_encoded(release: Release) -> None:
|
|
_published(release)
|
|
assert release.publish().returncode == 0
|
|
plist_urls = [e["url"] for e in release.gitea.state["log"] if ".plist" in e["url"]]
|
|
assert plist_urls and all(":" not in u.split("name=")[1] for u in plist_urls), plist_urls
|
|
assert f"{FINGERPRINT}.plist" in release.gitea.asset_names("latest")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"fail",
|
|
[
|
|
"POST|/releases|403", # the token may not create releases
|
|
"POST|/assets?|500", # an upload fails midway
|
|
"DELETE|/assets/|500",
|
|
"GET|/releases/tags/|500",
|
|
"PATCH|/releases/|500",
|
|
],
|
|
)
|
|
def test_any_non_2xx_response_fails_the_publish(release: Release, fail: str) -> None:
|
|
_published(release)
|
|
release.gitea.seed_release("latest", ["stale"])
|
|
release.gitea.seed_release(TAG, ["x86_64-repodata"])
|
|
result = release.publish(fail=fail)
|
|
assert result.returncode != 0, f"{fail} must fail the publish"
|
|
assert "HTTP" in result.stderr or "failed" in result.stderr
|
|
|
|
|
|
def test_a_failed_tag_release_never_touches_latest(release: Release) -> None:
|
|
_published(release)
|
|
result = release.publish(fail="POST|/assets?|500")
|
|
assert result.returncode != 0
|
|
assert not any(r["tag_name"] == "latest" for r in release.gitea.state["releases"].values())
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The token: from GITEA_TOKEN or the tea login, never printed, never left behind
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _tea_config(path: Path, token: str) -> Path:
|
|
path.write_text(
|
|
"logins:\n"
|
|
" - name: other\n"
|
|
" url: https://example.invalid\n"
|
|
" token: token-for-another-host\n"
|
|
" default: true\n"
|
|
" - name: xavierk\n"
|
|
f" url: {HOST}\n"
|
|
f" token: {token}\n"
|
|
" default: false\n"
|
|
" user: xavierk\n"
|
|
"preferences:\n"
|
|
" editor: false\n",
|
|
encoding="utf-8",
|
|
)
|
|
return path
|
|
|
|
|
|
def test_the_token_comes_from_the_tea_login_for_this_host_and_is_never_printed(
|
|
release: Release,
|
|
) -> None:
|
|
_published(release)
|
|
config = _tea_config(release.root / "tea.yml", TEA_TOKEN)
|
|
result = release.publish(token=None, tea_config=config)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
assert TEA_TOKEN not in result.stdout + result.stderr
|
|
for entry in release.gitea.state["log"]:
|
|
assert entry["auth"] == [f"Authorization: token {TEA_TOKEN}"], entry
|
|
|
|
|
|
def test_no_token_and_no_tea_login_refuses_before_any_request(release: Release) -> None:
|
|
_published(release)
|
|
config = _tea_config(release.root / "tea.yml", TEA_TOKEN)
|
|
other = release.root / "other-host.yml"
|
|
other.write_text(config.read_text(encoding="utf-8").replace(HOST, "https://elsewhere.invalid"))
|
|
result = release.publish(token=None, tea_config=other)
|
|
assert result.returncode != 0
|
|
assert f"no tea login for {HOST}" in result.stderr
|
|
assert not release.gitea.state["log"]
|
|
|
|
|
|
def test_the_header_file_is_removed_when_the_publish_ends(release: Release) -> None:
|
|
_published(release)
|
|
assert release.publish().returncode == 0
|
|
assert list(release.tmp.glob("publish-release.*")) == []
|
|
|
|
|
|
def test_a_failed_publish_also_removes_the_header_file(release: Release) -> None:
|
|
_published(release)
|
|
assert release.publish(fail="POST|/releases|403").returncode != 0
|
|
assert list(release.tmp.glob("publish-release.*")) == []
|
|
|
|
|
|
def test_the_token_never_appears_in_the_output_of_a_failed_request(release: Release) -> None:
|
|
_published(release)
|
|
result = release.publish(fail="POST|/releases|403")
|
|
assert TOKEN not in result.stdout + result.stderr
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# The script itself
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_the_script_is_executable_bash_that_fails_closed() -> None:
|
|
text = PUBLISH_RELEASE.read_text(encoding="utf-8")
|
|
assert text.startswith("#!/usr/bin/env bash\n")
|
|
assert "set -euo pipefail" in text
|
|
assert os.access(PUBLISH_RELEASE, os.X_OK)
|
|
|
|
|
|
def test_the_script_never_enables_tracing_or_echoes_the_header() -> None:
|
|
code = "\n".join(
|
|
line
|
|
for line in PUBLISH_RELEASE.read_text(encoding="utf-8").splitlines()
|
|
if not line.lstrip().startswith("#")
|
|
)
|
|
assert "set -x" not in code
|
|
assert 'cat "$AUTH"' not in code
|
|
assert 'echo "$GITEA_TOKEN"' not in code and "printf '%s' \"$GITEA_TOKEN\"" not in code
|
|
|
|
|
|
def test_the_workflow_publish_step_and_this_script_name_the_same_api() -> None:
|
|
"""Two copies of the publish logic: they must publish to the same place."""
|
|
workflow = (REPO_ROOT / ".gitea" / "workflows" / "xbps-release.yml").read_text(encoding="utf-8")
|
|
assert "GITEA_API: https://git.bongbetic.com/api/v1/repos/xavierk/voculinux" in workflow
|
|
assert "https://git.bongbetic.com/api/v1/repos/xavierk/voculinux" in PUBLISH_RELEASE.read_text(
|
|
encoding="utf-8"
|
|
)
|
|
script = PUBLISH_RELEASE.read_text(encoding="utf-8")
|
|
assert "xbps/repo/*-repodata*" in workflow and "xbps/repo/*-repodata*" in script
|