A repository per test run instead of the shared sandbox
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 5m17s
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 5m17s
Every live test that touches a repository gets one of its own from e2e_support.fresh_repo, made by the test operator as an operator makes a project's (D4, D8): seeded from tests/integration/seed/ (the sandbox's package, without past runs' leftovers), the agent's account a collaborator with write access, main protected (pushes only by the owner, an approval to merge), and a deploy key generated for the run. It is deleted afterwards (NISSE_E2E_KEEP_REPOS keeps it); scripts/e2e-cleanup.sh deletes leftovers. - Both e2e fixtures use it; per-run base branches are gone. The merge helper approves first, as the protection now requires. - test_gitea_live uses it, so CI needs NISSE_OPERATOR_TOKEN and no longer writes the sandbox deploy key (NISSE_PUSH_KEY). - A live test of the helper: seeded, shared with the agent, protected against it, deleted. - nisse-test.yaml's project entry is a placeholder; pytest and ruff skip the seed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
1 parent
6c962e1ed9
commit
679ace1fd8
18 files changed
+379
-55
No files matched your search
@@ -1,10 +1,14 @@
|
||||
"""Shared by the end-to-end tests: each run gets work that can't already be
|
||||
done, and a base branch of its own, so runs never depend on one another or
|
||||
on what earlier runs merged."""
|
||||
"""Shared by the live tests: each run gets a repository of its own, made by
|
||||
the test operator and deleted afterwards, so runs never depend on one
|
||||
another or on what earlier runs merged."""
|
||||
|
||||
import base64
|
||||
import contextlib
|
||||
import os
|
||||
import subprocess
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from nisse_dev.gitea import GiteaClient, GiteaError
|
||||
|
||||
@@ -34,18 +38,97 @@ def operator_gitea(url: str) -> GiteaClient:
|
||||
return GiteaClient(url, token)
|
||||
|
||||
|
||||
OWNER = "nisse-testing-operator" # the test operator: owns every test repository
|
||||
SEED = Path(__file__).parent / "seed"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FreshRepo:
|
||||
name: str # owner/repo
|
||||
push_key: Path # this repository's deploy key, private half
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def base_branch(operator: GiteaClient, repo: str, run: str) -> Iterator[str]:
|
||||
"""A branch off main for this run's PRs to target and merge into, so
|
||||
nothing accumulates in main; deleted afterwards."""
|
||||
name = f"e2e-base-{run}"
|
||||
def fresh_repo(
|
||||
operator: GiteaClient, run: str, keys: Path, *, agent: str
|
||||
) -> Iterator[FreshRepo]:
|
||||
"""A repository of its own for one test run, as the operator would make a
|
||||
project's (D4, D8): seeded with a small Python package, the ``agent``
|
||||
account a collaborator with write access, the default branch protected (pushes only by the
|
||||
owner, an approval to merge), and a deploy key of its own, written under
|
||||
``keys``. Deleted afterwards, unless NISSE_E2E_KEEP_REPOS is set."""
|
||||
name = f"nisse-e2e-{run}"
|
||||
full = f"{OWNER}/{name}"
|
||||
operator._request(
|
||||
"POST",
|
||||
f"/repos/{repo}/branches",
|
||||
json={"new_branch_name": name, "old_branch_name": "main"},
|
||||
"/user/repos",
|
||||
json={
|
||||
"name": name,
|
||||
"private": True,
|
||||
"auto_init": True,
|
||||
"default_branch": "main",
|
||||
},
|
||||
)
|
||||
try:
|
||||
yield name
|
||||
files = [
|
||||
{
|
||||
"operation": "create",
|
||||
"path": str(path.relative_to(SEED)),
|
||||
"content": base64.b64encode(path.read_bytes()).decode(),
|
||||
}
|
||||
for path in sorted(SEED.rglob("*"))
|
||||
if path.is_file() and path.name != "README.md"
|
||||
]
|
||||
operator._request(
|
||||
"POST",
|
||||
f"/repos/{full}/contents",
|
||||
json={"branch": "main", "message": "Seed the test project", "files": files},
|
||||
)
|
||||
readme = operator._request("GET", f"/repos/{full}/contents/README.md")
|
||||
operator._request(
|
||||
"PUT",
|
||||
f"/repos/{full}/contents/README.md",
|
||||
json={
|
||||
"branch": "main",
|
||||
"message": "README",
|
||||
"sha": readme["sha"],
|
||||
"content": base64.b64encode((SEED / "README.md").read_bytes()).decode(),
|
||||
},
|
||||
)
|
||||
operator._request(
|
||||
"PUT", f"/repos/{full}/collaborators/{agent}", json={"permission": "write"}
|
||||
)
|
||||
keys.mkdir(parents=True, exist_ok=True)
|
||||
key = keys / f"{name}.key"
|
||||
key.unlink(missing_ok=True)
|
||||
subprocess.run(
|
||||
["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", name, "-f", str(key)],
|
||||
check=True,
|
||||
)
|
||||
operator._request(
|
||||
"POST",
|
||||
f"/repos/{full}/keys",
|
||||
json={
|
||||
"title": f"{name} push",
|
||||
"key": key.with_suffix(".key.pub").read_text().strip(),
|
||||
"read_only": False,
|
||||
},
|
||||
)
|
||||
operator._request(
|
||||
"POST",
|
||||
f"/repos/{full}/branch_protections",
|
||||
# Pushes only by the operator (who sets up fixtures, as an owner
|
||||
# can); the agent's are refused, and merging needs an approval.
|
||||
json={
|
||||
"branch_name": "main",
|
||||
"enable_push": True,
|
||||
"enable_push_whitelist": True,
|
||||
"push_whitelist_usernames": [OWNER],
|
||||
"required_approvals": 1,
|
||||
},
|
||||
)
|
||||
yield FreshRepo(full, key)
|
||||
finally:
|
||||
with contextlib.suppress(GiteaError):
|
||||
operator._request("DELETE", f"/repos/{repo}/branches/{name}")
|
||||
if not os.environ.get("NISSE_E2E_KEEP_REPOS"):
|
||||
with contextlib.suppress(GiteaError):
|
||||
operator._request("DELETE", f"/repos/{full}")
|
||||
Reference in new issue
Block a user