All checks were successful
Build, test and publish / verify (push) Successful in 6m34s
125 lines
5.5 KiB
Java
125 lines
5.5 KiB
Java
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",
|
|
"Agiledancers " + 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");
|
|
}
|
|
}
|