diff --git a/modules/gitea/README.md b/modules/gitea/README.md index 853e59a1..09c8fada 100644 --- a/modules/gitea/README.md +++ b/modules/gitea/README.md @@ -275,12 +275,17 @@ reported no key gets no entry, and its client says it waits. Keys titled `mesh:` a key added by hand never is. Every consumer is made again every ten minutes and at once when a key changes. `go test ./cmd/forge-account-provider ./internal/forgeaccount` runs it against a fake forge. -## The tools' account reads every repository (the operator, 2026-10-26) +## The mesh's accounts read every repository (the operator, 2026-10-26) The tools act as the plain account `mesh-tools` (ADR 0365 §2, issue 469), which writes to the organisation `novox` -through its team. Any other repository it saw only when public, so a private repository of a user answered every tool -404 and was missing from every list. `readall.ts` makes the account a collaborator that **reads only** on each -repository it cannot read: one repository at a time, no organisation membership, the least the forge offers. +through its team; the build modules fetch over ssh as `nox-builder` (ADR 0466 §3), in a team that reads the +organisation's code; the machines' agents use `nox-agent` (ADR 0410 §1). Any other repository each saw only when public, +so a private repository of a user answered every tool 404 and every build's fetch "not authorized to read". `readall.ts` +makes each of these accounts (one list, `meshAccounts` in `tools-account.ts`, whose two Go names +`internal/forgeaccount`'s test holds to its own constants) a collaborator that **reads only** on each repository it +cannot read: one repository at a time, no organisation membership, the least the forge offers. An account the forge +does not hold yet (the forge-account provider makes `nox-builder` and `nox-agent` when their first consumer asks) is +named and asked nothing; the start's pass is run again ten minutes apart until every account was there once. - **When:** one pass over every repository of every user and organisation at the start (a forge not answering yet is asked again a minute apart until one pass ran), and on the forge's system webhook: `forge.repository.created` and @@ -292,11 +297,12 @@ repository it cannot read: one repository at a time, no organisation membership, the forge will not answer is named as refused (or, in the listing, with permission `unknown`) and the pass goes on. - **With what:** the site administrator's login (`MESH_GITEA_ADMIN_PASSWORD_FILE`), because adding a collaborator asks a repository's administration, which on another user's repository only the site administrator holds. It is used for - four calls alone: list every repository, read one, read the account's permission on one, add the account as a read - collaborator. The tools' own calls keep their token. -- **Tools:** `gitea_unreadable_repos` (every repository the account cannot read, the permission the forge gives it - there and why) and `gitea_grant_read` (the same grant by hand, now, on every repository or on those named; each grant - read back from the forge, a refusal answered in the forge's own words). + five calls alone: list every repository, read one, whether an account exists, read an account's permission on one, + add an account as a read collaborator. The tools' own calls keep their token. +- **Tools:** `gitea_unreadable_repos` (per account, every repository it cannot read, the permission the forge gives + it there and why, a count per account and the accounts the forge does not hold) and `gitea_grant_read` (the same + grant by hand, now, on every repository or on those named, for every account or those named; each grant read back + from the forge, a refusal answered in the forge's own words). ## Files it does not know diff --git a/modules/gitea/client.ts b/modules/gitea/client.ts index c62dc857..418c3e49 100644 --- a/modules/gitea/client.ts +++ b/modules/gitea/client.ts @@ -1013,6 +1013,15 @@ export class GiteaAdmin { return forgeRepository(res.body); } + /** Whether the forge holds an account of this name. */ + async accountExists(user: string): Promise { + const path = `/users/${encodeURIComponent(user)}`; + const res = await this.request(path); + if (res.status === 404) return false; + if (res.status !== 200) GiteaAdmin.fail(path, res); + return true; + } + /** What the forge itself says an account may do on a repository: none, read, write, admin or owner. */ async repositoryPermission(owner: string, repo: string, user: string): Promise { const path = `${repoPath(owner, repo)}/collaborators/${encodeURIComponent(user)}/permission`; diff --git a/modules/gitea/index.ts b/modules/gitea/index.ts index 4d2e39fb..70577358 100644 --- a/modules/gitea/index.ts +++ b/modules/gitea/index.ts @@ -61,7 +61,7 @@ import { keepSiblings, noteComposed, noteVerdict, readSiblings, siblingsFile, si import { bindInBackground } from "./bind.js"; import { announcementOf, keepTook, lateVerdictComment, outOfDateWords, sameCommit, tookFile, weighTrunks } from "./trunk.js"; import { ReadAll, readAllTools } from "./readall.js"; -import { TOOLS_ACCOUNT } from "./tools-account.js"; +import { TOOLS_ACCOUNT, meshAccounts } from "./tools-account.js"; // Without a way to a token — configured, or mintable with the admin account (token.ts) — there is // nothing to watch; log and stay quiet rather than crash the runtime. With one, the first poll mints @@ -502,11 +502,12 @@ if (!gitea) pace.unheard("the forge is not watched: no way to a token", false); // Said with `emit`, not the SDK's `keeps`, which the toolchain's SDK does not have yet: switch to `keeps` once that SDK is 0.1.15 or later (issue 1852). await emit("states", statesAnnouncement(process.env.MESH_MODULE || "gitea", process.env.MESH_NODE)); -// **The tools' account reads every repository** (readall.ts; the operator, 2026-10-26): granted with the site +// **The mesh's accounts read every repository** (readall.ts; the operator, 2026-10-26): the tools', the build modules' +// and the machines' agents' (meshAccounts, tools-account.ts), granted with the site // administrator's login, which the module holds for setting the forge up and uses here for that grant alone. Made on // first use, so a module whose admin login is missing still serves the rest and the tools say why. let readAllOnce: ReadAll | null = null; -const readAll = (): ReadAll => (readAllOnce ??= new ReadAll(GiteaAdmin.fromEnv(), process.env.MESH_GITEA_TOOLS_USER || TOOLS_ACCOUNT)); +const readAll = (): ReadAll => (readAllOnce ??= new ReadAll(GiteaAdmin.fromEnv(), meshAccounts(process.env.MESH_GITEA_TOOLS_USER || TOOLS_ACCOUNT))); // gitea_events is served whether or not the forge is watched: it says so (novox/hq issue 729). registerModuleTools("gitea", () => [eventsTool(pace), ...readAllTools(readAll), ...(gitea ? [commitStatusTool({ @@ -554,7 +555,7 @@ async function listenForForge(client: GiteaClient): Promise { act: async (event) => { // A repository made, or one not yet seen readable, is granted read before anything acts on it (readall.ts). await Promise.resolve().then(() => readAll().onEvent(event)).catch((err) => - console.error(`[gitea] whether ${TOOLS_ACCOUNT} reads the repository of ${event.name} could not be settled — ${err instanceof Error ? err.message : err}`)); + console.error(`[gitea] whether the mesh's accounts read the repository of ${event.name} could not be settled — ${err instanceof Error ? err.message : err}`)); await actOn(client, event); } }); await new Promise((resolve, reject) => { @@ -609,15 +610,20 @@ if (gitea) { void listenForForge(client); // The start's one pass over every repository (readall.ts), not awaited for the same reason. A forge not answering yet // is asked again, a minute apart, until one pass has run; after that only the forge's events grant. - const readAllAtStart = (): void => void Promise.resolve().then(() => readAll().atStart()).catch((err) => { - console.error(`[gitea] whether ${TOOLS_ACCOUNT} reads every repository could not be settled; asked again in a minute — ${err instanceof Error ? err.message : err}`); + // An account of the list the forge does not hold yet (the forge-account provider makes nox-builder and nox-agent when + // their first consumer asks, and the forge sends no event for a user made) is looked for again ten minutes apart + // until every account was granted once; the line of each pass names it. + const readAllAtStart = (): void => void Promise.resolve().then(() => readAll().atStart()).then((r) => { + if (r.absent.length) setTimeout(readAllAtStart, 600_000); + }, (err) => { + console.error(`[gitea] whether the mesh's accounts read every repository could not be settled; asked again in a minute — ${err instanceof Error ? err.message : err}`); setTimeout(readAllAtStart, 60_000); }); try { readAll(); readAllAtStart(); } catch (err) { - console.error(`[gitea] ${TOOLS_ACCOUNT} is not given read on every repository: no site administrator's login — ${err instanceof Error ? err.message : err}`); + console.error(`[gitea] the mesh's accounts are not given read on every repository: no site administrator's login — ${err instanceof Error ? err.message : err}`); } // Main takes no push and only the tools' account merges, in every repository, kept and not only when a token is // minted: at the start and every ten minutes after, so a repository made later gets it too (issue 948, ADR 0398 §2). diff --git a/modules/gitea/internal/forgeaccount/user_test.go b/modules/gitea/internal/forgeaccount/user_test.go index d1df3f91..d60c91e0 100644 --- a/modules/gitea/internal/forgeaccount/user_test.go +++ b/modules/gitea/internal/forgeaccount/user_test.go @@ -1,6 +1,10 @@ package forgeaccount -import "testing" +import ( + "os" + "regexp" + "testing" +) func TestForgeUserOfAMachineAccountIsTheOneAgentUser(t *testing.T) { if got := ForgeUser("g14", "machine"); got != "nox-agent" { @@ -15,6 +19,18 @@ func TestForgeUserOfAMachineAccountIsTheOneAgentUser(t *testing.T) { if got := NodeAccount("g14"); got != "agent-g14" { t.Fatalf("the token tool's per-node account = %q", got) } + // The accounts that read every repository (readall.ts, meshAccounts in tools-account.ts) name these two users: the + // list is held to the Go constants here, so the two cannot drift apart (the operator, 2026-10-26). + ts, err := os.ReadFile("../../tools-account.ts") + if err != nil { + t.Fatal(err) + } + for name, want := range map[string]string{"BUILDER_ACCOUNT": BuilderUser, "AGENT_ACCOUNT": AgentUser} { + m := regexp.MustCompile(`export const ` + name + ` = "([^"]+)";`).FindSubmatch(ts) + if m == nil || string(m[1]) != want { + t.Errorf("tools-account.ts %s = %q, want %q (internal/forgeaccount)", name, m, want) + } + } } func TestRemoveKeysTitledTakesOnlyTheMeshKeysOfThatTitle(t *testing.T) { diff --git a/modules/gitea/readall.ts b/modules/gitea/readall.ts index cd9d411d..c2b63fd6 100644 --- a/modules/gitea/readall.ts +++ b/modules/gitea/readall.ts @@ -1,5 +1,12 @@ -// The tools' account reads every repository on the forge (the operator, 2026-10-26: "the mesh's forge account should -// get read access to all repositories"). +// The mesh's own forge accounts read every repository on the forge (the operator, 2026-10-26: "the mesh's forge account +// should get read access to all repositories"; and "why wouldn't the forge account get access to all repo's?"). +// +// **Every account the mesh's parts use, from one list** (MESH_ACCOUNTS, tools-account.ts): the tools' `mesh-tools`, the +// build modules' `nox-builder` (their fetch over ssh, ADR 0466 §3) and the machines' `nox-agent` (ssh-client's key, +// ADR 0410 §1). The first pass granted the tools' account alone, so the build modules' fetch of jschoubben/car-hunter +// still failed on ace and g14 ("nox-builder ... is not authorized to read"). The two Go names are the ones of +// internal/forgeaccount, whose test holds the list to them. An account the forge does not hold yet (its consumer never +// asked) is said and left out, never a refusal per repository. // // Since issue 469 the tools act as the plain account `mesh-tools`, which writes to the organisation novox's // repositories through its team (tools-account.ts). Every other repository it saw only when public: a private @@ -32,12 +39,15 @@ import type { ForgeRepository } from "./client.js"; export interface ReadAllForge { everyRepository(): Promise; repository(owner: string, repo: string): Promise; + /** Whether the forge holds the account at all. */ + accountExists(user: string): Promise; repositoryPermission(owner: string, repo: string, user: string): Promise; addReadCollaborator(owner: string, repo: string, user: string): Promise; } -/** A repository the account cannot read, and why. */ +/** A repository an account cannot read, and why. */ export interface Unreadable { + account: string; repository: string; permission: string; private: boolean; @@ -46,16 +56,22 @@ export interface Unreadable { } export interface Granted { - /** Repositories looked at. */ + /** Repositories looked at (for each account). */ checked: number; - /** Made readable now, by a read collaborator. */ - granted: string[]; - /** The forge refused the grant: each repository and the forge's own words. */ - refused: { repository: string; why: string }[]; + /** The accounts looked at: those the forge holds. */ + accounts: string[]; + /** Accounts of the list the forge does not hold yet: nothing is granted to them until it does. */ + absent: string[]; + /** Made readable now, by a read collaborator: each repository and the account. */ + granted: { account: string; repository: string }[]; + /** The forge refused the grant: each repository, the account and the forge's own words. */ + refused: { account: string; repository: string; why: string }[]; } const READS = new Set(["read", "write", "admin", "owner"]); +const said = (err: unknown): string => (err instanceof Error ? err.message : String(err)); + /** Why the account cannot read a repository, from what the forge says of it. */ export function whyUnreadable(repo: ForgeRepository, account: string): string { const parts: string[] = []; @@ -68,31 +84,53 @@ export function whyUnreadable(repo: ForgeRepository, account: string): string { } export class ReadAll { - /** Repositories this process has seen the account read, by full name: an event of one of them asks nothing more. */ - readonly readable = new Set(); + /** Per account, the repositories this process has seen it read, by full name: an event of a repository every account + * was seen reading asks nothing more. */ + readonly readable = new Map>(); + readonly accounts: readonly string[]; - constructor(private readonly forge: ReadAllForge, readonly account: string, - private readonly log: (line: string) => void = (l) => console.log(`[gitea] ${l}`)) {} - - /** Every repository the account cannot read, and why, as the forge says it now. */ - async unreadable(): Promise<{ checked: number; unreadable: Unreadable[] }> { - const repos = await this.forge.everyRepository(); - const out: Unreadable[] = []; - for (const repo of repos) { - try { - const u = await this.judge(repo); - if (u) out.push(u); - } catch (err) { - // A permission the forge would not answer is said for that repository; the rest are still looked at. - out.push({ repository: repo.full_name, permission: "unknown", private: repo.private, archived: repo.archived, - why: `the forge did not answer ${this.account}'s permission on it — ${err instanceof Error ? err.message : String(err)}` }); - } - } - return { checked: repos.length, unreadable: out }; + constructor(private readonly forge: ReadAllForge, accounts: readonly string[] | string, + private readonly log: (line: string) => void = (l) => console.log(`[gitea] ${l}`)) { + this.accounts = [...new Set(typeof accounts === "string" ? [accounts] : accounts)]; + if (!this.accounts.length) throw new Error("no forge account to give read: the list is empty"); + for (const a of this.accounts) this.readable.set(a, new Set()); } - /** Grant read on every repository the account cannot read (or on those named, `owner/name`). */ - async grant(only?: string[]): Promise { + /** The accounts of the list (or of those named) the forge holds, and those it does not hold yet. */ + private async present(only?: string[]): Promise<{ accounts: string[]; absent: string[] }> { + const wanted = only && only.length ? only : [...this.accounts]; + for (const a of wanted) if (!this.accounts.includes(a)) throw new Error(`"${a}" is not one of the mesh's accounts: ${this.accounts.join(", ")}`); + const accounts: string[] = []; + const absent: string[] = []; + for (const a of wanted) ((await this.forge.accountExists(a)) ? accounts : absent).push(a); + return { accounts, absent }; + } + + /** Every repository each account cannot read, and why, as the forge says it now. */ + async unreadable(only?: string[]): Promise<{ checked: number; accounts: { account: string; unreadable: number }[]; absent: string[]; unreadable: Unreadable[] }> { + const { accounts, absent } = await this.present(only); + const repos = await this.forge.everyRepository(); + const out: Unreadable[] = []; + for (const account of accounts) { + for (const repo of repos) { + try { + const u = await this.judge(repo, account); + if (u) out.push(u); + } catch (err) { + // A permission the forge would not answer is said for that repository; the rest are still looked at. + out.push({ account, repository: repo.full_name, permission: "unknown", private: repo.private, archived: repo.archived, + why: `the forge did not answer ${account}'s permission on it — ${said(err)}` }); + } + } + } + return { checked: repos.length, accounts: accounts.map((account) => ({ account, unreadable: out.filter((u) => u.account === account).length })), + absent, unreadable: out }; + } + + /** Grant read on every repository an account cannot read (or on those named, `owner/name`), for every account of the + * list the forge holds (or for those named). */ + async grant(only?: string[], accountsOnly?: string[]): Promise { + const { accounts, absent } = await this.present(accountsOnly); let repos: ForgeRepository[]; if (only && only.length) { repos = []; @@ -104,36 +142,39 @@ export class ReadAll { repos.push(repo); } } else repos = await this.forge.everyRepository(); - const result: Granted = { checked: repos.length, granted: [], refused: [] }; - for (const repo of repos) { - let u: Unreadable | null; - try { - u = await this.judge(repo); - } catch (err) { - // One repository whose permission cannot be read is a refusal of its own, never the end of the pass. - result.refused.push({ repository: repo.full_name, why: `its permission could not be read — ${err instanceof Error ? err.message : String(err)}` }); - continue; - } - if (!u) continue; - try { - await this.forge.addReadCollaborator(repo.owner, repo.name, this.account); - } catch (err) { - result.refused.push({ repository: repo.full_name, why: err instanceof Error ? err.message : String(err) }); - continue; - } - // Read back: the forge's own word that the account reads it now, never the grant's answer alone. - let after: string; - try { - after = await this.forge.repositoryPermission(repo.owner, repo.name, this.account); - } catch (err) { - result.refused.push({ repository: repo.full_name, why: `granted, but its permission could not be read back — ${err instanceof Error ? err.message : String(err)}` }); - continue; - } - if (READS.has(after)) { - this.readable.add(repo.full_name); - result.granted.push(repo.full_name); - } else { - result.refused.push({ repository: repo.full_name, why: `the grant was answered, but the forge still gives ${this.account} "${after}"` }); + const result: Granted = { checked: repos.length, accounts, absent, granted: [], refused: [] }; + for (const account of accounts) { + for (const repo of repos) { + const repository = repo.full_name; + let u: Unreadable | null; + try { + u = await this.judge(repo, account); + } catch (err) { + // One repository whose permission cannot be read is a refusal of its own, never the end of the pass. + result.refused.push({ account, repository, why: `its permission could not be read — ${said(err)}` }); + continue; + } + if (!u) continue; + try { + await this.forge.addReadCollaborator(repo.owner, repo.name, account); + } catch (err) { + result.refused.push({ account, repository, why: said(err) }); + continue; + } + // Read back: the forge's own word that the account reads it now, never the grant's answer alone. + let after: string; + try { + after = await this.forge.repositoryPermission(repo.owner, repo.name, account); + } catch (err) { + result.refused.push({ account, repository, why: `granted, but its permission could not be read back — ${said(err)}` }); + continue; + } + if (READS.has(after)) { + this.readable.get(account)!.add(repository); + result.granted.push({ account, repository }); + } else { + result.refused.push({ account, repository, why: `the grant was answered, but the forge still gives ${account} "${after}"` }); + } } } return result; @@ -142,69 +183,81 @@ export class ReadAll { /** The start's one pass: grant what is missing and say it. Answers the result; a failure is the caller's to say. */ async atStart(): Promise { const r = await this.grant(); - this.log(`${this.account} reads every repository: ${r.checked} looked at, ${r.granted.length} granted read now` + - `${r.granted.length ? ` (${r.granted.join(", ")})` : ""}${r.refused.length ? `, ${r.refused.length} refused: ` + - r.refused.map((x) => `${x.repository} — ${x.why}`).join("; ") : ""}`); + const per = r.accounts.map((a) => { + const g = r.granted.filter((x) => x.account === a).map((x) => x.repository); + return `${a} ${g.length} granted read now${g.length ? ` (${g.join(", ")})` : ""}`; + }); + this.log(`the mesh's accounts read every repository: ${r.checked} looked at; ${per.join("; ")}` + + `${r.absent.length ? `; not on the forge yet, so not granted: ${r.absent.join(", ")}` : ""}` + + `${r.refused.length ? `; ${r.refused.length} refused: ` + r.refused.map((x) => `${x.account} on ${x.repository} — ${x.why}`).join("; ") : ""}`); return r; } - /** A forge event: a repository made (or forked), or the first event of one not seen readable, is granted read now. */ + /** A forge event: a repository made (or forked), or the first event of one some account was not seen reading, is + * granted read now to every account that cannot read it. */ async onEvent(event: { name: string; body: unknown }): Promise { const b = (event.body ?? {}) as { owner?: string; repo?: string }; if (!b.owner || !b.repo) return; const full = `${b.owner}/${b.repo}`; if (event.name === "forge.repository.deleted") { - this.readable.delete(full); + for (const seen of this.readable.values()) seen.delete(full); return; } const made = event.name === "forge.repository.created" || event.name === "forge.fork"; - // Seen readable once, its events ask nothing: so a repository made private after that is not caught here (the - // forge says no change of visibility), only at the next start or by gitea_grant_read. - if (!made && this.readable.has(full)) return; + // Seen readable once by every account, its events ask nothing: so a repository made private after that is not + // caught here (the forge says no change of visibility), only at the next start or by gitea_grant_read. + if (!made && [...this.readable.values()].every((seen) => seen.has(full))) return; const r = await this.grant([full]); - if (r.granted.length) this.log(`${this.account} was given read on ${full}, on the forge's ${event.name}`); - for (const x of r.refused) this.log(`${this.account} could not be given read on ${x.repository} — ${x.why}`); + for (const x of r.granted) this.log(`${x.account} was given read on ${full}, on the forge's ${event.name}`); + for (const x of r.refused) this.log(`${x.account} could not be given read on ${x.repository} — ${x.why}`); } - private async judge(repo: ForgeRepository): Promise { - const permission = await this.forge.repositoryPermission(repo.owner, repo.name, this.account); + private async judge(repo: ForgeRepository, account: string): Promise { + const permission = await this.forge.repositoryPermission(repo.owner, repo.name, account); + const seen = this.readable.get(account)!; if (READS.has(permission)) { - this.readable.add(repo.full_name); + seen.add(repo.full_name); return null; } - this.readable.delete(repo.full_name); - return { repository: repo.full_name, permission, private: repo.private, archived: repo.archived, why: whyUnreadable(repo, this.account) }; + seen.delete(repo.full_name); + return { account, repository: repo.full_name, permission, private: repo.private, archived: repo.archived, why: whyUnreadable(repo, account) }; } } type Tool = { name: string; description: string; input: Record; run: (args: Readonly>) => Promise }; -/** The two tools: what the account cannot read and why, and the grant now. */ +const strings = (v: unknown): string[] | undefined => (Array.isArray(v) ? v.map(String) : undefined); + +/** The two tools: what each account cannot read and why, and the grant now. */ export function readAllTools(readAll: () => ReadAll): Tool[] { return [ { name: "gitea_unreadable_repos", - description: "Every repository on the forge the tools' own account (mesh-tools) cannot read, and why: the " + - "permission the forge gives it there, whether the repository is private or archived, and in words the reason " + - "(a private repository of a user with the account no collaborator, an owner not public). Empty when it reads " + - "every repository. Read with the site administrator's login, which alone sees every repository.", - input: {}, - run: async () => readAll().unreadable(), + description: "Every repository on the forge one of the mesh's own forge accounts cannot read, and why, per account: " + + "the tools' (mesh-tools), the build modules' fetch over ssh (nox-builder) and the machines' agents' (nox-agent). " + + "For each, the permission the forge gives it there, whether the repository is private or archived, and in words " + + "the reason (a private repository of a user with the account no collaborator, an owner not public); a count per " + + "account, and the accounts the forge does not hold yet. Empty when every account reads every repository. Read " + + "with the site administrator's login, which alone sees every repository.", + input: { + accounts: { type: "array", items: { type: "string" }, description: "the accounts to look at; empty or absent, every one of the mesh's" }, + }, + run: async (args) => readAll().unreadable(strings(args.accounts)), }, { name: "gitea_grant_read", - description: "Give the tools' own account (mesh-tools) read on every repository it cannot read now, or on the " + - "repositories named: each becomes a collaborator that reads only, the least the forge offers. The module does " + - "this itself at its start and on the forge's event of a repository made; this is the same act by hand. Answers " + - "how many were looked at, which were granted (each read back from the forge) and which the forge refused, in its " + - "own words. Needs the site administrator's login, used for this grant alone.", + description: "Give each of the mesh's own forge accounts (mesh-tools, nox-builder, nox-agent) read on every " + + "repository it cannot read now, or on the repositories named, or for the accounts named: each becomes a " + + "collaborator that reads only, the least the forge offers. The module does this itself at its start and on the " + + "forge's event of a repository made; this is the same act by hand, such as when a build's fetch says its account " + + "is not authorized to read a repository. Answers how many were looked at, which were granted to which account " + + "(each read back from the forge), which the forge refused, in its own words, and the accounts it does not hold " + + "yet. Needs the site administrator's login, used for this grant alone.", input: { repositories: { type: "array", items: { type: "string" }, description: "owner/name of each repository; empty or absent, every repository" }, + accounts: { type: "array", items: { type: "string" }, description: "the accounts to grant; empty or absent, every one of the mesh's" }, }, - run: async (args) => { - const named = Array.isArray(args.repositories) ? args.repositories.map(String) : undefined; - return readAll().grant(named); - }, + run: async (args) => readAll().grant(strings(args.repositories), strings(args.accounts)), }, ]; } diff --git a/modules/gitea/test/tools-account.test.ts b/modules/gitea/test/tools-account.test.ts index 2e4c1083..5d275189 100644 --- a/modules/gitea/test/tools-account.test.ts +++ b/modules/gitea/test/tools-account.test.ts @@ -16,7 +16,7 @@ import { join } from "node:path"; import { MintedToken } from "../dist/token.js"; import { GiteaClient } from "../dist/client.js"; -import { TOOLS_ACCOUNT, TOOLS_TEAM_UNITS } from "../dist/tools-account.js"; +import { TOOLS_ACCOUNT, TOOLS_TEAM_UNITS, BUILDER_ACCOUNT, AGENT_ACCOUNT, meshAccounts } from "../dist/tools-account.js"; import { repoCheckFacts } from "../dist/pulls.js"; const ADMIN = "mesh-admin"; @@ -447,12 +447,15 @@ test("a mint with keep-main-rules false (the default) writes no branch protectio assert.deepEqual(writes, []); }); -// The tools' account reads every repository (readall.ts; the operator, 2026-10-26): jschoubben/car-hunter and -// jschoubben/snake, private repositories of a user, answered the tools 404 while that user's public ones were seen. +// The mesh's accounts read every repository (readall.ts; the operator, 2026-10-26): jschoubben/car-hunter and +// jschoubben/snake, private repositories of a user, answered the tools 404 while that user's public ones were seen; and +// once the tools' account read them, the build modules' fetch as nox-builder still could not. -test("the start's pass gives the tools' account read, as a collaborator that reads only, on each repository it could not read, and on no other", async () => { +test("the start's pass gives each of the mesh's accounts read, as a collaborator that reads only, on each repository it could not read, and on no other; an account the forge does not hold is said, not refused", async () => { const env = await environment(); await fresh(env).listRepos(); // the account and its team exist, as after any mint + forge.users.set(BUILDER_ACCOUNT, { admin: false, password: "unused", active: true }); // made by the forge-account provider + forge.users.delete(AGENT_ACCOUNT); // no machine's key asked for it yet forgeRepos.splice(0); addRepo("novox/hq", true); addRepo("jschoubben/txt-game", false); @@ -463,14 +466,24 @@ test("the start's pass gives the tools' account read, as a collaborator that rea const { GiteaAdmin } = await import("../dist/client.js"); const { ReadAll, readAllTools } = await import("../dist/readall.js"); const lines: string[] = []; - const readAll = new ReadAll(GiteaAdmin.fromEnv(env), TOOLS_ACCOUNT, (l: string) => lines.push(l)); + const readAll = new ReadAll(GiteaAdmin.fromEnv(env), meshAccounts(), (l: string) => lines.push(l)); + assert.deepEqual(readAll.accounts, [TOOLS_ACCOUNT, BUILDER_ACCOUNT, AGENT_ACCOUNT]); const [unreadableTool, grantTool] = readAllTools(() => readAll); - // What the tool says before: the four the account cannot read, each with why. + // What the tool says before, per account: the four the tools' account cannot read, each with why; the builders' + // account, in no write team here, also not novox/hq; nox-agent not on the forge. const before = await unreadableTool.run({}) as any; assert.equal(before.checked, 6); - assert.deepEqual(before.unreadable.map((u: any) => u.repository).sort(), - ["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "shy/open"]); + const of = (account: string) => before.unreadable.filter((u: any) => u.account === account).map((u: any) => u.repository).sort(); + assert.deepEqual(of(TOOLS_ACCOUNT), ["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "shy/open"]); + assert.deepEqual(of(BUILDER_ACCOUNT), ["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "novox/hq", "shy/open"]); + assert.deepEqual(before.accounts, [{ account: TOOLS_ACCOUNT, unreadable: 4 }, { account: BUILDER_ACCOUNT, unreadable: 5 }]); + assert.deepEqual(before.absent, [AGENT_ACCOUNT]); + assert.match(before.unreadable.find((u: any) => u.account === BUILDER_ACCOUNT && u.repository === "jschoubben/car-hunter").why, + /a private repository of jschoubben, on which nox-builder is no collaborator/); + const builderOnly = await unreadableTool.run({ accounts: [BUILDER_ACCOUNT] }) as any; + assert.deepEqual(builderOnly.accounts, [{ account: BUILDER_ACCOUNT, unreadable: 5 }]); + await assert.rejects(unreadableTool.run({ accounts: ["someone"] }), /not one of the mesh's accounts/); assert.match(before.unreadable.find((u: any) => u.repository === "jschoubben/car-hunter").why, /a private repository of jschoubben, on which mesh-tools is no collaborator/); assert.match(before.unreadable.find((u: any) => u.repository === "shy/open").why, /visibility is limited/); @@ -484,13 +497,27 @@ test("the start's pass gives the tools' account read, as a collaborator that rea const from = forge.calls.length; const r = await readAll.atStart(); - assert.deepEqual(r.granted.sort(), ["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "shy/open"]); + const grantedTo = (account: string) => r.granted.filter((x: any) => x.account === account).map((x: any) => x.repository).sort(); + assert.deepEqual(grantedTo(TOOLS_ACCOUNT), ["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "shy/open"]); + assert.deepEqual(grantedTo(BUILDER_ACCOUNT), ["elsewhere/secret", "jschoubben/car-hunter", "jschoubben/snake", "novox/hq", "shy/open"]); assert.deepEqual(r.refused, []); + assert.deepEqual(r.absent, [AGENT_ACCOUNT]); const puts = forge.calls.slice(from).filter((c) => c.startsWith("PUT ")); - assert.equal(puts.length, 4, "the repositories it read already were left alone"); + assert.equal(puts.length, 9, "the repositories each read already were left alone, and the absent account was asked nothing"); + assert.ok(!puts.some((c) => c.includes(AGENT_ACCOUNT))); assert.ok(puts.every((c) => c.endsWith(` as ${ADMIN}`)), "only the administrator's login grants"); - for (const repo of forgeRepos.filter((x) => x.collaborators.size)) assert.equal(repo.collaborators.get(TOOLS_ACCOUNT), "read", `${repo.full} reads only`); - assert.match(lines.join("\n"), /6 looked at, 4 granted read now/); + for (const repo of forgeRepos.filter((x) => x.collaborators.size)) { + for (const [account, permission] of repo.collaborators) assert.equal(permission, "read", `${account} on ${repo.full} reads only`); + } + assert.equal(forgeRepos.find((x) => x.full === "jschoubben/car-hunter")!.collaborators.get(BUILDER_ACCOUNT), "read"); + assert.match(lines.join("\n"), /6 looked at; mesh-tools 4 granted read now .*; nox-builder 5 granted read now .*; not on the forge yet, so not granted: nox-agent/); + + // The account made later is granted by hand (or at the next pass), its rows like any other. + forge.users.set(AGENT_ACCOUNT, { admin: false, password: "unused", active: true }); + const agent = await grantTool.run({ accounts: [AGENT_ACCOUNT], repositories: ["jschoubben/car-hunter"] }) as any; + assert.deepEqual(agent.granted, [{ account: AGENT_ACCOUNT, repository: "jschoubben/car-hunter" }]); + assert.deepEqual(agent.absent, []); + forge.users.delete(AGENT_ACCOUNT); // After: the account's own token sees every repository, and the tool names none. assert.equal((await fresh(env).listAllRepos()).length, 6); @@ -509,7 +536,8 @@ test("on the forge's events: a repository made is granted read at once, a known const { GiteaAdmin } = await import("../dist/client.js"); const { ReadAll, readAllTools } = await import("../dist/readall.js"); const lines: string[] = []; - const readAll = new ReadAll(GiteaAdmin.fromEnv(env), TOOLS_ACCOUNT, (l: string) => lines.push(l)); + forge.users.set(BUILDER_ACCOUNT, { admin: false, password: "unused", active: true }); + const readAll = new ReadAll(GiteaAdmin.fromEnv(env), [TOOLS_ACCOUNT, BUILDER_ACCOUNT], (l: string) => lines.push(l)); await readAll.atStart(); const cases: { name: string; event: string; repo: string; make?: boolean; refuse?: boolean; unanswered?: boolean; asks: boolean; reads: string }[] = [ @@ -528,17 +556,20 @@ test("on the forge's events: a repository made is granted read at once, a known const [owner, repo] = c.repo.split("/"); await readAll.onEvent({ name: c.event, body: { owner, repo } }); assert.equal(forge.calls.length > from, c.asks, `${c.name}: asks the forge`); - assert.equal(forgeRepos.find((x) => x.full === c.repo)!.collaborators.get(TOOLS_ACCOUNT) ?? (c.make ? "none" : "read"), c.reads, c.name); + for (const account of [TOOLS_ACCOUNT, BUILDER_ACCOUNT]) { + assert.equal(forgeRepos.find((x) => x.full === c.repo)!.collaborators.get(account) ?? (c.make ? "none" : "read"), c.reads, `${c.name}: ${account}`); + } } - assert.match(lines.join("\n"), /could not be given read on jschoubben\/stuck — .*422/); + assert.match(lines.join("\n"), /mesh-tools could not be given read on jschoubben\/stuck — .*422/); + assert.match(lines.join("\n"), /nox-builder was given read on someone\/fork, on the forge's forge.fork/); const named = await (readAllTools(() => readAll)[1].run({ repositories: ["jschoubben/stuck"] })) as any; - assert.deepEqual(named.refused.map((x: any) => x.repository), ["jschoubben/stuck"]); + assert.deepEqual(named.refused.map((x: any) => `${x.account} ${x.repository}`), [`${TOOLS_ACCOUNT} jschoubben/stuck`, `${BUILDER_ACCOUNT} jschoubben/stuck`]); await assert.rejects(readAllTools(() => readAll)[1].run({ repositories: ["no-slash"] }), /not a repository/); assert.match(lines.join("\n"), /could not be given read on jschoubben\/mute — its permission could not be read — .*500/); // A whole pass goes past the repository it cannot judge: every other one is still granted, and the listing names it. addRepo("jschoubben/after-mute", true); const pass = await readAll.grant(); - assert.ok(pass.granted.includes("jschoubben/after-mute"), "the pass did not stop at the repository it could not judge"); + assert.ok(pass.granted.some((x: any) => x.repository === "jschoubben/after-mute" && x.account === BUILDER_ACCOUNT), "the pass did not stop at the repository it could not judge"); assert.ok(pass.refused.some((x: any) => x.repository === "jschoubben/mute" && /permission could not be read/.test(x.why))); const listed = await readAllTools(() => readAll)[0].run({}) as any; assert.equal(listed.unreadable.find((u: any) => u.repository === "jschoubben/mute").permission, "unknown"); diff --git a/modules/gitea/tools-account.ts b/modules/gitea/tools-account.ts index 8fdd6956..6aff73f6 100644 --- a/modules/gitea/tools-account.ts +++ b/modules/gitea/tools-account.ts @@ -22,6 +22,15 @@ import { keepMainRules } from "./mainrules.js"; export const TOOLS_ACCOUNT = "mesh-tools"; +/** The build modules' account, whose keys fetch over ssh (internal/forgeaccount BuilderUser, ADR 0466 §3). */ +export const BUILDER_ACCOUNT = "nox-builder"; +/** The machines' agents' account, whose keys ssh-client reports (internal/forgeaccount AgentUser, ADR 0410 §1). */ +export const AGENT_ACCOUNT = "nox-agent"; +/** Every forge account the mesh's own parts use: each reads every repository on the forge (readall.ts; the operator, + * 2026-10-26). internal/forgeaccount's test holds the two Go names to its own constants. */ +export function meshAccounts(tools: string = TOOLS_ACCOUNT): string[] { + return [tools, BUILDER_ACCOUNT, AGENT_ACCOUNT]; +} /** The organisation whose repositories are the mesh's, and the team through which the tools' account writes there. */ export const TOOLS_ORG = "novox"; export const TOOLS_TEAM = "mesh-tools"; diff --git a/modules/mesh-build-go/cmd/mesh-build-go/candidate.go b/modules/mesh-build-go/cmd/mesh-build-go/candidate.go index 38a27ad6..4379d3ca 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/candidate.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/candidate.go @@ -13,6 +13,8 @@ import ( "path/filepath" "strings" "sync" + + "git.novox.be/novox/mesh-sdk/go/forgekey" ) // The runners of a digest or test step: this installed program, or the candidate's compiled from the workspace. @@ -108,7 +110,7 @@ func (m *Module) mergeCommit(ctx context.Context, workspace string) (string, err r := m.Run(ctx, nil, "git", "-C", filepath.Join(m.Work, workspace, "tree"), "rev-parse", "HEAD") commit := strings.TrimSpace(r.Stdout) if r.Status != 0 || r.Err != "" || commit == "" { - return "", fmt.Errorf("the workspace's commit: git rev-parse: %s", firstLine(r.Err+"\n"+r.Stderr)) + return "", fmt.Errorf("the workspace's commit: git rev-parse: %s", forgekey.Said(r.Err+"\n"+r.Stderr)) } return commit, nil } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go index 710d5cee..2b166c9e 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go @@ -18,6 +18,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/buildstep" + "git.novox.be/novox/mesh-sdk/go/forgekey" ) var commitOK = regexp.MustCompile(`^[0-9a-f]{40}$`) @@ -255,7 +256,7 @@ func (m *Module) downloadAndVerify(ctx context.Context, dir, d string) error { func (m *Module) goModDirs(ctx context.Context, tree string) ([]string, error) { r := m.Run(ctx, nil, "git", "-C", tree, "ls-files", "--", "go.mod", "*/go.mod") if r.Status != 0 || r.Err != "" { - return nil, fmt.Errorf("git ls-files: %s", firstLine(r.Err+" "+r.Stderr)) + return nil, fmt.Errorf("git ls-files: %s", forgekey.Said(r.Err+"\n"+r.Stderr)) } dirs := []string{} for _, l := range strings.Split(r.Stdout, "\n") { diff --git a/modules/mesh-build-go/cmd/mesh-build-go/mirror.go b/modules/mesh-build-go/cmd/mesh-build-go/mirror.go index 0ad17fda..7cd7dfad 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/mirror.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/mirror.go @@ -9,6 +9,7 @@ import ( "syscall" "git.novox.be/novox/mesh-sdk/go/buildstep" + "git.novox.be/novox/mesh-sdk/go/forgekey" ) // mirrorLocks hold one mutex per mirror of this process; the lock file beside the mirror holds off another process. @@ -67,7 +68,7 @@ func (m *Module) dissociate(ctx context.Context, dir string) error { return nil // no mirror was used } if r := m.Run(ctx, []string{"GIT_TERMINAL_PROMPT=0"}, "git", "-C", dir, "repack", "-a", "-d", "-q"); r.Status != 0 || r.Err != "" { - return fmt.Errorf("git repack in %s: %s", dir, firstLine(r.Err+" "+r.Stderr)) + return fmt.Errorf("git repack in %s: %s", dir, forgekey.Said(r.Err+"\n"+r.Stderr)) } return os.Remove(alternates) } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/mirror_test.go b/modules/mesh-build-go/cmd/mesh-build-go/mirror_test.go index 6721155e..32b8d595 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/mirror_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/mirror_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "fmt" "git.novox.be/novox/mesh-sdk/go/buildstep" "os" "os/exec" @@ -121,20 +122,35 @@ func TestTwoUpdatesOfOneMirrorNeverRunAtOnce(t *testing.T) { } } -// A mirror that cannot be updated does not fail the fetch: the fetch goes on without it. +// A mirror that cannot be updated does not fail the fetch: the fetch goes on without it, and the line saying so carries +// git's own words. The forge's refusal of a repository its account cannot read came out as "git -C: error:" (ace, +// 2026-10-26: git writes a bare "error:" line before the forge's message). func TestAMirrorThatCannotBeUpdatedLetsTheFetchGoOnWithoutIt(t *testing.T) { f := &fakeStep{gomods: "go.mod\n"} f.rule = func(line string) (r runAnswer, ok bool) { if strings.Contains(line, "mirror") && strings.Contains(line, " fetch ") { - return runAnswer{Status: 1, Stderr: "fatal: no space left\n"}, true + return runAnswer{Status: 128, Stderr: "error: \nerror: User: 22:nox-builder with Key: 30:mesh:ace.mesh-build-go is not authorized to read jschoubben/car-hunter.\nerror: \nfatal: Could not read from remote repository.\n"}, true } return r, false } m, states := fetchModule(t, f) + var mu sync.Mutex + var said []string + m.Say = func(format string, a ...any) { + mu.Lock() + defer mu.Unlock() + said = append(said, fmt.Sprintf(format, a...)) + } ws := fetchOnce(t, m, states, fetchArgs).Workspace if _, err := os.Stat(filepath.Join(m.Work, ws, "tree", ".git", "objects", "info", "alternates")); err == nil { t.Errorf("the workspace borrows from a mirror that is not there") } + mu.Lock() + defer mu.Unlock() + all := strings.Join(said, "\n") + if !strings.Contains(all, "is not used: git fetch: error: User: 22:nox-builder with Key: 30:mesh:ace.mesh-build-go is not authorized to read jschoubben/car-hunter.; fatal: Could not read from remote repository.") { + t.Errorf("the mirror's line lost git's words:\n%s", all) + } } func sleepABit() { time.Sleep(30 * time.Millisecond) } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/pins.go b/modules/mesh-build-go/cmd/mesh-build-go/pins.go index 4247d447..52532d6f 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/pins.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/pins.go @@ -15,6 +15,8 @@ import ( "path/filepath" "regexp" "strings" + + "git.novox.be/novox/mesh-sdk/go/forgekey" ) const forgePrefix = "git.novox.be/novox/" @@ -125,7 +127,7 @@ func (m *Module) placePin(ctx context.Context, root string, p pin, failure strin repository := "novox/" + p.repo r := m.Run(ctx, []string{"GIT_TERMINAL_PROMPT=0"}, "git", "-c", "credential.helper=", "ls-remote", m.remote(repository), "refs/pull/*/head") if r.Status != 0 || r.Err != "" { - return "", fmt.Errorf("the pin of %s at commit %s: listing the pull requests of %s: %s", p.module, p.short, repository, firstLine(r.Err+" "+r.Stderr)) + return "", fmt.Errorf("the pin of %s at commit %s: listing the pull requests of %s: %s", p.module, p.short, repository, forgekey.Said(r.Err+"\n"+r.Stderr)) } commit, ref := "", "" for _, l := range strings.Split(r.Stdout, "\n") { diff --git a/modules/mesh-build-go/go.mod b/modules/mesh-build-go/go.mod index 1a012772..868b22d2 100644 --- a/modules/mesh-build-go/go.mod +++ b/modules/mesh-build-go/go.mod @@ -3,7 +3,7 @@ module meshbuildgo go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 diff --git a/modules/mesh-build-go/go.sum b/modules/mesh-build-go/go.sum index a005d55c..c86aa01f 100644 --- a/modules/mesh-build-go/go.sum +++ b/modules/mesh-build-go/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d h1:7F/dFerm01wvuGLPgJ67/VfdcvDexc+BQEE3qzOrlu4= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f h1:GYLCvz2x+JNUOTbXzyh2QwzX0sTzvRlXR7lGocWjk3A= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= diff --git a/modules/mesh-build-image/go.mod b/modules/mesh-build-image/go.mod index 9adfce92..52658707 100644 --- a/modules/mesh-build-image/go.mod +++ b/modules/mesh-build-image/go.mod @@ -3,7 +3,7 @@ module meshbuildimage go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-build-image/go.sum b/modules/mesh-build-image/go.sum index 0728d2e9..ba65afa3 100644 --- a/modules/mesh-build-image/go.sum +++ b/modules/mesh-build-image/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d h1:7F/dFerm01wvuGLPgJ67/VfdcvDexc+BQEE3qzOrlu4= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f h1:GYLCvz2x+JNUOTbXzyh2QwzX0sTzvRlXR7lGocWjk3A= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-build-node/cmd/mesh-build-node/fetch.go b/modules/mesh-build-node/cmd/mesh-build-node/fetch.go index 35b1eeab..5f712e16 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/fetch.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/fetch.go @@ -212,7 +212,7 @@ func (m *Module) fetchInto(ctx context.Context, req buildstep.FetchRequest, grou func (m *Module) typeScriptModules(ctx context.Context, tree string) (dirs []string, packages map[string]bool, err error) { r := m.Run(ctx, m.baseEnv(), "git", "-C", tree, "ls-files", "--", "module.json", "*/module.json") if r.Status != 0 || r.Err != "" { - return nil, nil, fmt.Errorf("git ls-files: %s", firstLine(r.Err+" "+r.Stderr)) + return nil, nil, fmt.Errorf("git ls-files: %s", forgekey.Said(r.Err+"\n"+r.Stderr)) } packages = map[string]bool{} for _, l := range strings.Split(r.Stdout, "\n") { diff --git a/modules/mesh-build-node/go.mod b/modules/mesh-build-node/go.mod index 7976769a..e79f663b 100644 --- a/modules/mesh-build-node/go.mod +++ b/modules/mesh-build-node/go.mod @@ -3,7 +3,7 @@ module meshbuildnode go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f github.com/distribution/distribution/v3 v3.1.2 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-build-node/go.sum b/modules/mesh-build-node/go.sum index af033231..28122ac9 100644 --- a/modules/mesh-build-node/go.sum +++ b/modules/mesh-build-node/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d h1:7F/dFerm01wvuGLPgJ67/VfdcvDexc+BQEE3qzOrlu4= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026134601-17b7cc480d6d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f h1:GYLCvz2x+JNUOTbXzyh2QwzX0sTzvRlXR7lGocWjk3A= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026142629-ff81bdf8cc8f/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=