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:
@@ -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[]> {
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]],
|
||||
|
||||
Reference in New Issue
Block a user