vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Drop the legacy run-record import
59badc638dde0655b11d01eebfe3a457735d2501 · Keenan Nemetz · 11d ago · parent b312e166
Drop the legacy run-record import There is no released instance to stay compatible with, so the file format under runs/, the branch importer, and the pre-receive allowance for that directory are gone. The Record value is now only the finish wire format and the shape stored in run_attempts. metadata.ValidateIssues rejects runs/ like any other unknown directory, and vole admin index rebuild no longer reads run history from Git. An existing instance migrates once with the previous build, then rewrites the vole branch to remove the old records. Nothing in this build reads them.
server/hook/records_test.go −193
| @@ -1,193 +0,0 @@ | |||
| 1 | package hook | ||
| 2 | |||
| 3 | // Who may report a run result, from the gate's side: the record is the runner's, so | ||
| 4 | // the questions are whether the account may report at all, whether the commit is | ||
| 5 | // signed by that account's key, and whether the record describes the job it was handed. | ||
| 6 | |||
| 7 | import ( | ||
| 8 | "os" | ||
| 9 | "os/exec" | ||
| 10 | "path/filepath" | ||
| 11 | "strings" | ||
| 12 | "testing" | ||
| 13 | "time" | ||
| 14 | |||
| 15 | "github.com/rodent-software/vole/ci/runs" | ||
| 16 | "github.com/rodent-software/vole/core/git" | ||
| 17 | "github.com/rodent-software/vole/core/id" | ||
| 18 | "github.com/rodent-software/vole/forge/access" | ||
| 19 | "github.com/rodent-software/vole/forge/identity" | ||
| 20 | "github.com/rodent-software/vole/forge/repo" | ||
| 21 | "github.com/rodent-software/vole/metadata" | ||
| 22 | ) | ||
| 23 | |||
| 24 | // recordFixture is everything a legacy record push needs: a repository, a roster, an ACL naming | ||
| 25 | // one allowed runner, and a queue with a leased job to report on. | ||
| 26 | type recordFixture struct { | ||
| 27 | t *testing.T | ||
| 28 | dir string | ||
| 29 | repo *repo.Meta | ||
| 30 | usersDB string | ||
| 31 | runsDB string | ||
| 32 | runnerID string | ||
| 33 | otherID string | ||
| 34 | run *runs.Run | ||
| 35 | current string // the metadata branch's tip, as the fixture has pushed it | ||
| 36 | } | ||
| 37 | |||
| 38 | func newRecordFixture(t *testing.T) *recordFixture { | ||
| 39 | t.Helper() | ||
| 40 | dir := t.TempDir() | ||
| 41 | usersDB := filepath.Join(dir, "users.db") | ||
| 42 | if err := identity.Init(usersDB); err != nil { | ||
| 43 | t.Fatal(err) | ||
| 44 | } | ||
| 45 | ident, err := identity.Open(usersDB) | ||
| 46 | if err != nil { | ||
| 47 | t.Fatal(err) | ||
| 48 | } | ||
| 49 | ident.AuditPath = filepath.Join(dir, "audit.log") | ||
| 50 | defer ident.Close() | ||
| 51 | |||
| 52 | f := &recordFixture{t: t, dir: dir, usersDB: usersDB, | ||
| 53 | runsDB: filepath.Join(dir, "runs.db")} | ||
| 54 | for _, u := range []struct { | ||
| 55 | name string | ||
| 56 | into *string | ||
| 57 | }{ | ||
| 58 | {"runner", &f.runnerID}, | ||
| 59 | {"other", &f.otherID}, | ||
| 60 | } { | ||
| 61 | _, pub := writeKey(t, filepath.Join(dir, u.name)) | ||
| 62 | acct, err := ident.Create(u.name, pub, "test") | ||
| 63 | if err != nil { | ||
| 64 | t.Fatal(err) | ||
| 65 | } | ||
| 66 | *u.into = acct.Name | ||
| 67 | } | ||
| 68 | |||
| 69 | acls, err := access.Open(usersDB) | ||
| 70 | if err != nil { | ||
| 71 | t.Fatal(err) | ||
| 72 | } | ||
| 73 | acls.AuditPath = filepath.Join(dir, "audit.log") | ||
| 74 | t.Cleanup(func() { acls.Close() }) | ||
| 75 | st := repo.New(filepath.Join(dir, "repos"), "/bin/true", acls) | ||
| 76 | m, err := st.Create("demo", f.runnerID, "") | ||
| 77 | if err != nil { | ||
| 78 | t.Fatal(err) | ||
| 79 | } | ||
| 80 | a, err := m.ACL() | ||
| 81 | if err != nil { | ||
| 82 | t.Fatal(err) | ||
| 83 | } | ||
| 84 | if err := a.SetEntry(f.otherID, "maintain"); err != nil { | ||
| 85 | t.Fatal(err) | ||
| 86 | } | ||
| 87 | a.SetRunners([]string{f.runnerID}) | ||
| 88 | if err := m.SetACL(a, "test"); err != nil { | ||
| 89 | t.Fatal(err) | ||
| 90 | } | ||
| 91 | f.repo = m | ||
| 92 | |||
| 93 | queue, err := runs.Open(f.runsDB) | ||
| 94 | if err != nil { | ||
| 95 | t.Fatal(err) | ||
| 96 | } | ||
| 97 | defer queue.Close() | ||
| 98 | runID, err := id.New(time.Now().UTC()) | ||
| 99 | if err != nil { | ||
| 100 | t.Fatal(err) | ||
| 101 | } | ||
| 102 | run := runs.Run{ | ||
| 103 | ID: runID, Repo: "demo", Ref: "refs/heads/main", Branch: "main", | ||
| 104 | SHA: strings.Repeat("ab", 20), Workflow: ".vole/workflows/ci.yaml", | ||
| 105 | WorkflowSHA: strings.Repeat("cd", 20), Job: "build", | ||
| 106 | Steps: []runs.Step{{Name: "test", Run: "true"}}, Created: time.Now().UTC(), | ||
| 107 | } | ||
| 108 | if _, err := queue.Enqueue(run, nil); err != nil { | ||
| 109 | t.Fatal(err) | ||
| 110 | } | ||
| 111 | // The runner holds it, which is the only state in which a result may be reported. | ||
| 112 | leased, err := queue.Lease(runs.LeaseRequest{Owner: f.runnerID, | ||
| 113 | Repos: []string{"demo"}, TTL: time.Minute}, time.Now().UTC()) | ||
| 114 | if err != nil || leased == nil { | ||
| 115 | t.Fatalf("leasing: %v %v", leased, err) | ||
| 116 | } | ||
| 117 | f.run = leased | ||
| 118 | return f | ||
| 119 | } | ||
| 120 | |||
| 121 | // writeKey makes an OpenSSH key pair and returns the private path and the public line. | ||
| 122 | func writeKey(t *testing.T, base string) (string, string) { | ||
| 123 | t.Helper() | ||
| 124 | if out, err := exec.Command("ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "test", | ||
| 125 | "-f", base).CombinedOutput(); err != nil { | ||
| 126 | t.Fatalf("ssh-keygen: %v: %s", err, out) | ||
| 127 | } | ||
| 128 | pub, err := os.ReadFile(base + ".pub") | ||
| 129 | if err != nil { | ||
| 130 | t.Fatal(err) | ||
| 131 | } | ||
| 132 | return base, strings.TrimSpace(string(pub)) | ||
| 133 | } | ||
| 134 | |||
| 135 | // record is a well-formed record for the fixture's leased job, with the attempt and | ||
| 136 | // signer the caller asks for. | ||
| 137 | func (f *recordFixture) record(attempt int, signer string) runs.Record { | ||
| 138 | f.t.Helper() | ||
| 139 | return runs.Record{ | ||
| 140 | RunID: f.run.ID, Attempt: attempt, Repo: "demo", Ref: f.run.Ref, SHA: f.run.SHA, | ||
| 141 | Workflow: f.run.Workflow, WorkflowSHA: f.run.WorkflowSHA, Job: f.run.Job, | ||
| 142 | StepsSHA: f.run.StepsSHA, Exit: 0, Finished: time.Now().UTC(), | ||
| 143 | Signer: signer, | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | // push writes a record on top of the branch and returns the new tip. | ||
| 148 | func (f *recordFixture) push(rec runs.Record) string { | ||
| 149 | f.t.Helper() | ||
| 150 | g := f.repo.Git() | ||
| 151 | commit, err := runs.WriteRecord(g, metadata.Ref, rec, "runner", nil) | ||
| 152 | if err != nil { | ||
| 153 | f.t.Fatalf("writing the record: %v", err) | ||
| 154 | } | ||
| 155 | if err := g.UpdateRef(metadata.Ref, commit); err != nil { | ||
| 156 | f.t.Fatal(err) | ||
| 157 | } | ||
| 158 | old := f.current | ||
| 159 | f.current = commit | ||
| 160 | return old | ||
| 161 | } | ||
| 162 | |||
| 163 | // preReceive runs the gate for a push of the metadata branch by one account. | ||
| 164 | func (f *recordFixture) preReceive(actor, old, new string) (int, string) { | ||
| 165 | f.t.Helper() | ||
| 166 | if old == "" { | ||
| 167 | old = git.ZeroSHA | ||
| 168 | } | ||
| 169 | var out strings.Builder | ||
| 170 | code := PreReceive(Config{ | ||
| 171 | RepoDir: f.repo.Dir, | ||
| 172 | Actor: actor, | ||
| 173 | UsersDB: f.usersDB, | ||
| 174 | RunsDB: f.runsDB, | ||
| 175 | In: strings.NewReader(old + " " + new + " " + metadata.Ref + "\n"), | ||
| 176 | Out: &out, | ||
| 177 | }) | ||
| 178 | return code, out.String() | ||
| 179 | } | ||
| 180 | |||
| 181 | func TestRunRecordPushIsRefused(t *testing.T) { | ||
| 182 | f := newRecordFixture(t) | ||
| 183 | rec := f.record(f.run.Attempts, f.runnerID) | ||
| 184 | f.push(rec) | ||
| 185 | |||
| 186 | code, out := f.preReceive(f.runnerID, "", f.current) | ||
| 187 | if code == 0 { | ||
| 188 | t.Fatal("a run-record push was accepted; records belong in the database now") | ||
| 189 | } | ||
| 190 | if !strings.Contains(out, "stored in the instance database") { | ||
| 191 | t.Fatalf("the refusal does not name the new home:\n%s", out) | ||
| 192 | } | ||
| 193 | } | ||