From 2038943c3e5f36836782ab35f0f1d624b4264a3f Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 15:11:37 +0200 Subject: [PATCH] Write no merge-check.sh in a new repository's first commit and announce none: tests run by convention (issue 1296, E1b) --- modules/gitea/README.md | 14 ++-- modules/gitea/cmd/repositories/first.go | 33 ++-------- .../cmd/repositories/repositories_test.go | 64 +++---------------- modules/gitea/index.ts | 8 +-- modules/gitea/merge.ts | 9 +-- modules/gitea/module.json | 2 +- modules/gitea/protection.ts | 2 +- modules/gitea/trunk.ts | 10 +-- 8 files changed, 32 insertions(+), 110 deletions(-) diff --git a/modules/gitea/README.md b/modules/gitea/README.md index e735d7bb..6e204169 100644 --- a/modules/gitea/README.md +++ b/modules/gitea/README.md @@ -22,8 +22,7 @@ Every new head of an open pull request is announced as `pull.updated`, once, and minutes, or when the search fails. - **A pull request nobody checks is said — by the delivery's owner, not here.** `gitea_open_pulls` answers every open pull request from the forge itself, with the statuses its base requires and the ones its head has. The - delivery gate asks it every five minutes, and a head on a base requiring `mesh/check` (or hq's - `mesh/repo-check`) with none of them for ten minutes since it first saw it so is a delivery held past its bound + delivery gate asks it every five minutes, and a head on a base requiring `mesh/check` with none of them for ten minutes since it first saw it so is a delivery held past its bound (`delivery..stalled`, state `unannounced`), which the controller raises like any other (issue 347). Each of the head's statuses comes with its state and when it was set, so a head whose check started and never answered — a required check pending, or never set beside one that is — is said as a stalled line in the state @@ -153,19 +152,18 @@ listed repository: - one that is not in the organisation and not under the setup account is made there, **empty and under the setup account**, where neither the tools' team nor the agents' team (both on every repository of the organisation) reaches it. Main gets a rule letting the setup login alone push; main's first commit is written with it: a README - naming the description, and a `merge-check.sh` declaring `# mesh-check-toolchain: go` that vets and runs the unit - tests, then the integration tests (`-tags integration`), of every Go module in the tree, and says so and passes on - a tree with none. The rule is then narrowed to main's rule of ADR 0398 §2 (no push, only `mesh-tools` merges), + naming the description and nothing else (the checker runs a repository's tests by their language's convention, + ADR 0445, ADR 0529 §5). The rule is then narrowed to main's rule of ADR 0398 §2 (no push, only `mesh-tools` merges), requiring `mesh/check` (ADR 0447 §1, issue 1301), and only then is the repository moved into the organisation. There main's rule is read back and put right once more (the step `rule-after-move`): the forge drops from a merge list a user without access to the repository, and under the setup account `mesh-tools` has none; - one found still under the setup account is finished: the steps it lacks are done, main is never written twice; - one in the organisation is only put right: main's rule narrowed, `mesh/check` required in place of `mesh/merge-gate` and `mesh/repo-check`, and every other status already there kept. A main that requires - `mesh/repo-check` without `mesh/merge-gate` is judged by its own script alone (hq, ADR 0503 §5, until issue 1296) - and keeps it; `mesh/check` is never dropped. + `mesh/repo-check` alone (hq's did, until issue 1296) is given `mesh/check` in its place too, since nothing writes + `mesh/repo-check` once build-agent is gone (ADR 0527 §2); `mesh/check` is never dropped. -Every other repository of the organisation whose main requires `mesh/merge-gate` has the two old statuses swapped +Every other repository of the organisation whose main requires `mesh/merge-gate` or `mesh/repo-check` has the old statuses swapped for `mesh/check`, and only its statuses (issue 1301): the switch reaches every repository the merge gate judged, with no list kept by hand. A step the forge refuses stops that repository, and the others go on. Beyond that swap, a repository taken out of the value is never changed: nothing is deleted, renamed, moved out or unprotected. The pass keeps what it did per repository in diff --git a/modules/gitea/cmd/repositories/first.go b/modules/gitea/cmd/repositories/first.go index 1ae7365a..61abc30e 100644 --- a/modules/gitea/cmd/repositories/first.go +++ b/modules/gitea/cmd/repositories/first.go @@ -2,34 +2,9 @@ package main import "strings" -// MergeCheck is the first commit's merge-check.sh (ADR 0428 §4): it declares the Go toolchain (ADR 0238 §4, read -// among a script's first lines by the build seat), and runs the unit tests, then with -tags integration the -// integration tests, of every Go module in the tree; on a tree with none it says so and passes. -const MergeCheck = `#!/bin/sh -# mesh-check-toolchain: go -# -# This repository's own check (novox/hq ADR 0238 §4), run by the checker within mesh/check (ADR 0447), written with its first commit (ADR 0428 §4): -# every Go module in the tree is vetted, its unit tests run, and then its integration tests (-tags integration). -# A tree with no Go module has nothing to test, and the check says so and passes. -set -eu -cd "$(dirname "$0")" - -modules=$(find . -name go.mod -not -path '*/vendor/*' -not -path './.git/*' | sort) -if [ -z "$modules" ]; then - echo "This tree holds no Go module, so there are no tests to run; the check passes." - exit 0 -fi -for mod in $modules; do - dir=$(dirname "$mod") - echo "== $dir: go vet and the unit tests" - (cd "$dir" && go vet ./... && go test ./...) - echo "== $dir: the integration tests (-tags integration)" - (cd "$dir" && go vet -tags integration ./... && go test -tags integration ./...) -done -` - // FirstCommit is main's first commit of a listed repository: its README, naming the repository's purpose, and -// its own check. +// nothing else: the checker runs a repository's tests by their language's convention (ADR 0445), so no script is written +// (ADR 0529 §5). func FirstCommit(name string, r Repo) map[string]string { purpose := strings.TrimSpace(r.Description) if purpose == "" { @@ -38,6 +13,6 @@ func FirstCommit(name string, r Repo) map[string]string { readme := "# " + name + "\n\n" + purpose + "\n\n" + "Made by the mesh from the gitea module's config value `repositories` (novox/hq ADR 0428). Main takes no push: " + "a change reaches it by a pull request whose `mesh/check` passed: the checker's verdict on its head merged onto " + - "main, this repository's `merge-check.sh` among its tests (novox/hq ADR 0447).\n" - return map[string]string{"README.md": readme, "merge-check.sh": MergeCheck} + "main, its tests run by their language's convention (novox/hq ADR 0447, ADR 0445).\n" + return map[string]string{"README.md": readme} } diff --git a/modules/gitea/cmd/repositories/repositories_test.go b/modules/gitea/cmd/repositories/repositories_test.go index 19bb868d..23b5b451 100644 --- a/modules/gitea/cmd/repositories/repositories_test.go +++ b/modules/gitea/cmd/repositories/repositories_test.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "os" - "os/exec" "path/filepath" "strings" "testing" @@ -53,7 +52,7 @@ func TestNewRepositoryIsMadeInOrder(t *testing.T) { if repo.Private || repo.Description != "the mesh's delivery" { t.Errorf("made as %+v, want public with its description", repo) } - if repo.Commits != 1 || len(repo.Files) != 2 || repo.Files["README.md"] == "" || repo.Files["merge-check.sh"] == "" { + if repo.Commits != 1 || len(repo.Files) != 1 || repo.Files["README.md"] == "" { t.Errorf("first commit: %d commits, files %v", repo.Commits, keysOf(repo.Files)) } if !strings.Contains(repo.Files["README.md"], "the mesh's delivery") { @@ -409,66 +408,19 @@ func TestReadValue(t *testing.T) { } } -// 1124/AC4: the first commit's merge-check.sh declares its toolchain in its first lines, runs the tests of a Go -// module in the tree, and on a tree with none says so and passes. -func TestFirstCommitMergeCheck(t *testing.T) { +// 1296/E1b (ADR 0527 §2, ADR 0529 §5): the first commit holds a README naming the purpose and no merge-check.sh: the +// checker runs a repository's tests by their language's convention and no script is written for it. +func TestFirstCommitHasNoMergeCheck(t *testing.T) { files := FirstCommit("mesh-delivery", Repo{Description: "the mesh's delivery"}) - script := files["merge-check.sh"] - lines := strings.SplitN(script, "\n", 4) - if lines[0] != "#!/bin/sh" || lines[1] != "# mesh-check-toolchain: go" { - t.Fatalf("first lines %q", lines[:2]) + if _, has := files["merge-check.sh"]; has || len(files) != 1 { + t.Fatalf("first commit files %v, want the README alone", keysOf(files)) } if !strings.Contains(files["README.md"], "mesh-delivery") || !strings.Contains(files["README.md"], "the mesh's delivery") { t.Errorf("README %q", files["README.md"]) } - goBin, err := exec.LookPath("go") - if err != nil { - t.Skip("no go on PATH to run the script with") + if strings.Contains(files["README.md"], "merge-check") { + t.Errorf("the README names a merge-check.sh: %q", files["README.md"]) } - - tree := t.TempDir() - mod := filepath.Join(tree, "sub") - _ = os.MkdirAll(mod, 0o755) - _ = os.WriteFile(filepath.Join(tree, "merge-check.sh"), []byte(script), 0o755) - _ = os.WriteFile(filepath.Join(mod, "go.mod"), []byte("module tiny\n\ngo 1.21\n"), 0o644) - _ = os.WriteFile(filepath.Join(mod, "tiny_test.go"), []byte(`package tiny - -import ( - "os" - "testing" -) - -func TestRan(t *testing.T) { _ = os.WriteFile("ran", []byte("yes"), 0o644) } -`), 0o644) - cmd := exec.Command("sh", "merge-check.sh") - cmd.Dir = tree - cmd.Env = append(os.Environ(), "GOFLAGS=", "GOTOOLCHAIN=local", "PATH="+filepath.Dir(goBin)+":"+os.Getenv("PATH")) - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("on a tree with a Go module: %v\n%s", err, out) - } - if _, err := os.Stat(filepath.Join(mod, "ran")); err != nil || !strings.Contains(string(out), "integration") { - t.Fatalf("its tests did not run, or not with -tags integration:\n%s", out) - } - - // A failing test fails the check. - _ = os.WriteFile(filepath.Join(mod, "fail_test.go"), []byte("package tiny\n\nimport \"testing\"\n\nfunc TestFails(t *testing.T) { t.Fatal(\"no\") }\n"), 0o644) - if out, err := cmd2(tree, cmd.Env).CombinedOutput(); err == nil { - t.Fatalf("a failing test passed the check:\n%s", out) - } - - empty := t.TempDir() - _ = os.WriteFile(filepath.Join(empty, "merge-check.sh"), []byte(script), 0o755) - out, err = cmd2(empty, cmd.Env).CombinedOutput() - if err != nil || !strings.Contains(string(out), "no Go module") { - t.Fatalf("on an empty tree: %v\n%s", err, out) - } -} - -func cmd2(dir string, env []string) *exec.Cmd { - c := exec.Command("sh", "merge-check.sh") - c.Dir, c.Env = dir, env - return c } func keysOf(m map[string]string) []string { diff --git a/modules/gitea/index.ts b/modules/gitea/index.ts index fe80595e..27b6e6a1 100644 --- a/modules/gitea/index.ts +++ b/modules/gitea/index.ts @@ -13,8 +13,8 @@ // commit's statuses: `mesh/merge-gate`, the modules of the mesh's graph the change touches, and // `mesh/repo-check`, the repository's own merge-check.sh (novox/hq ADR 0237 as amended); with a comment // saying why when the gate is not a pass or the repository's own check failed. Of the two, only those the base -// still requires are written (issue 1301): main requires `mesh/check` — the checker's — everywhere but hq, whose main -// requires `mesh/repo-check` alone (ADR 0503 §5). +// still requires are written (issue 1301): main requires `mesh/check` — the checker's — everywhere, hq included since +// ADR 0527 §2, so these are written only for a revert. // // Reads mesh-delivery-checker.owed — what the checker owes the forge, mesh/check on each head it judged — by a watch, and // writes it (check.ts; ADR 0521, R4e finding R2-1). No tool writes mesh/check (finding K1), and no event: the watch hands @@ -257,8 +257,8 @@ async function announceHead(client: GiteaClient, repo: RepoOf, pull: OpenOf): Pr console.log(`[gitea] announced ${key} at ${sha.slice(0, 8)} to be checked before it merges`); // Pending until the verdict comes, so the pull request says a check is running rather than nothing — and // where it is: in the build queue from now, until a build machine takes it (novox/hq issue 446). Only where the base - // still requires mesh/merge-gate (issue 1301): a main that requires mesh/check is the checker's, and hq's requires - // mesh/repo-check alone; a rule that cannot be read is said pending, as before. + // still requires mesh/merge-gate (issue 1301): a main that requires mesh/check is the checker's; + // a rule that cannot be read is said pending, as before. const rule = await client.appliedProtection(repo.owner, repo.name, String(pull.base || "main")).catch(() => undefined); if (writesGate(requiredOf(rule))) await gates.pending(repo.owner, repo.name, sha); return true; diff --git a/modules/gitea/merge.ts b/modules/gitea/merge.ts index 5ba292f7..832ae958 100644 --- a/modules/gitea/merge.ts +++ b/modules/gitea/merge.ts @@ -7,7 +7,7 @@ // // **It judges on the status the base requires** (issue 1301; ADR 0447 §1, §4; ADR 0503 §5): mesh/check, the checker's, // whose line names the trunk it judged ("pass on "); where a base does not require it, the controller's -// status it still requires (hq's mesh/repo-check), as before. A mesh/check of another trunk is refused and nothing is +// status it still requires (only on a revert), as before. A mesh/check of another trunk is refused and nothing is // asked: the checker sets it out of date on the trunk's move and checks it again. import { announce } from "./announce.js"; @@ -37,7 +37,7 @@ export function mergeTool(gitea: GiteaClient, given: MergeDeps): ToolDefinition return { name: "gitea_merge_pull_request", description: "Merge a pull request at the head you name — the commit you reviewed — when that is still its head " + - "and every status its base branch's protection requires succeeded there (mesh/check; hq's mesh/repo-check), " + + "and every status its base branch's protection requires succeeded there (mesh/check), " + "judged against the trunk's head as it is now, optionally deleting the source branch afterwards. Refused " + "otherwise, with nothing merged: no tool merges past a check, and none merges a head that moved after it was " + "reviewed. A verdict judged against an earlier trunk commit is refused, naming both, and the head's check against " + @@ -131,7 +131,7 @@ export function mergeTool(gitea: GiteaClient, given: MergeDeps): ToolDefinition `ADR 0447); this tool asks nothing. Ask again at ${twelve(pull.head_sha)} once it passes. Nothing was merged`); } } - // Where the base still requires the controller's statuses (hq's mesh/repo-check; mesh/merge-gate only on a + // Where the base still requires the controller's status (mesh/merge-gate, only on a // revert), the first it requires judges, as before (ADR 0323 §5). const kept = maintained(required); const judge = kept[0] ?? CHECK_CONTEXT; @@ -161,7 +161,8 @@ export function mergeTool(gitea: GiteaClient, given: MergeDeps): ToolDefinition // Pending first, so a second ask finds the check on its way; then the head is announced as a new head is. An // announcement that cannot be made puts the statuses back to out of date, so the next ask asks again rather // than wait for ever on a check nobody asked. - // The statuses the base requires of the controller's (issue 1301): hq's mesh/repo-check alone. + // The statuses the base requires of the controller's (issue 1301): the one that judges, and the repository's own + // check beside it where a revert has the base require both. const contexts = kept.filter((c) => c === judge || (c === REPO_CHECK_CONTEXT && checks[REPO_CHECK_CONTEXT])); // Through the module's one writer of the merge check (gate.ts, novox/hq issue 456): the pending opens a new // check of the head, so where it runs is said ("building on …") and its verdict is not weighed as older. diff --git a/modules/gitea/module.json b/modules/gitea/module.json index 279529fb..bf2211a4 100644 --- a/modules/gitea/module.json +++ b/modules/gitea/module.json @@ -172,7 +172,7 @@ }, "repositories": { "type": "object", - "description": "The repositories of the organisation novox the mesh holds (ADR 0428): each by name, {\"\": {\"private\": true, \"description\": \"...\"}}. One that does not exist is made by the module's setup login under its own account, its main protected and given a first commit (a README naming the description and a merge-check.sh), its rule narrowed to no push and the tools' merge requiring mesh/check, and only then moved into the organisation; one that exists only has main's rule put right, mesh/check in place of mesh/merge-gate and mesh/repo-check and its other statuses kept (a main requiring mesh/repo-check alone keeps it). Every other repository whose main requires mesh/merge-gate has the two swapped for mesh/check; nothing else of a repository out of the value is touched.", + "description": "The repositories of the organisation novox the mesh holds (ADR 0428): each by name, {\"\": {\"private\": true, \"description\": \"...\"}}. One that does not exist is made by the module's setup login under its own account, its main protected and given a first commit (a README naming the description), its rule narrowed to no push and the tools' merge requiring mesh/check, and only then moved into the organisation; one that exists only has main's rule put right, mesh/check in place of mesh/merge-gate and mesh/repo-check and its other statuses kept (a main requiring mesh/repo-check alone is swapped too, ADR 0527 §2). Every other repository whose main requires mesh/merge-gate or mesh/repo-check has the old statuses swapped for mesh/check; nothing else of a repository out of the value is touched.", "trusted": true, "default": {} } diff --git a/modules/gitea/protection.ts b/modules/gitea/protection.ts index 5336a827..41525c37 100644 --- a/modules/gitea/protection.ts +++ b/modules/gitea/protection.ts @@ -1,5 +1,5 @@ // A branch's protection (novox/hq ADR 0237): what makes a pull request wait for the mesh's check — `mesh/check`, -// the checker's verdict on the head merged onto its trunk (ADR 0447 §1; hq's `mesh/repo-check` until issue 1296) — +// the checker's verdict on the head merged onto its trunk (ADR 0447 §1, hq's too since ADR 0527 §2) — // before it may merge. Pure, so it is tested without a forge; the client does the asking (client.ts). import { TOOLS_ACCOUNT } from "./tools-account.js"; diff --git a/modules/gitea/trunk.ts b/modules/gitea/trunk.ts index 11100ca1..55866f30 100644 --- a/modules/gitea/trunk.ts +++ b/modules/gitea/trunk.ts @@ -14,8 +14,8 @@ // // Since issue 1301 (ADR 0447 §4) a main that requires mesh/check is the checker's: its line names the trunk commit it judged // ("pass on "), the checker sets it out of date when the trunk moves, and the merge tool refuses a verdict -// of another trunk and asks nothing. The two statuses above are kept only where a base still requires them (hq's -// mesh/repo-check, pulls.ts maintained). +// of another trunk and asks nothing. The two statuses above are kept only where a base still requires them (a revert, +// pulls.ts maintained). // // Pure functions and two small files of the module's own state, so this is tested without a forge. @@ -234,14 +234,11 @@ export async function announcementOf(client: GiteaClient, owner: string, name: s const files = await client.listPullFiles(owner, name, pull.number); const head = String(pull.head_sha); // What the controller maps the change onto the mesh's module graph with (novox/hq ADR 0237 as amended): which - // changed directories hold a module at the head — the merge's rule (issue 278) — and whether the head holds the - // repository's own merge-check.sh. Not said when it could not be read; the controller then reads the change as + // changed directories hold a module at the head — the merge's rule (issue 278). Not said when it could not be read; the controller then reads the change as // touching everything built from the repository. let moduleDirs: string[] | null = null; - let mergeCheck: boolean | null = null; try { if (!files.truncated) moduleDirs = await client.moduleDirsAt(owner, name, head, files.paths); - mergeCheck = await client.holdsFile(owner, name, head, "merge-check.sh"); } catch (err) { console.error(`[gitea] ${owner}/${name}#${pull.number}: what the head holds could not be read — ${err instanceof Error ? err.message : err}`); } @@ -264,6 +261,5 @@ export async function announcementOf(client: GiteaClient, owner: string, name: s paths_truncated: files.truncated, removed: files.removed, ...(moduleDirs ? { module_dirs: moduleDirs, module_dirs_said: true } : {}), - ...(mergeCheck !== null ? { merge_check: mergeCheck, merge_check_said: true } : {}), }; }