Dev #72
No files matched your search
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in new issue
Block a user