GIT11: the seat git's tag writes go/vMAJOR.MINOR.PATCH at the commit a released version was built from

A tag at that commit is answered performed; at another commit, refused (a tag is never moved). The builder alone calls it:
the bus refuses any other account from the definition's callers.

Builds: design 47 G §370

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 11:37:36 +01:00
parent a00429bc80
commit 5daf3a333a
4 changed files with 41 additions and 3 deletions
+18
View File
@@ -496,6 +496,24 @@ export class GiteaClient {
return list.map((t) => String(t?.name ?? "")).filter((n) => n !== "");
}
/** The commit a tag names, or null when the repository has no such tag (the forge's 404 only). */
async tagCommit(owner: string, repo: string, tag: string): Promise<string | null> {
try {
const t = await this.request<any>(`${repoPath(owner, repo)}/tags/${pathSegments("tag", tag)}`);
const sha = String(t?.commit?.sha ?? t?.id ?? "");
if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`the forge named no commit for ${owner}/${repo}'s tag ${tag}`);
return sha;
} catch (err) {
if (/: 404 /.test(err instanceof Error ? err.message : "")) return null;
throw err;
}
}
/** Write a lightweight tag at a commit (design 47 §370): what the seat git's `tag` does. */
async createTag(owner: string, repo: string, tag: string, commit: string): Promise<void> {
await this.request(`${repoPath(owner, repo)}/tags`, { method: "POST", body: JSON.stringify({ tag_name: tag, target: commit }) });
}
/** A commit's parents, by sha, in order: what a merge commit says it joined (novox/hq issue 381). An answer
* that names no parents is no answer, never an empty list read as a root commit. */
async commitParents(owner: string, repo: string, sha: string): Promise<string[]> {
+3 -2
View File
@@ -181,7 +181,8 @@
"comment",
"view",
"note",
"merge-pull"
"merge-pull",
"tag"
]
}
],
@@ -252,7 +253,7 @@
"type": "string",
"trusted": true,
"runtime": true,
"default": "{\"publish\":[\"mesh-build-go\",\"mesh-build-node\"],\"publish-part\":[\"mesh-build-go\",\"mesh-build-node\"],\"remove\":[\"mesh-delivery-builder\"],\"versions\":[\"*\"],\"pull\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-gate\"],\"branch\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-indexer\"],\"open-pulls\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-indexer\",\"mesh-delivery-gate\"],\"tags\":[\"mesh-delivery-forge-bridge\"],\"trees\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-checker\",\"mesh-delivery-indexer\"],\"file\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-checker\",\"mesh-delivery-indexer\"],\"contains\":[\"mesh-delivery-indexer\",\"mesh-delivery-gate\"],\"statuses\":[\"mesh-delivery-gate\"],\"status\":[\"mesh-delivery-forge-bridge\"],\"comment\":[\"mesh-delivery-forge-bridge\"],\"view\":[\"mesh-delivery-forge-bridge\"],\"note\":[\"mesh-delivery-forge-bridge\"],\"merge-pull\":[\"mesh-delivery-forge-bridge\"]}",
"default": "{\"publish\":[\"mesh-build-go\",\"mesh-build-node\"],\"publish-part\":[\"mesh-build-go\",\"mesh-build-node\"],\"remove\":[\"mesh-delivery-builder\"],\"versions\":[\"*\"],\"pull\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-gate\"],\"branch\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-indexer\"],\"open-pulls\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-indexer\",\"mesh-delivery-gate\"],\"tags\":[\"mesh-delivery-forge-bridge\"],\"trees\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-checker\",\"mesh-delivery-indexer\"],\"file\":[\"mesh-delivery-forge-bridge\",\"mesh-delivery-checker\",\"mesh-delivery-indexer\"],\"contains\":[\"mesh-delivery-indexer\",\"mesh-delivery-gate\"],\"statuses\":[\"mesh-delivery-gate\"],\"status\":[\"mesh-delivery-forge-bridge\"],\"comment\":[\"mesh-delivery-forge-bridge\"],\"view\":[\"mesh-delivery-forge-bridge\"],\"note\":[\"mesh-delivery-forge-bridge\"],\"merge-pull\":[\"mesh-delivery-forge-bridge\"],\"tag\":[\"mesh-delivery-builder\"]}",
"description": "per verb of the seats the module holds, the callers that may call it (JSON object of verb to callers; * every proved caller, operator the operator's session); each holder reads the lines of its own verbs. The package seats' lines are design 51's (empty is that list for them); the seat git's are design 47's: its reads the parts that declare them in invokes, its acts (status, comment, view, note, merge-pull) the forge bridge alone, and a verb of git with no line is called by no one"
}
},
+15 -1
View File
@@ -22,7 +22,7 @@ import {
seat as GIT, verbs as GIT_VERBS,
type BranchAnswer, type ContainsAnswer, type CommentAnswer, type ErrorKind, type FileAnswer, type MergePullAnswer, type Merging, type NoteAnswer,
type OpenPull, type OpenPullsAnswer, type Pull, type PullAnswer, type Status, type StatusAnswer, type StatusesAnswer,
type TagsAnswer, type TreesAnswer, type ViewAnswer,
type TagAnswer, type TagsAnswer, type TreesAnswer, type ViewAnswer,
} from "@novox/mesh-delivery-contracts-git";
import type { CommitStatus, GiteaClient, GiteaComment, GiteaPull } from "./client.js";
import { MAX_FILE_BYTES } from "./client.js";
@@ -95,6 +95,8 @@ export function repositoryOf(raw: unknown): { owner: string; repo: string; full:
}
const SHA = /^[0-9a-f]{40}$/;
/** The git tag of a released Go version (design 51; design 47 §370). */
const GO_TAG = /^go\/v\d+\.\d+\.\d+$/;
const STATES = new Set(["pending", "success", "failure", "error", "warning"]);
function commitOf(name: string, raw: unknown): string {
const sha = String(raw ?? "").trim();
@@ -280,6 +282,18 @@ export function seatTools(gitea: GiteaClient, deps: SeatDeps): ToolDefinition[]
}),
verb(GIT_VERBS.mergePull, "The seat git's merge-pull: an act of the forge bridge, a pull request merged at the head the caller names; it judges no check.",
async (args): Promise<MergePullAnswer> => mergePull(gitea, args, deps.registered)),
verb(GIT_VERBS.tag, "The seat git's tag: the git tag go/vMAJOR.MINOR.PATCH of a released version, written at the commit its build was made from; a tag that is at that commit is performed.",
async (args): Promise<TagAnswer> => {
const { owner, repo } = repositoryOf(args.repository);
const tag = String(args.tag ?? "");
if (!GO_TAG.test(tag)) throw new Refused(`${JSON.stringify(tag)} is not go/vMAJOR.MINOR.PATCH`);
const commit = commitOf("commit", args.commit);
const at = await gitea.tagCommit(owner, repo, tag);
if (at === commit) return { tag, performed: true };
if (at !== null) throw new Refused(`${tag} is at ${at.slice(0, 12)} already, not at ${commit.slice(0, 12)}; a tag is never moved`);
await gitea.createTag(owner, repo, tag, commit);
return { tag, performed: true };
}),
];
}
+5
View File
@@ -536,6 +536,7 @@ test("every read and act of the seat git answers its contract, and an error is a
const gone = () => { throw new Error("Gitea API GET /repos/novox/app/pulls/9: 404 Not Found"); };
const down = () => { throw new Error("Gitea API GET /repos/novox/app/x: 502 Bad Gateway"); };
const pr = { number: 9, title: "t", state: "open", merged: false, head: "feat/x", head_sha: SHA, base: "main", user: "jochen", html_url: "http://f/9", mergeable: true };
const tagged: string[] = [];
let comments: { id: number; body: string }[] = [], noteShown = "", added = 0, noteWritten = 0;
const forge: Record<string, any> = {
getPullRequest: async (_o: string, _r: string, n: number) => (n === 9 ? pr : gone()),
@@ -583,6 +584,10 @@ test("every read and act of the seat git answers its contract, and an error is a
["contains", "forge down", { repository: "novox/app", commit: SHA, in: OTHER }, { compare: down }, (a) => [a.contains, a["error-kind"]], [undefined, "failed"]],
["statuses", "newest of each", { repository: "novox/app", commit: SHA }, {}, (a) => [a.combined, a.statuses.map((s: any) => s.context)], ["success", ["mesh/check"]]],
["statuses", "forge down", { repository: "novox/app", commit: SHA }, { combinedStatus: down }, (a) => [a.statuses, a["error-kind"]], [undefined, "failed"]],
["tag", "written", { repository: "novox/app", tag: "go/v1.2.3", commit: SHA }, { tagCommit: async () => null, createTag: async (_o: string, _r: string, tag: string, c: string) => { tagged.push(`${tag}@${c}`); } }, (a) => [a.performed, tagged.splice(0)], [true, [`go/v1.2.3@${SHA}`]]],
["tag", "already at that commit", { repository: "novox/app", tag: "go/v1.2.3", commit: SHA }, { tagCommit: async () => SHA, createTag: async () => { throw new Error("written again"); } }, (a) => [a.performed, a.error], [true, undefined]],
["tag", "at another commit is never moved", { repository: "novox/app", tag: "go/v1.2.3", commit: SHA }, { tagCommit: async () => OTHER }, (a) => [a.performed, a["error-kind"]], [undefined, "refused"]],
["tag", "no Go version", { repository: "novox/app", tag: "v1", commit: SHA }, {}, (a) => [a.performed, a["error-kind"]], [undefined, "refused"]],
["status", "refused state", { id: "s1", repository: "novox/app", commit: SHA, context: "c", state: "bogus" }, {}, (a) => [a.performed, a["error-kind"]], [undefined, "refused"]],
["comment", "first time", { id: "c1", repository: "novox/app", number: 9, text: "hi" }, {}, (a) => [a.performed, a.held, added], [true, undefined, 1]],
["comment", "forge down", { id: "c2", repository: "novox/app", number: 9, text: "hi" }, { listComments: down }, (a) => [a.performed, a["error-kind"]], [undefined, "failed"]],