Say which modules wait for a person's push, and announce the files a merge deleted (hq ADR 0236)

With a gate on the first machine and a rollback after it, a module's build rolls out by
default. The ones kept back say why: the network path a rollback could not cross, the
providers every consumer on a machine drops with, and the stores holding the photos.
A merge's deleted files are announced, so a module whose manifest went is forgotten
rather than asked to build (the public-acme plan failure).
This commit is contained in:
jochen
2026-10-06 18:39:13 +02:00
parent 7f99fb4a85
commit 1fd2914ae1
14 changed files with 61 additions and 5 deletions
+4
View File
@@ -1,6 +1,10 @@
{
"module": "dnsmasq",
"version": "1",
"upgrade": {
"policy": "record",
"why": "the mesh's resolver: a build that breaks it can stop a machine resolving the bus, and then neither the gate's rollback nor a push reaches it (hq ADR 0236, issue 260)"
},
"provides": [
{
"name": "wildcard-resolution",
+11 -4
View File
@@ -256,18 +256,25 @@ export class GiteaClient {
* whose own files moved was not rebuilt (novox/hq issue 252). Read until a page comes back short; past
* `most` files the list is cut and says so, and the mesh then rebuilds everything built from the
* repository, the safe direction. */
async listPullFiles(owner: string, repo: string, index: number, most = 3000): Promise<{ paths: string[]; truncated: boolean }> {
/** Every file a pull request changes, and which of them it deleted: a module whose manifest the merge
* deleted is gone from its source, and the mesh forgets it rather than asking its build (novox/hq ADR
* 0236). The forge says `deleted`; `removed` is read the same. */
async listPullFiles(owner: string, repo: string, index: number, most = 3000): Promise<{ paths: string[]; removed: string[]; truncated: boolean }> {
const paths: string[] = [];
const removed: string[] = [];
let pageSize = 0;
for (let page = 1; ; page++) {
const files = (await this.request<any[]>(`/repos/${owner}/${repo}/pulls/${index}/files?limit=50&page=${page}`)) ?? [];
if (page === 1) pageSize = files.length;
for (const f of files) {
const name = String(f?.filename ?? "");
if (name !== "") paths.push(name);
if (name === "") continue;
paths.push(name);
const status = String(f?.status ?? "");
if (status === "deleted" || status === "removed") removed.push(name);
}
if (files.length === 0 || files.length < pageSize) return { paths, truncated: false };
if (paths.length >= most) return { paths, truncated: true };
if (files.length === 0 || files.length < pageSize) return { paths, removed, truncated: false };
if (paths.length >= most) return { paths, removed, truncated: true };
}
}
+3
View File
@@ -120,6 +120,9 @@ async function pollMerged(client: GiteaClient): Promise<void> {
html_url: pull.html_url,
paths: changed.paths,
paths_truncated: changed.truncated,
// Which of them the merge deleted (novox/hq ADR 0236): a module whose manifest went is forgotten,
// not built.
removed: changed.removed,
});
// Said, because a trigger that fires silently is indistinguishable from one that did not
// fire (novox/hq 04-ISSUES/131) — this line is how an operator knows the mesh was told.
+3 -1
View File
@@ -9,7 +9,8 @@ test("every page of a pull request's files is read, though the forge caps a page
const url = new URL(req.url ?? "", "http://x");
const page = Number(url.searchParams.get("page") ?? "1");
const start = (page - 1) * 50;
const files = Array.from({ length: Math.max(0, Math.min(50, total - start)) }, (_, i) => ({ filename: `modules/m${start + i}/x` }));
const files = Array.from({ length: Math.max(0, Math.min(50, total - start)) }, (_, i) => ({
filename: `modules/m${start + i}/x`, status: start + i === 3 ? "deleted" : "changed" }));
res.setHeader("content-type", "application/json");
res.end(JSON.stringify(files));
});
@@ -21,6 +22,7 @@ test("every page of a pull request's files is read, though the forge caps a page
assert.equal(got.paths.length, total);
assert.equal(got.truncated, false);
assert.equal(new Set(got.paths).size, total);
assert.deepEqual(got.removed, ["modules/m3/x"], "a file the merge deleted is said as deleted");
});
test("a pass asks only the repositories that moved since the last look, every one before the first", async () => {
+4
View File
@@ -1,6 +1,10 @@
{
"module": "keycloak",
"version": "1",
"upgrade": {
"policy": "record",
"why": "every person's sign-in to every site goes through it, and its new version migrates its database on start: a person takes each build, after a backup (hq ADR 0236)"
},
"provides": [
{
"name": "oidc-client",
+4
View File
@@ -1,6 +1,10 @@
{
"module": "minio",
"version": "1",
"upgrade": {
"policy": "record",
"why": "holds the photos themselves (irreplaceable, kept by photos) for its consumers: a person takes each build, after a backup (hq ADR 0236)"
},
"provides": [
{
"name": "s3-bucket",
+4
View File
@@ -1,6 +1,10 @@
{
"module": "mongodb",
"version": "1",
"upgrade": {
"policy": "record",
"why": "a provider whose restart drops every consumer on its machine, and which holds the photos' albums (irreplaceable, kept by photos): a person takes each build, after a backup (hq ADR 0236)"
},
"provides": [
{
"name": "mongodb-database",
+4
View File
@@ -1,6 +1,10 @@
{
"module": "mssql",
"version": "1",
"upgrade": {
"policy": "record",
"why": "a provider whose restart drops every consumer on its machine, and whose new version may upgrade its databases in place: a person takes each build, after a backup (hq ADR 0236)"
},
"provides": [
{
"name": "mssql-database",
+4
View File
@@ -1,6 +1,10 @@
{
"module": "nats",
"version": "1",
"upgrade": {
"policy": "record",
"why": "the bus: replaced only as a planned step a person starts (`bus upgrade`), its streams snapshotted first and checked after (hq ADR 0236); the controller holds this whatever is said here"
},
"provides": [
{
"name": "mesh-bus",
+4
View File
@@ -1,6 +1,10 @@
{
"module": "networkmanager",
"version": "1",
"upgrade": {
"policy": "record",
"why": "the machine's network: a build that breaks it can cut the machine off from the bus, and then neither the gate's rollback nor a push reaches it (hq ADR 0236)"
},
"slug": "nm",
"requires": [
"wildcard-resolution"
+4
View File
@@ -1,6 +1,10 @@
{
"module": "nftables",
"version": "1",
"upgrade": {
"policy": "record",
"why": "the machine's packet filter: a build that breaks it can cut the machine off from the bus, and then neither the gate's rollback nor a push reaches it (hq ADR 0236)"
},
"capabilities": [
"firewall"
],
+4
View File
@@ -1,6 +1,10 @@
{
"module": "postgres",
"version": "1",
"upgrade": {
"policy": "record",
"why": "a provider whose restart drops every consumer on its machine, and whose new major version changes its data's format: a person takes each build, after a backup (hq ADR 0236)"
},
"provides": [
{
"name": "postgres-database",
+4
View File
@@ -1,6 +1,10 @@
{
"module": "sshd",
"version": "1",
"upgrade": {
"policy": "record",
"why": "the operator's way into the machine when the mesh cannot reach it: a build that breaks it is found only when that way is needed, which no gate sees (hq ADR 0236)"
},
"capabilities": [
"package-manager",
"service-manager"
+4
View File
@@ -1,6 +1,10 @@
{
"module": "systemd-networkd",
"version": "1",
"upgrade": {
"policy": "record",
"why": "the machine's network: a build that breaks it can cut the machine off from the bus, and then neither the gate's rollback nor a push reaches it (hq ADR 0236)"
},
"slug": "networkd",
"requires": [
"wildcard-resolution"