C1: the gate and the forge hold no branch name; the trunk is the asked branch, a default is the repository's
Builds: design 47 G §428, §507, A-93 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -17,12 +17,19 @@ const rules = [
|
||||
{ branch_name: "release", enable_push: false },
|
||||
];
|
||||
const seen: string[] = [];
|
||||
const opened: { base?: string }[] = [];
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
const path = (req.url ?? "").replace(/^\/api\/v1/, "");
|
||||
const as = req.headers.authorization === adminBasic ? "admin" : "tools";
|
||||
seen.push(`${req.method} ${path} as ${as}`);
|
||||
const send = (status: number, body: unknown) => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(JSON.stringify(body)); };
|
||||
if (path === "/repos/novox/app") return send(200, { full_name: "novox/app", default_branch: "trunk" });
|
||||
if (req.method === "POST" && path === "/repos/novox/app/pulls") {
|
||||
let raw = "";
|
||||
req.on("data", (c) => (raw += c));
|
||||
return req.on("end", () => { opened.push(JSON.parse(raw)); send(201, { number: 7, title: "t", state: "open", html_url: "u" }); });
|
||||
}
|
||||
if (path.includes("/branch_protections")) {
|
||||
if (as !== "admin") return send(403, { message: "user should be an owner or a collaborator with admin write of a repository" });
|
||||
if (req.method === "GET") return send(200, rules);
|
||||
@@ -57,3 +64,10 @@ test("gitea_branch_protection_set still gets the forge's refusal, as the tools'
|
||||
);
|
||||
assert.ok(!seen.some((s) => /^(PATCH|POST|PUT|DELETE) .*branch_protections.* as admin$/.test(s)), seen.join("\n"));
|
||||
});
|
||||
|
||||
// Design 47 G §428: no part's code holds a branch's name; a pull request opened with no base goes to the repository's default branch.
|
||||
test("gitea_create_pull_request with no base opens against the repository's default branch, and a named base stays", async () => {
|
||||
await tool("gitea_create_pull_request").run({ owner: "novox", repo: "app", title: "t", head: "feat/x" });
|
||||
await tool("gitea_create_pull_request").run({ owner: "novox", repo: "app", title: "t", head: "feat/x", base: "release" });
|
||||
assert.deepEqual(opened.map((o) => o.base), ["trunk", "release"]);
|
||||
});
|
||||
|
||||
@@ -93,6 +93,12 @@ export function getGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin): Too
|
||||
// A merge, a status, a comment, a delivery's view and its note are the seat git's acts (seat.ts), asked by the forge
|
||||
// bridge alone: no tool of the module's own does them (novox/hq design 47 *The seat git*).
|
||||
function allGiteaTools(gitea: GiteaClient, admin: (() => GiteaAdmin) | undefined): ToolDefinition[] {
|
||||
// A branch no caller named is the repository's default branch, as the forge says it; no part's code holds one (§428).
|
||||
const defaultBranchOf = async (owner: string, repo: string): Promise<string> => {
|
||||
const found = String((await gitea.getRepo(owner, repo)).default_branch ?? "");
|
||||
if (!found) throw new Error(`the forge names no default branch of ${owner}/${repo}: name the branch`);
|
||||
return found;
|
||||
};
|
||||
return [
|
||||
// ---- Repositories ----
|
||||
{
|
||||
@@ -269,14 +275,14 @@ function allGiteaTools(gitea: GiteaClient, admin: (() => GiteaAdmin) | undefined
|
||||
title: { type: "string", description: "the PR title" },
|
||||
body: { type: "string", description: "the PR body (markdown)" },
|
||||
head: { type: "string", description: "the source branch" },
|
||||
base: { type: "string", description: "the target branch (default main)" },
|
||||
base: { type: "string", description: "the target branch (default: the repository's default branch)" },
|
||||
},
|
||||
run: async (args) => ({
|
||||
pull: await gitea.createPullRequest(String(args.owner), String(args.repo), {
|
||||
title: String(args.title),
|
||||
body: args.body ? String(args.body) : undefined,
|
||||
head: String(args.head),
|
||||
base: args.base ? String(args.base) : "main",
|
||||
base: args.base ? String(args.base) : await defaultBranchOf(String(args.owner), String(args.repo)),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
@@ -679,7 +685,7 @@ function allGiteaTools(gitea: GiteaClient, admin: (() => GiteaAdmin) | undefined
|
||||
repo: { type: "string", description: "the repository name" },
|
||||
number: { type: "number", description: "a pull request's number: its head is read, and its base's protection" },
|
||||
sha: { type: "string", description: "a commit, or the start of one, instead of a pull request" },
|
||||
base: { type: "string", description: "with sha: the branch whose protection is read (default main)" },
|
||||
base: { type: "string", description: "with sha: the branch whose protection is read (default: the repository's default branch)" },
|
||||
},
|
||||
run: async (args) => {
|
||||
const owner = String(args.owner ?? "").trim(), repo = String(args.repo ?? "").trim();
|
||||
@@ -702,7 +708,7 @@ function allGiteaTools(gitea: GiteaClient, admin: (() => GiteaAdmin) | undefined
|
||||
if (!ref) throw new Error("name a pull request's number or a commit");
|
||||
const combined = await gitea.combinedStatus(owner, repo, ref);
|
||||
const statuses = newestByContext(combined.statuses);
|
||||
base = base || "main";
|
||||
base = base || (await defaultBranchOf(owner, repo));
|
||||
let rule: BranchProtection | null = null, unreadable: string | undefined;
|
||||
try {
|
||||
rule = await gitea.appliedProtection(owner, repo, base);
|
||||
|
||||
@@ -283,10 +283,10 @@ func tools(h *Holder, l *listening) []stdio.Tool {
|
||||
{Name: "delivery_status",
|
||||
Description: "Whether this owner works: how many delivery records are open and ended, what is owed to the forge and not yet " +
|
||||
"answered, how many owed effects were tried since it started (done, failed and tried again, given up) and when the last " +
|
||||
"was, whether its events arrive, the last refusals, and source.main as the gate's bucket source holds it.",
|
||||
"was, whether its events arrive, the last refusals, and the source revision of every branch the gate's bucket source holds.",
|
||||
Run: func(map[string]any) (any, error) {
|
||||
out := h.Status(l.get())
|
||||
out["source-main"] = h.SourceMain()
|
||||
out["source"] = h.SourceAll()
|
||||
return out, nil
|
||||
}},
|
||||
}
|
||||
|
||||
@@ -16,9 +16,6 @@ import (
|
||||
// (switch.go, §613), never written; `source` is the gate's source revision per watched branch (§15, §291), served by the verb
|
||||
// `source` below.
|
||||
|
||||
// MergeBranch is the trunk branch each repository's merge branch names in a source key.
|
||||
const MergeBranch = "main"
|
||||
|
||||
// changeAttempts is how often a lost compare-and-set is read and applied again before it is said.
|
||||
const changeAttempts = 10
|
||||
|
||||
@@ -155,7 +152,7 @@ func (kvSource) Branches() ([]string, error) {
|
||||
|
||||
// SourceRevision is the seat's verb source (§690): given a branch, every repository that branch's source key names, its
|
||||
// trunk branch and head, the group that moved it last and its revision, or why it cannot be read; given none, `branches`,
|
||||
// the branch of each source key the gate keeps. A key names each repository's merge branch only (MergeBranch). Outside the lock.
|
||||
// the branch of each source key the gate keeps. The trunk of each repository is the branch asked, the key's own (§428, §507). Outside the lock.
|
||||
func (h *Holder) SourceRevision(branch string) seat.SourceAnswer {
|
||||
if h.Source == nil {
|
||||
return seat.SourceAnswer{Error: "no source bucket"}
|
||||
@@ -179,24 +176,8 @@ func (h *Holder) SourceRevision(branch string) seat.SourceAnswer {
|
||||
}
|
||||
out := seat.SourceAnswer{Branch: branch, Group: held.Group, Revision: int64(revision)}
|
||||
for repository, head := range held.Heads {
|
||||
out.Repositories = append(out.Repositories, seat.SourceHead{Repository: repository, Trunk: MergeBranch, Head: head})
|
||||
out.Repositories = append(out.Repositories, seat.SourceHead{Repository: repository, Trunk: branch, Head: head})
|
||||
}
|
||||
sort.Slice(out.Repositories, func(i, j int) bool { return out.Repositories[i].Repository < out.Repositories[j].Repository })
|
||||
return out
|
||||
}
|
||||
|
||||
// SourceMain is source.main as the bucket source holds it, read and never written: its group and each repository's trunk
|
||||
// head, or why it cannot be read. Outside the lock.
|
||||
func (h *Holder) SourceMain() map[string]any {
|
||||
if h.Source == nil {
|
||||
return map[string]any{"error": "no source bucket"}
|
||||
}
|
||||
held, _, err := h.Source.Read(MergeBranch)
|
||||
switch {
|
||||
case err != nil:
|
||||
return map[string]any{"error": err.Error()}
|
||||
case held == nil:
|
||||
return map[string]any{"heads": map[string]string{}, "says": "source.main is not written yet"}
|
||||
}
|
||||
return map[string]any{"group": held.Group, "heads": held.Heads}
|
||||
}
|
||||
|
||||
@@ -84,3 +84,22 @@ func TestTheGateStatesItsPinsThenSaysPinsStated(t *testing.T) {
|
||||
t.Fatalf("%d %v %+v", n, err, events)
|
||||
}
|
||||
}
|
||||
|
||||
// Design 47 G §428, §507: no part's code holds a branch's name; the trunk of each repository a source revision names is the
|
||||
// branch whose key was read, and delivery_status says every branch's source, not one fixed branch's.
|
||||
func TestTheSourceAnswerNamesTheBranchItWasAskedAndStatusSaysEveryBranch(t *testing.T) {
|
||||
src := &branchSource{}
|
||||
for _, b := range []string{"release", "feat"} {
|
||||
_, _ = src.Set(b, func(*Source) (Source, bool) {
|
||||
return Source{Group: "g-" + b, Heads: map[string]string{"novox/app": "c1"}}, true
|
||||
})
|
||||
}
|
||||
h := &Holder{Now: time.Now, Source: src}
|
||||
got := h.SourceRevision("release")
|
||||
if len(got.Repositories) != 1 || got.Repositories[0].Trunk != "release" {
|
||||
t.Fatalf("the trunk is the asked branch: %+v", got.Repositories)
|
||||
}
|
||||
if all, ok := h.SourceAll()["branches"].([]seat.SourceAnswer); !ok || len(all) != 2 {
|
||||
t.Fatalf("status says every branch's source: %+v", h.SourceAll())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,6 @@ import (
|
||||
seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
|
||||
)
|
||||
|
||||
// The holders the gate reads or hears, by the names their modules go by (the gate's own constants, one per reader: the
|
||||
// manifest's reads and consumes name them).
|
||||
const (
|
||||
tellerModule = "mesh-delivery-teller"
|
||||
viewModule = "mesh-delivery-view"
|
||||
forgeBridgeModule = "mesh-delivery-forge-bridge"
|
||||
)
|
||||
|
||||
// The values the gate keeps in the seat mesh-delivery's states. The seat's types are the layout and meshdeliveryclient
|
||||
// holds their rules and codecs; Source, Owed and Answer are used as they are, and the three whose seat form differs from the
|
||||
// form the gate works in (a pull request that holds its head, the router's ask as bytes, a turn's times as plain times)
|
||||
|
||||
Reference in New Issue
Block a user