Dev #72

Merged
Latte merged 71 commits from dev into main 2026-10-14 17:52:23 +00:00
3 changed files with 161 additions and 8 deletions
Showing only changes of commit 7dc749d7cd - Show all commits

No files matched your search

+19
View File
@@ -35,6 +35,25 @@ _HERE = Path(__file__).parent.resolve()
ENABLE_ENV = "AEGIS_INTEGRATION" ENABLE_ENV = "AEGIS_INTEGRATION"
REQUIRE_RUNNER_ENV = "AEGIS_IT_REQUIRE_RUNNER"
def runner_required() -> bool:
"""True when a runner that cannot start must fail the test instead of xfailing it.
Set by the CI job (``AEGIS_IT_REQUIRE_RUNNER=1``) and implied by ``CI``: a pipeline that
silently skips the Actions scenario would hide a broken runner setup.
"""
return os.environ.get(REQUIRE_RUNNER_ENV) == "1" or bool(os.environ.get("CI"))
def runner_unavailable(reason: str) -> None:
"""Report that the act_runner could not start or come online (fails or xfails)."""
if runner_required():
pytest.fail(f"act_runner unavailable: {reason}", pytrace=False)
pytest.xfail(reason)
def integration_enabled() -> bool: def integration_enabled() -> bool:
"""Return True when the opt-in env var is set.""" """Return True when the opt-in env var is set."""
return os.environ.get(ENABLE_ENV) == "1" return os.environ.get(ENABLE_ENV) == "1"
+13 -3
View File
@@ -561,9 +561,19 @@ def stop_gitea(instance: GiteaInstance) -> None:
cleanup_resources(res) cleanup_resources(res)
def start_runner(instance: GiteaInstance, registration_token: str) -> str: def start_runner(
instance: GiteaInstance,
registration_token: str,
*,
suffix: str = "runner",
label: str = RUNNER_LABEL,
) -> str:
"""Start an ``act_runner`` container registered with ``registration_token``. """Start an ``act_runner`` container registered with ``registration_token``.
``suffix`` names the container (``aegis-it-<run id>-<suffix>``, also the runner's name in
Gitea) and ``label`` is the single label it serves, so extra runners at other scopes
never pick up the jobs of the Actions scenario.
The runner joins the run's private network, reaches Gitea by its container name The runner joins the run's private network, reaches Gitea by its container name
(so it works in local and in container network mode) and runs jobs labelled (so it works in local and in container network mode) and runs jobs labelled
``aegis-host`` in host mode inside its own container. The token travels in the ``aegis-host`` in host mode inside its own container. The token travels in the
@@ -575,7 +585,7 @@ def start_runner(instance: GiteaInstance, registration_token: str) -> str:
if res is None: if res is None:
raise HarnessError("the Gitea instance is not tracked (already stopped?)") raise HarnessError("the Gitea instance is not tracked (already stopped?)")
ensure_image(ACT_RUNNER_IMAGE) ensure_image(ACT_RUNNER_IMAGE)
name = f"{NAME_PREFIX}{instance.run_id}-runner" name = f"{NAME_PREFIX}{instance.run_id}-{suffix}"
res.extra_containers.append(name) res.extra_containers.append(name)
run_docker( run_docker(
"run", "run",
@@ -593,7 +603,7 @@ def start_runner(instance: GiteaInstance, registration_token: str) -> str:
"-e", "-e",
f"GITEA_RUNNER_NAME={name}", f"GITEA_RUNNER_NAME={name}",
"-e", "-e",
f"GITEA_RUNNER_LABELS={RUNNER_LABEL}:host", f"GITEA_RUNNER_LABELS={label}:host",
ACT_RUNNER_IMAGE, ACT_RUNNER_IMAGE,
env={RUNNER_REGISTRATION_ENV: registration_token}, env={RUNNER_REGISTRATION_ENV: registration_token},
timeout=300, timeout=300,
+129 -5
View File
@@ -6,9 +6,7 @@ import asyncio
import time import time
from typing import Any from typing import Any
import pytest from tests.integration.conftest import McpSession, RepoRef, b64, drop_org, runner_unavailable
from tests.integration.conftest import McpSession, RepoRef, b64
from tests.integration.harness import GiteaInstance, HarnessError, runner_logs, start_runner from tests.integration.harness import GiteaInstance, HarnessError, runner_logs, start_runner
WORKFLOW_PATH = ".gitea/workflows/it.yml" WORKFLOW_PATH = ".gitea/workflows/it.yml"
@@ -45,7 +43,7 @@ async def test_workflow_run_on_act_runner(
try: try:
runner = start_runner(gitea, token.data["token"]) runner = start_runner(gitea, token.data["token"])
except HarnessError as exc: except HarnessError as exc:
pytest.xfail(f"the act_runner container could not be started: {exc}") runner_unavailable(f"the act_runner container could not be started: {exc}")
async def online() -> dict[str, Any] | None: async def online() -> dict[str, Any] | None:
runners = await mcp.ok("gitea_actions_get_repo_runners", **repo) runners = await mcp.ok("gitea_actions_get_repo_runners", **repo)
@@ -58,7 +56,7 @@ async def test_workflow_run_on_act_runner(
registered = await _wait_for(online, timeout=RUNNER_ONLINE_TIMEOUT) registered = await _wait_for(online, timeout=RUNNER_ONLINE_TIMEOUT)
if not registered: if not registered:
pytest.xfail( runner_unavailable(
"the act_runner did not come online within " "the act_runner did not come online within "
f"{RUNNER_ONLINE_TIMEOUT:.0f}s; runner log tail: {runner_logs(runner, 15)}" f"{RUNNER_ONLINE_TIMEOUT:.0f}s; runner log tail: {runner_logs(runner, 15)}"
) )
@@ -115,3 +113,129 @@ async def test_workflow_run_on_act_runner(
assert (await mcp.ok("gitea_actions_get_artifacts", **repo)).data["total_count"] == 0 assert (await mcp.ok("gitea_actions_get_artifacts", **repo)).data["total_count"] == 0
tasks = await mcp.ok("gitea_actions_list_action_tasks", **repo) tasks = await mcp.ok("gitea_actions_list_action_tasks", **repo)
assert tasks.data["total_count"] >= 2 assert tasks.data["total_count"] >= 2
# The runner registered above, managed through the repository-scope runner tools.
runner_id = registered["id"]
got = await mcp.ok("gitea_actions_get_repo_runner", runner_id=str(runner_id), **repo)
assert got.data["id"] == runner_id and got.data["name"] == registered["name"]
off = await mcp.ok(
"gitea_actions_update_repo_runner",
runner_id=str(runner_id),
body={"disabled": True},
**repo,
)
assert off.data["disabled"] is True
on = await mcp.ok(
"gitea_actions_update_repo_runner",
runner_id=str(runner_id),
body={"disabled": False},
**repo,
)
assert on.data["disabled"] is False
# The completed run: logs archive, rerun of the run and of one job, then delete the run.
archive = await mcp.ok("gitea_actions_get_workflow_run_logs", run=run_id, **repo)
assert archive.data
await mcp.ok("gitea_actions_rerun_workflow_run", status=201, run=run_id, **repo)
await _wait_for_attempt(mcp, repo, run_id, attempt=2)
rerun_jobs = await mcp.ok("gitea_actions_list_workflow_run_jobs", run=run_id, **repo)
await mcp.ok(
"gitea_actions_rerun_workflow_job",
status=201,
run=run_id,
job_id=rerun_jobs.data["jobs"][0]["id"],
**repo,
)
await _wait_for_attempt(mcp, repo, run_id, attempt=3)
await mcp.ok("gitea_actions_delete_action_run", run=run_id, **repo)
await mcp.fail("gitea_actions_get_workflow_run", 404, run=run_id, **repo)
await mcp.ok("gitea_actions_delete_repo_runner", runner_id=str(runner_id), **repo)
await mcp.fail("gitea_actions_get_repo_runner", 404, runner_id=str(runner_id), **repo)
async def _wait_for_attempt(
mcp: McpSession, repo: dict[str, str], run_id: int, *, attempt: int
) -> None:
"""Wait until ``run_id`` reaches ``attempt`` and is completed again."""
async def done() -> bool:
run = (await mcp.ok("gitea_actions_get_workflow_run", run=run_id, **repo)).data
return bool(run["status"] == "completed" and run.get("run_attempt", 1) >= attempt)
assert await _wait_for(done, timeout=RUN_TIMEOUT), (
f"run {run_id} did not finish attempt {attempt}"
)
async def test_runner_management_at_every_scope(mcp: McpSession, gitea: GiteaInstance) -> None:
"""Register one runner per scope (org, user, admin) and manage it through the tools."""
org = gitea.unique("it-runner")
await mcp.ok("gitea_org_create", status=201, body={"username": org})
try:
scopes: list[tuple[str, str, dict[str, str], str]] = [
("org", "gitea_actions_org_create_runner_registration_token", {"org": org}, "org"),
("user", "gitea_actions_user_create_runner_registration_token", {}, "user"),
("admin", "gitea_admin_create_runner_registration_token", {}, "admin"),
]
for scope, token_tool, args, _ in scopes:
token = await mcp.ok(token_tool, **args)
suffix = f"{scope}-runner"
try:
container = start_runner(
gitea, token.data["token"], suffix=suffix, label=f"aegis-{scope}"
)
except HarnessError as exc:
runner_unavailable(f"the {scope} runner could not be started: {exc}")
return
listing = (
"gitea_admin_get_runners"
if scope == "admin"
else (f"gitea_actions_get_{scope}_runners")
)
getter = (
"gitea_admin_get_runner"
if scope == "admin"
else (f"gitea_actions_get_{scope}_runner")
)
updater = (
"gitea_admin_update_runner"
if scope == "admin"
else (f"gitea_actions_update_{scope}_runner")
)
deleter = (
"gitea_admin_delete_runner"
if scope == "admin"
else (f"gitea_actions_delete_{scope}_runner")
)
async def registered(
listing: str = listing, args: dict[str, str] = args, container: str = container
) -> dict[str, Any] | None:
runners = await mcp.ok(listing, **args)
entries = (
runners.data["runners"] if isinstance(runners.data, dict) else runners.data
)
for entry in entries or []:
if entry["name"] == container:
found: dict[str, Any] = entry
return found
return None
runner = await _wait_for(registered, timeout=RUNNER_ONLINE_TIMEOUT)
if not runner:
runner_unavailable(
f"the {scope} runner did not register; log tail: {runner_logs(container, 15)}"
)
return
assert token.data["token"] not in runner_logs(container, 200)
got = await mcp.ok(getter, runner_id=str(runner["id"]), **args)
assert got.data["name"] == container
changed = await mcp.ok(
updater, runner_id=str(runner["id"]), body={"disabled": True}, **args
)
assert changed.data["disabled"] is True
await mcp.ok(deleter, runner_id=str(runner["id"]), **args)
await mcp.fail(getter, 404, runner_id=str(runner["id"]), **args)
finally:
drop_org(gitea, org)