S8 apply Palantir formatting to maintained Java sources
All checks were successful
Build, test and publish / verify (push) Successful in 1m39s
All checks were successful
Build, test and publish / verify (push) Successful in 1m39s
This commit is contained in:
270
ci/Setup.java
270
ci/Setup.java
@@ -8,154 +8,280 @@ import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
class Setup {
|
||||
static Path home() { return Path.of(System.getProperty("user.home")); }
|
||||
static String required(Map<String,String> env, String key) {
|
||||
static Path home() {
|
||||
return Path.of(System.getProperty("user.home"));
|
||||
}
|
||||
|
||||
static String required(Map<String, String> env, String key) {
|
||||
String value = env.get(key);
|
||||
if (value == null || value.isBlank()) throw new IllegalArgumentException("Missing " + key);
|
||||
return value;
|
||||
}
|
||||
static String secret(Map<String,String> env, String key) throws IOException {
|
||||
Path path = Path.of(required(env,key));
|
||||
|
||||
static String secret(Map<String, String> env, String key) throws IOException {
|
||||
Path path = Path.of(required(env, key));
|
||||
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(path);
|
||||
if (permissions.stream().anyMatch(p -> p.name().startsWith("GROUP_") || p.name().startsWith("OTHERS_")))
|
||||
if (permissions.stream()
|
||||
.anyMatch(p -> p.name().startsWith("GROUP_") || p.name().startsWith("OTHERS_")))
|
||||
throw new IllegalArgumentException(key + " must refer to an owner-only file (mode 600)");
|
||||
String token = Files.readString(path).strip();
|
||||
if (token.isEmpty()) throw new IllegalArgumentException(key + " is empty");
|
||||
return token;
|
||||
}
|
||||
|
||||
static class ApiFailure extends IOException {
|
||||
final int status;
|
||||
ApiFailure(int status) { super("Gitea HTTP " + status); this.status = status; }
|
||||
|
||||
ApiFailure(int status) {
|
||||
super("Gitea HTTP " + status);
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
static Object api(String server, String token, String method, String path, Object body) throws Exception {
|
||||
Pipeline.check(token.matches("[a-zA-Z0-9_-]+"), "Invalid token format");
|
||||
HttpRequest request = HttpRequest.newBuilder(URI.create(server.replaceAll("/+$", "") + path))
|
||||
.timeout(Duration.ofSeconds(30)).header("Authorization", "token " + token)
|
||||
.timeout(Duration.ofSeconds(30))
|
||||
.header("Authorization", "token " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(Json.write(body)))
|
||||
.method(
|
||||
method,
|
||||
body == null
|
||||
? HttpRequest.BodyPublishers.noBody()
|
||||
: HttpRequest.BodyPublishers.ofString(Json.write(body)))
|
||||
.build();
|
||||
try (HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build()) {
|
||||
try (HttpClient client =
|
||||
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build()) {
|
||||
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) throw new ApiFailure(response.statusCode());
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300)
|
||||
throw new ApiFailure(response.statusCode());
|
||||
return response.body().isBlank() ? null : Json.parse(response.body());
|
||||
}
|
||||
}
|
||||
static void configureGitea(Map<String,String> env) throws Exception {
|
||||
String admin = secret(env,"GITEA_ADMIN_TOKEN_FILE"), registry = secret(env,"REGISTRY_TOKEN_FILE");
|
||||
String user = required(env,"REGISTRY_USER"), platform = required(env,"TARGET_PLATFORM");
|
||||
Pipeline.check(Set.of("linux/amd64","linux/arm64").contains(platform), "Set TARGET_PLATFORM from the verified Synology architecture");
|
||||
|
||||
static void configureGitea(Map<String, String> env) throws Exception {
|
||||
String admin = secret(env, "GITEA_ADMIN_TOKEN_FILE"), registry = secret(env, "REGISTRY_TOKEN_FILE");
|
||||
String user = required(env, "REGISTRY_USER"), platform = required(env, "TARGET_PLATFORM");
|
||||
Pipeline.check(
|
||||
Set.of("linux/amd64", "linux/arm64").contains(platform),
|
||||
"Set TARGET_PLATFORM from the verified Synology architecture");
|
||||
String server = "http://192.168.2.28:3000", base = "/api/v1/repos/wanjakrah/agiledancers";
|
||||
api(server,admin,"PATCH",base,Map.of("has_actions",true));
|
||||
api(server,admin,"PUT",base + "/actions/secrets/REGISTRY_USER",Map.of("data",user));
|
||||
api(server,admin,"PUT",base + "/actions/secrets/REGISTRY_TOKEN",Map.of("data",registry));
|
||||
api(server, admin, "PATCH", base, Map.of("has_actions", true));
|
||||
api(server, admin, "PUT", base + "/actions/secrets/REGISTRY_USER", Map.of("data", user));
|
||||
api(server, admin, "PUT", base + "/actions/secrets/REGISTRY_TOKEN", Map.of("data", registry));
|
||||
String variable = base + "/actions/variables/TARGET_PLATFORM";
|
||||
boolean exists;
|
||||
try { api(server,admin,"GET",variable,null); exists = true; }
|
||||
catch (ApiFailure e) { if (e.status != 404) throw e; 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));
|
||||
try {
|
||||
api(server, admin, "GET", variable, null);
|
||||
exists = true;
|
||||
} catch (ApiFailure e) {
|
||||
if (e.status != 404) throw e;
|
||||
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/agiledancers-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.");
|
||||
}
|
||||
|
||||
static void configureDocker(Path file) throws IOException {
|
||||
Map<String,Object> data = Files.exists(file) ? Json.object(Json.parse(Files.readString(file))) : new LinkedHashMap<>();
|
||||
Map<String, Object> data =
|
||||
Files.exists(file) ? Json.object(Json.parse(Files.readString(file))) : new LinkedHashMap<>();
|
||||
Object entry = data.getOrDefault("insecure-registries", new ArrayList<>());
|
||||
if (!(entry instanceof List<?>)) throw new IllegalArgumentException("Existing insecure-registries must be an array; unchanged");
|
||||
List<Object> registries = new ArrayList<>((List<?>)entry);
|
||||
if (registries.contains("192.168.2.28:3000")) { System.out.println("Internal registry already configured; no change"); return; }
|
||||
registries.add("192.168.2.28:3000"); data.put("insecure-registries",registries);
|
||||
Path absolute = file.toAbsolutePath(), parent = absolute.getParent(); Files.createDirectories(parent);
|
||||
if (!(entry instanceof List<?>))
|
||||
throw new IllegalArgumentException("Existing insecure-registries must be an array; unchanged");
|
||||
List<Object> registries = new ArrayList<>((List<?>) entry);
|
||||
if (registries.contains("192.168.2.28:3000")) {
|
||||
System.out.println("Internal registry already configured; no change");
|
||||
return;
|
||||
}
|
||||
registries.add("192.168.2.28:3000");
|
||||
data.put("insecure-registries", registries);
|
||||
Path absolute = file.toAbsolutePath(), parent = absolute.getParent();
|
||||
Files.createDirectories(parent);
|
||||
if (Files.exists(file)) {
|
||||
Path backup = file.resolveSibling(file.getFileName() + ".before-agiledancers");
|
||||
if (!Files.exists(backup)) Files.copy(file,backup,StandardCopyOption.COPY_ATTRIBUTES);
|
||||
if (!Files.exists(backup)) Files.copy(file, backup, StandardCopyOption.COPY_ATTRIBUTES);
|
||||
}
|
||||
Path temp = Files.createTempFile(parent,"daemon-", ".json");
|
||||
Path temp = Files.createTempFile(parent, "daemon-", ".json");
|
||||
try {
|
||||
Files.writeString(temp,Json.write(data) + "\n");
|
||||
if (Files.exists(file)) Files.setPosixFilePermissions(temp,Files.getPosixFilePermissions(file));
|
||||
Files.move(temp,absolute,StandardCopyOption.ATOMIC_MOVE,StandardCopyOption.REPLACE_EXISTING);
|
||||
} finally { Files.deleteIfExists(temp); }
|
||||
Files.writeString(temp, Json.write(data) + "\n");
|
||||
if (Files.exists(file)) Files.setPosixFilePermissions(temp, Files.getPosixFilePermissions(file));
|
||||
Files.move(temp, absolute, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
} finally {
|
||||
Files.deleteIfExists(temp);
|
||||
}
|
||||
System.out.println("Internal registry added; restart/apply Docker Engine configuration to activate");
|
||||
}
|
||||
static void installRunner(Path root, Map<String,String> env, Commands commands, boolean prepareOnly) throws Exception {
|
||||
Pipeline.check(System.getProperty("os.name").equals("Linux") && Set.of("amd64","x86_64").contains(System.getProperty("os.arch")),
|
||||
|
||||
static void installRunner(Path root, Map<String, String> env, Commands commands, boolean prepareOnly)
|
||||
throws Exception {
|
||||
Pipeline.check(
|
||||
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/agiledancers-runner"), configDir = home().resolve(".config/agiledancers-runner");
|
||||
Path runnerDir = home().resolve(".local/share/agiledancers-runner"),
|
||||
configDir = home().resolve(".config/agiledancers-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);
|
||||
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);
|
||||
String javaHome = System.getProperty("java.home");
|
||||
String version = commands.execute(List.of("node","--version"),root,env,null,false).checked();
|
||||
String version = commands.execute(List.of("node", "--version"), root, env, null, false)
|
||||
.checked();
|
||||
String[] parts = version.replaceFirst("^v", "").split("\\.");
|
||||
int major = Integer.parseInt(parts[0]), minor = Integer.parseInt(parts[1]), patch = Integer.parseInt(parts[2]);
|
||||
Pipeline.check((major == 22 && (minor > 22 || minor == 22 && patch >= 3)) || (major == 24 && minor >= 15) || major >= 26,
|
||||
Pipeline.check(
|
||||
(major == 22 && (minor > 22 || minor == 22 && patch >= 3))
|
||||
|| (major == 24 && minor >= 15)
|
||||
|| major >= 26,
|
||||
"Supported Node version required");
|
||||
Path runner = bin.resolve("act_runner");
|
||||
if (!Files.isExecutable(runner)) {
|
||||
URI uri = URI.create("https://dl.gitea.com/act_runner/0.2.13/act_runner-0.2.13-linux-amd64");
|
||||
byte[] bytes;
|
||||
try (HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build()) {
|
||||
var response = client.send(HttpRequest.newBuilder(uri).timeout(Duration.ofMinutes(2)).build(),HttpResponse.BodyHandlers.ofByteArray());
|
||||
Pipeline.check(response.statusCode() == 200, "Runner download failed"); bytes = response.body();
|
||||
try (HttpClient client = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.build()) {
|
||||
var response = client.send(
|
||||
HttpRequest.newBuilder(uri)
|
||||
.timeout(Duration.ofMinutes(2))
|
||||
.build(),
|
||||
HttpResponse.BodyHandlers.ofByteArray());
|
||||
Pipeline.check(response.statusCode() == 200, "Runner download failed");
|
||||
bytes = response.body();
|
||||
}
|
||||
String hash = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
|
||||
Pipeline.check(hash.equals("3acac8b506ac8cadc88a55155b5d6378f0fab0b8f62d1e0c0450f4ccd69733e2"),"Runner checksum mismatch");
|
||||
Files.write(runner,bytes); Files.setPosixFilePermissions(runner,PosixFilePermissions.fromString("rwxr-xr-x"));
|
||||
String hash = HexFormat.of()
|
||||
.formatHex(MessageDigest.getInstance("SHA-256").digest(bytes));
|
||||
Pipeline.check(
|
||||
hash.equals("3acac8b506ac8cadc88a55155b5d6378f0fab0b8f62d1e0c0450f4ccd69733e2"),
|
||||
"Runner checksum mismatch");
|
||||
Files.write(runner, bytes);
|
||||
Files.setPosixFilePermissions(runner, PosixFilePermissions.fromString("rwxr-xr-x"));
|
||||
}
|
||||
String actual = commands.execute(List.of(runner.toString(),"--version"),root,env,null,false).checked();
|
||||
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/agiledancers-runner.service"),units.resolve("agiledancers-runner.service"),StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.copy(root.resolve("ci/runner/WaitForDocker.java"),runnerDir.resolve("WaitForDocker.java"),StandardCopyOption.REPLACE_EXISTING);
|
||||
privateWrite(runnerDir.resolve("config.yaml"), Files.readString(root.resolve("ci/runner/config.yaml")));
|
||||
Files.copy(
|
||||
root.resolve("ci/runner/agiledancers-runner.service"),
|
||||
units.resolve("agiledancers-runner.service"),
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.copy(
|
||||
root.resolve("ci/runner/WaitForDocker.java"),
|
||||
runnerDir.resolve("WaitForDocker.java"),
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
String dockerHost = env.get("DOCKER_HOST");
|
||||
if (dockerHost == null || dockerHost.isBlank()) dockerHost = commands.execute(List.of("docker","context","inspect","--format","{{.Endpoints.docker.Host}}"),root,env,null,false).checked();
|
||||
String path = javaHome + "/bin:" + executable(env,"node").getParent() + ":" + executable(env,"mvn").getParent() + ":/usr/local/bin:/usr/bin:/bin";
|
||||
privateWrite(configDir.resolve("environment"),"JAVA_HOME=" + quoted(javaHome) + "\nPATH=" + quoted(path) + "\nDOCKER_HOST=" + quoted(dockerHost) + "\n");
|
||||
if (dockerHost == null || dockerHost.isBlank())
|
||||
dockerHost = commands.execute(
|
||||
List.of("docker", "context", "inspect", "--format", "{{.Endpoints.docker.Host}}"),
|
||||
root,
|
||||
env,
|
||||
null,
|
||||
false)
|
||||
.checked();
|
||||
String path = javaHome + "/bin:" + executable(env, "node").getParent() + ":"
|
||||
+ executable(env, "mvn").getParent() + ":/usr/local/bin:/usr/bin:/bin";
|
||||
privateWrite(
|
||||
configDir.resolve("environment"),
|
||||
"JAVA_HOME=" + quoted(javaHome) + "\nPATH=" + quoted(path) + "\nDOCKER_HOST=" + quoted(dockerHost)
|
||||
+ "\n");
|
||||
boolean desktop = dockerHost.endsWith("/.docker/desktop/docker.sock");
|
||||
Path overrides = units.resolve("agiledancers-runner.service.d");
|
||||
Path desktopOverride = overrides.resolve("10-agiledancers-docker-desktop.conf");
|
||||
if (desktop) {
|
||||
Files.createDirectories(overrides);
|
||||
Files.copy(root.resolve("ci/runner/docker-desktop.conf"),desktopOverride,StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.copy(
|
||||
root.resolve("ci/runner/docker-desktop.conf"),
|
||||
desktopOverride,
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
} else {
|
||||
Files.deleteIfExists(desktopOverride);
|
||||
}
|
||||
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."); return; }
|
||||
commands.execute(List.of("docker","info"),root,env,null,false).checked();
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
commands.execute(List.of("docker", "info"), root, env, null, false).checked();
|
||||
if (!Files.exists(runnerDir.resolve(".runner"))) {
|
||||
Path tokenPath = Path.of(env.getOrDefault("RUNNER_REGISTRATION_TOKEN_FILE",configDir.resolve("registration-token").toString()));
|
||||
Map<String,String> tokenEnv = Map.of("RUNNER_TOKEN_FILE",tokenPath.toString());
|
||||
String token = secret(tokenEnv,"RUNNER_TOKEN_FILE");
|
||||
commands.execute(List.of(runner.toString(),"register","--no-interactive","--instance","http://192.168.2.28:3000",
|
||||
"--name","agiledancers-mini-pc","--labels","agiledancers-mini-pc:host","--token",token),runnerDir,env,null,true).checked();
|
||||
Files.setPosixFilePermissions(runnerDir.resolve(".runner"),PosixFilePermissions.fromString("rw-------"));
|
||||
Path tokenPath = Path.of(env.getOrDefault(
|
||||
"RUNNER_REGISTRATION_TOKEN_FILE",
|
||||
configDir.resolve("registration-token").toString()));
|
||||
Map<String, String> tokenEnv = Map.of("RUNNER_TOKEN_FILE", tokenPath.toString());
|
||||
String token = secret(tokenEnv, "RUNNER_TOKEN_FILE");
|
||||
commands.execute(
|
||||
List.of(
|
||||
runner.toString(),
|
||||
"register",
|
||||
"--no-interactive",
|
||||
"--instance",
|
||||
"http://192.168.2.28:3000",
|
||||
"--name",
|
||||
"agiledancers-mini-pc",
|
||||
"--labels",
|
||||
"agiledancers-mini-pc:host",
|
||||
"--token",
|
||||
token),
|
||||
runnerDir,
|
||||
env,
|
||||
null,
|
||||
true)
|
||||
.checked();
|
||||
Files.setPosixFilePermissions(runnerDir.resolve(".runner"), PosixFilePermissions.fromString("rw-------"));
|
||||
}
|
||||
if (desktop) {
|
||||
commands.execute(List.of("systemctl","--user","enable","--now","docker-desktop.service"),root,env,null,true).checked();
|
||||
commands.execute(
|
||||
List.of("systemctl", "--user", "enable", "--now", "docker-desktop.service"),
|
||||
root,
|
||||
env,
|
||||
null,
|
||||
true)
|
||||
.checked();
|
||||
// Replace any previous default.target link with the desktop-login target.
|
||||
commands.execute(List.of("systemctl","--user","disable","agiledancers-runner.service"),root,env,null,true).checked();
|
||||
commands.execute(
|
||||
List.of("systemctl", "--user", "disable", "agiledancers-runner.service"),
|
||||
root,
|
||||
env,
|
||||
null,
|
||||
true)
|
||||
.checked();
|
||||
}
|
||||
commands.execute(List.of("systemctl","--user","enable","--now","agiledancers-runner.service"),root,env,null,true).checked();
|
||||
commands.execute(
|
||||
List.of("systemctl", "--user", "enable", "--now", "agiledancers-runner.service"),
|
||||
root,
|
||||
env,
|
||||
null,
|
||||
true)
|
||||
.checked();
|
||||
System.out.println("Runner registered and started; keep the mini-PC and Docker available.");
|
||||
}
|
||||
static Path executable(Map<String,String> env, String name) {
|
||||
|
||||
static Path executable(Map<String, String> env, String name) {
|
||||
for (String dir : env.getOrDefault("PATH", "").split(File.pathSeparator)) {
|
||||
Path path = Path.of(dir,name); if (Files.isRegularFile(path) && Files.isExecutable(path)) return path.toAbsolutePath();
|
||||
Path path = Path.of(dir, name);
|
||||
if (Files.isRegularFile(path) && Files.isExecutable(path)) return path.toAbsolutePath();
|
||||
}
|
||||
throw new IllegalArgumentException("Missing executable: " + name);
|
||||
}
|
||||
|
||||
static String quoted(String value) {
|
||||
Pipeline.check(!value.contains("\n") && !value.contains("\r"), "Invalid service environment value");
|
||||
return "\"" + value.replace("\\","\\\\").replace("\"","\\\"") + "\"";
|
||||
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
|
||||
}
|
||||
|
||||
static void privateDirectory(Path path) throws IOException {
|
||||
Files.createDirectories(path); Files.setPosixFilePermissions(path,PosixFilePermissions.fromString("rwx------"));
|
||||
Files.createDirectories(path);
|
||||
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rwx------"));
|
||||
}
|
||||
|
||||
static void privateWrite(Path path, String text) throws IOException {
|
||||
if (!Files.exists(path)) Files.createFile(path,PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")));
|
||||
Files.setPosixFilePermissions(path,PosixFilePermissions.fromString("rw-------")); Files.writeString(path,text);
|
||||
if (!Files.exists(path))
|
||||
Files.createFile(path, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")));
|
||||
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rw-------"));
|
||||
Files.writeString(path, text);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user