S19: adopt shared mini-pc runner and landscape workflows

This commit is contained in:
2026-11-10 08:53:20 +01:00
parent c6a495a93c
commit 77b7159f0a
43 changed files with 265 additions and 548 deletions

View File

@@ -79,12 +79,79 @@ class Setup {
exists = false;
}
api(server, admin, exists ? "PUT" : "POST", variable, Map.of("value", platform));
Map<String, Object> response =
Json.object(api(server, admin, "POST", base + "/actions/runners/registration-token", null));
Path directory = home().resolve(".config/" + Recipe.project(env) + "-runner");
IO.println("Actions, project registry secrets and platform configured; using the shared runner.");
}
static String runnerName(final Map<String, String> env) {
final String name = env.getOrDefault("RUNNER_NAME", env.getOrDefault("RUNNER_LABEL", "mini-pc"));
Pipeline.check(name.matches("[a-z][a-z0-9-]*"), "Invalid runner name");
return name;
}
static String runnerRegistrationPath(final Map<String, String> env) {
return switch (env.getOrDefault("RUNNER_SCOPE", "user")) {
case "user" -> "/api/v1/user/actions/runners/registration-token";
case "instance" -> "/api/v1/admin/actions/runners/registration-token";
case "organization" -> {
final String organization = required(env, "RUNNER_ORGANIZATION");
Pipeline.check(organization.matches("[a-zA-Z0-9_-]+"), "Invalid runner organization");
yield "/api/v1/orgs/" + organization + "/actions/runners/registration-token";
}
case "repository" ->
"/api/v1/repos/" + required(env, "CI_REPOSITORY") + "/actions/runners/registration-token";
default ->
throw new IllegalArgumentException("Runner scope must be user, organization, instance or repository");
};
}
static void configureRunner(final Map<String, String> env) throws Exception {
final String admin = secret(env, "GITEA_ADMIN_TOKEN_FILE");
final Map<String, Object> response =
Json.object(api(required(env, "GITEA_SERVER_URL"), admin, "POST", runnerRegistrationPath(env), null));
final Path directory = home().resolve(".config/" + runnerName(env) + "-runner");
privateDirectory(directory);
privateWrite(directory.resolve("registration-token"), Objects.toString(response.get("token")));
System.out.println("Actions, registry secrets and platform configured; runner token saved outside Git.");
IO.println("Shared runner registration token saved outside Git; run install-runner once on the workstation.");
}
static void checkRunner(final Map<String, String> env) throws Exception {
final String token = secret(env, "GITEA_ADMIN_TOKEN_FILE");
final String repository = required(env, "CI_REPOSITORY");
final String server = required(env, "GITEA_SERVER_URL");
final Map<String, Object> project = Json.object(api(server, token, "GET", "/api/v1/repos/" + repository, null));
Pipeline.check(Boolean.TRUE.equals(project.get("has_actions")), "Enable Actions for the repository first");
if ("user".equals(env.getOrDefault("RUNNER_SCOPE", "user"))) {
final Map<String, Object> user = Json.object(api(server, token, "GET", "/api/v1/user", null));
Pipeline.check(
repository.split("/", 2)[0].equalsIgnoreCase(Objects.toString(user.get("login"))),
"User-scoped runner must belong to the repository owner; use organization scope for an"
+ " organization");
}
// Gitea's repository runner list excludes inherited user/organization runners.
final String base = runnerRegistrationPath(env).replace("/registration-token", "");
final Map<String, Object> response =
Json.object(api(required(env, "GITEA_SERVER_URL"), token, "GET", base, null));
Pipeline.check(
hasRunner(response, env.getOrDefault("RUNNER_LABEL", "mini-pc")),
"No online shared runner with the configured label is available to this repository");
IO.println("Shared runner is online and available to " + required(env, "CI_REPOSITORY"));
}
static boolean hasRunner(final Map<String, Object> response, final String label) {
for (final Object entry : (List<?>) response.getOrDefault("runners", List.of())) {
final Map<String, Object> runner = Json.object(entry);
if (!"online".equals(runner.get("status"))
&& !"idle".equals(runner.get("status"))
&& !"active".equals(runner.get("status"))) {
continue;
}
for (final Object item : (List<?>) runner.getOrDefault("labels", List.of())) {
if (label.equals(Json.object(item).get("name"))) {
return true;
}
}
}
return false;
}
static void configureDocker(Path file, String registry) throws IOException {
@@ -95,7 +162,7 @@ class Setup {
throw new IllegalArgumentException("Existing insecure-registries must be an array; unchanged");
List<Object> registries = new ArrayList<>((List<?>) entry);
if (registries.contains(registry)) {
System.out.println("Internal registry already configured; no change");
IO.println("Internal registry already configured; no change");
return;
}
registries.add(registry);
@@ -114,7 +181,7 @@ class Setup {
} finally {
Files.deleteIfExists(temp);
}
System.out.println("Internal registry added; restart/apply Docker Engine configuration to activate");
IO.println("Internal registry added; restart/apply Docker Engine configuration to activate");
}
static void installRunner(Path root, Map<String, String> env, Commands commands, boolean prepareOnly)
@@ -123,8 +190,8 @@ class Setup {
System.getProperty("os.name").equals("Linux")
&& Set.of("amd64", "x86_64").contains(System.getProperty("os.arch")),
"Installer targets the existing Linux x86_64 mini-PC");
Path runnerDir = home().resolve(".local/share/" + Recipe.project(env) + "-runner"),
configDir = home().resolve(".config/" + Recipe.project(env) + "-runner");
Path runnerDir = home().resolve(".local/share/" + runnerName(env) + "-runner"),
configDir = home().resolve(".config/" + runnerName(env) + "-runner");
Path bin = home().resolve(".local/bin"), units = home().resolve(".config/systemd/user");
for (Path path : List.of(runnerDir, configDir, bin, units)) privateDirectory(path);
for (String tool : List.of("node", "java", "mvn", "docker", "git", "systemctl")) executable(env, tool);
@@ -164,11 +231,14 @@ class Setup {
String actual = commands.execute(List.of(runner.toString(), "--version"), root, env, null, false)
.checked();
Pipeline.check(actual.contains("v0.2.13"), "Expected runner v0.2.13");
privateWrite(runnerDir.resolve("config.yaml"), Files.readString(root.resolve("ci/runner/config.yaml")));
Files.copy(
root.resolve("ci/runner/runner.service"),
units.resolve(Recipe.project(env) + "-runner.service"),
StandardCopyOption.REPLACE_EXISTING);
privateWrite(
runnerDir.resolve("config.yaml"),
Files.readString(root.resolve("ci/runner/config.yaml"))
.replace("@RUNNER@", env.getOrDefault("RUNNER_LABEL", "mini-pc")));
privateWrite(
units.resolve(runnerName(env) + "-runner.service"),
Files.readString(root.resolve("ci/runner/runner.service")).replace("@RUNNER_NAME@", runnerName(env)));
Files.copy(
root.resolve("generated/ci/runner/WaitForDocker.java"),
runnerDir.resolve("WaitForDocker.java"),
@@ -189,7 +259,7 @@ class Setup {
"JAVA_HOME=" + quoted(javaHome) + "\nPATH=" + quoted(path) + "\nDOCKER_HOST=" + quoted(dockerHost)
+ "\n");
boolean desktop = dockerHost.endsWith("/.docker/desktop/docker.sock");
Path overrides = units.resolve(Recipe.project(env) + "-runner.service.d");
Path overrides = units.resolve(runnerName(env) + "-runner.service.d");
Path desktopOverride = overrides.resolve("10-docker-desktop.conf");
if (desktop) {
Files.createDirectories(overrides);
@@ -203,7 +273,7 @@ class Setup {
commands.execute(List.of("systemctl", "--user", "daemon-reload"), root, env, null, true)
.checked();
if (prepareOnly) {
System.out.println("Runner installed; registration and startup still required.");
IO.println("Runner installed; registration and startup still required.");
return;
}
commands.execute(List.of("docker", "info"), root, env, null, false).checked();
@@ -221,9 +291,9 @@ class Setup {
"--instance",
required(env, "GITEA_SERVER_URL"),
"--name",
required(env, "RUNNER_LABEL"),
runnerName(env),
"--labels",
required(env, "RUNNER_LABEL") + ":host",
env.getOrDefault("RUNNER_LABEL", "mini-pc") + ":host",
"--token",
token),
runnerDir,
@@ -243,7 +313,7 @@ class Setup {
.checked();
// Replace any previous default.target link with the desktop-login target.
commands.execute(
List.of("systemctl", "--user", "disable", Recipe.project(env) + "-runner.service"),
List.of("systemctl", "--user", "disable", runnerName(env) + "-runner.service"),
root,
env,
null,
@@ -251,13 +321,13 @@ class Setup {
.checked();
}
commands.execute(
List.of("systemctl", "--user", "enable", "--now", Recipe.project(env) + "-runner.service"),
List.of("systemctl", "--user", "enable", "--now", runnerName(env) + "-runner.service"),
root,
env,
null,
true)
.checked();
System.out.println("Runner registered and started; keep the mini-PC and Docker available.");
IO.println("Runner registered and started; keep the mini-PC and Docker available.");
}
static Path executable(Map<String, String> env, String name) {