vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 55m ago
4b8c9e1d
| 1 | package server_test |
| 2 | |
| 3 | // A tag push is a release build: the workflow opts in with on.push.tags, the run |
| 4 | // carries the tag, and the run's release-flagged artifacts are committed as the |
| 5 | // tag's release artifacts — the same pointers `vole release attach` writes. |
| 6 | |
| 7 | import ( |
| 8 | "path/filepath" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | "time" |
| 12 | |
| 13 | "rodent.software/vole/ci/runs" |
| 14 | "rodent.software/vole/forge/config" |
| 15 | ) |
| 16 | |
| 17 | func tagWorkflow() map[string]string { |
| 18 | return workflowFile(`version: 1 |
| 19 | on: |
| 20 | push: |
| 21 | tags: ['v*'] |
| 22 | jobs: |
| 23 | release: |
| 24 | image: alpine:3.22 |
| 25 | env: |
| 26 | RELEASE_KIND: tool |
| 27 | steps: |
| 28 | - name: build |
| 29 | run: mkdir -p dist && printf 'release binary\n' > dist/tool-linux |
| 30 | artifacts: |
| 31 | - path: dist/* |
| 32 | release: true |
| 33 | `) |
| 34 | } |
| 35 | |
| 36 | func TestTagPushBuildsARelease(t *testing.T) { |
| 37 | t.Parallel() |
| 38 | f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 5 }) |
| 39 | f.publish(f.repo) |
| 40 | |
| 41 | work := workRepo(t, tagWorkflow()) |
| 42 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { |
| 43 | t.Fatalf("push: %s", errs) |
| 44 | } |
| 45 | // A workflow that says "tags:" runs on tags: a branch push must not queue it, or |
| 46 | // every commit would try to publish a release. |
| 47 | time.Sleep(1500 * time.Millisecond) // one spool tick |
| 48 | if list, _ := f.srv.Runs.Recent(f.repo, 10); len(list) != 0 { |
| 49 | t.Fatalf("a branch push queued %d run(s) for a tag-only workflow", len(list)) |
| 50 | } |
| 51 | |
| 52 | if _, errs, err := runGit(t, work, "tag", "-a", "v1.0.0", "-m", "first release\n"); err != nil { |
| 53 | t.Fatalf("tag: %s", errs) |
| 54 | } |
| 55 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "refs/tags/v1.0.0:refs/tags/v1.0.0"); err != nil { |
| 56 | t.Fatalf("push tag: %s", errs) |
| 57 | } |
| 58 | |
| 59 | queued := f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool { return r.Status == runs.StatusQueued }) |
| 60 | if queued.Tag != "v1.0.0" || !queued.TagAnnotated || queued.Branch != "" { |
| 61 | t.Fatalf("queued run: tag=%q annotated=%v branch=%q", queued.Tag, queued.TagAnnotated, queued.Branch) |
| 62 | } |
| 63 | if queued.SHA == "" { |
| 64 | t.Fatal("the run has no commit: a tag object is not something a runner can check out") |
| 65 | } |
| 66 | if len(queued.ReleaseArtifacts) != 1 || queued.ReleaseArtifacts[0] != "dist/*" { |
| 67 | t.Fatalf("release artifacts = %v", queued.ReleaseArtifacts) |
| 68 | } |
| 69 | // The job's environment is pinned with everything else and travels with the lease. |
| 70 | if queued.Env["RELEASE_KIND"] != "tool" { |
| 71 | t.Fatalf("queued env = %v", queued.Env) |
| 72 | } |
| 73 | |
| 74 | f.runOnce(t) |
| 75 | done := f.terminalRun(t) |
| 76 | |
| 77 | // The release page is the proof: the runner committed the pointers, and the web |
| 78 | // reads them exactly as it reads a person's `vole release attach`. |
| 79 | code, page := f.get("/" + f.repo + "/releases/v1.0.0") |
| 80 | if code != 200 { |
| 81 | t.Fatalf("release page = %d", code) |
| 82 | } |
| 83 | if !strings.Contains(page, "tool-linux") { |
| 84 | t.Fatalf("release page does not show the built artifact:\n%s", page) |
| 85 | } |
| 86 | code, body := f.get("/" + f.repo + "/releases/v1.0.0/artifacts/tool-linux") |
| 87 | if code != 200 || body != "release binary\n" { |
| 88 | t.Fatalf("artifact download = %d %q", code, body) |
| 89 | } |
| 90 | // The record is in the database. The branch carries the release pointer, and it |
| 91 | // no longer carries a run record: a release can still name the build that made it, |
| 92 | // but the CI history does not travel with a clone. |
| 93 | rec, err := f.srv.Runs.Attempt(done.ID, done.Attempts) |
| 94 | if err != nil || rec == nil { |
| 95 | t.Fatalf("the run has no stored attempt: %v", err) |
| 96 | } |
| 97 | repoDir := filepath.Join(f.cfg.ReposDir(), f.repo+".git") |
| 98 | tip, _, _ := runGit(t, repoDir, "ls-tree", "-r", "--name-only", "refs/heads/vole") |
| 99 | if !strings.Contains(tip, "releases/v1.0.0/tool-linux") { |
| 100 | t.Fatalf("the release commit does not carry the pointer; it has:\n%s", tip) |
| 101 | } |
| 102 | if strings.Contains(tip, "runs/") { |
| 103 | t.Fatalf("the release commit still carries a run record; it has:\n%s", tip) |
| 104 | } |
| 105 | // And the run page shows the tag it built for. |
| 106 | if _, runPage := f.get("/" + f.repo + "/runs/" + done.ID); !strings.Contains(runPage, "tag v1.0.0") { |
| 107 | t.Fatalf("run page does not show the tag:\n%s", runPage) |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | // TestTagPushRefusesALightweightTag: a lightweight tag has no notes and no tagger, so |
| 112 | // it cannot name a release. The build still runs — the failure is at the publish. |
| 113 | func TestTagPushRefusesALightweightTag(t *testing.T) { |
| 114 | t.Parallel() |
| 115 | f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 5 }) |
| 116 | f.publish(f.repo) |
| 117 | work := workRepo(t, tagWorkflow()) |
| 118 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { |
| 119 | t.Fatalf("push: %s", errs) |
| 120 | } |
| 121 | if _, errs, err := runGit(t, work, "tag", "v2.0.0"); err != nil { |
| 122 | t.Fatalf("tag: %s", errs) |
| 123 | } |
| 124 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "refs/tags/v2.0.0:refs/tags/v2.0.0"); err != nil { |
| 125 | t.Fatalf("push tag: %s", errs) |
| 126 | } |
| 127 | queued := f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool { return r.Status == runs.StatusQueued }) |
| 128 | if queued.TagAnnotated { |
| 129 | t.Fatal("a lightweight tag was reported as annotated") |
| 130 | } |
| 131 | f.runOnce(t) |
| 132 | failed := f.waitForRun(t, 10*time.Second, func(r *runs.Run) bool { return r.Status == runs.StatusFailed }) |
| 133 | _, log := f.get("/" + f.repo + "/runs/" + failed.ID + "/log") |
| 134 | if !strings.Contains(log, "lightweight tag") { |
| 135 | t.Fatalf("the failure does not explain itself:\n%s", log) |
| 136 | } |
| 137 | // Nothing was published, so the release page has no artifacts. |
| 138 | if _, page := f.get("/" + f.repo + "/releases/v2.0.0"); strings.Contains(page, "tool-linux") { |
| 139 | t.Fatalf("a lightweight tag got release artifacts:\n%s", page) |
| 140 | } |
| 141 | } |
| 142 |