S19: establish engineering repository

This commit is contained in:
2026-11-08 15:15:50 +01:00
commit c25ce11a1b
84 changed files with 6599 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** Gitea 1.25 API adapter. Never include response bodies or tokens in errors. */
class ReleaseApi {
record Tag(ReleaseVersion version, String sha, String object) {}
final String server, token, base;
ReleaseApi(Map<String, String> env) {
server = Setup.required(env, "GITEA_SERVER_URL");
var uri = URI.create(server);
Pipeline.check(
List.of("http", "https").contains(uri.getScheme())
&& uri.getUserInfo() == null
&& uri.getQuery() == null
&& uri.getFragment() == null
&& (uri.getPath().isEmpty() || uri.getPath().equals("/")),
"Gitea must use a server-root URL");
String repository = Setup.required(env, "CI_REPOSITORY");
Pipeline.check(repository.matches("[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+"), "Invalid repository name");
base = "/api/v1/repos/" + repository;
token = Setup.required(env, "RELEASE_TOKEN");
}
Object request(String method, String path, Object body) throws Exception {
return Setup.api(server, token, method, base + path, body);
}
String head() throws Exception {
return sha(
Json.object(Json.object(request("GET", "/branches/main", null)).get("commit"))
.get("id"));
}
static String sha(Object value) {
String result = Objects.toString(value, "");
Pipeline.check(result.matches("[0-9a-f]{40}"), "Invalid release commit SHA");
return result;
}
List<Tag> tags() throws Exception {
List<Tag> tags = new ArrayList<>();
var seen = new java.util.HashSet<String>();
for (int page = 1; ; page++) {
var entries = (List<?>) request("GET", "/tags?limit=50&page=" + page, null);
if (entries.isEmpty()) return tags;
for (Object entry : entries) {
var tag = Json.object(entry);
String name = Objects.toString(tag.get("name"), "");
Pipeline.check(
seen.add(name),
"Remote tag pagination repeated a tag; cannot establish a safe version baseline");
// Unknown version-looking tags require explicit reconciliation, not a second baseline.
if (!name.matches("v?[0-9].*")) continue;
ReleaseVersion version = ReleaseVersion.parse(name);
String commit = sha(Json.object(tag.get("commit")).get("sha"));
String object = sha(tag.get("id"));
tags.add(new Tag(version, commit, object));
}
}
}
void annotated(Tag tag) throws Exception {
var data = Json.object(request("GET", "/git/tags/" + tag.object(), null));
Pipeline.check(
tag.version().toString().equals(data.get("tag"))
&& tag.sha().equals(Json.object(data.get("object")).get("sha")),
"Release tag must be annotated and point directly to its recorded commit");
}
void create(ReleaseVersion version, String commit, String run) throws Exception {
request(
"POST",
"/tags",
Map.of(
"tag_name",
version.toString(),
"target",
commit,
"message",
"Release " + version + "\nCommit: " + commit + "\nDeployment attempt: " + run));
Tag tag = tags().stream()
.filter(t -> t.version().equals(version))
.findFirst()
.orElseThrow();
Pipeline.check(tag.sha().equals(commit), "Created release tag does not match selected commit");
annotated(tag);
}
String verified(String commit) throws Exception {
// A successful build.yaml main-push run includes verification AND successful image publication.
// Do not accept another workflow, a branch build, or merely an existing image/tag.
var seen = new java.util.HashSet<String>();
for (int page = 1; ; page++) {
var response = Json.object(request(
"GET",
"/actions/runs?head_sha=" + commit + "&event=push&branch=main&status=success&limit=50&page=" + page,
null));
var runs = (List<?>) response.get("workflow_runs");
if (runs.isEmpty()) break;
boolean fresh = false;
for (Object entry : runs) {
var run = Json.object(entry);
fresh |= seen.add(Objects.toString(run.get("id")));
if (commit.equals(run.get("head_sha"))
&& "push".equals(run.get("event"))
&& List.of("main", "refs/heads/main").contains(Objects.toString(run.get("head_branch")))
&& "completed".equals(run.get("status"))
&& "success".equals(run.get("conclusion"))
&& "build.yaml@refs/heads/main".equals(run.get("path"))) {
return Objects.toString(run.get("id"));
}
}
// Older Gitea installations may ignore some list filters/pagination.
// Validate each entry locally and stop if a page makes no progress.
if (!fresh) break;
}
throw new IllegalArgumentException("No successful build.yaml main-push run verifies and publishes this SHA");
}
}