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.
|
0f442b1c
Keenan Nemetz
17d ago
|
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 | ||
|
d37d301f
Keenan Nemetz
16h ago
|
13 | "rodent.software/vole/ci/runs" |
| 14 | "rodent.software/vole/forge/config" | |
|
0f442b1c
Keenan Nemetz
17d ago
|
15 | ) |
| 16 | ||
| 17 | func tagWorkflow() map[string]string { | |
| 18 | return workflowFile(`version: 1 | |
| 19 | on: | |
| 20 | push: | |
| 21 | tags: ['v*'] | |
| 22 | jobs: | |
| 23 | release: | |
|
70b70264
Keenan Nemetz
15d ago
|
24 | image: alpine:3.22 |
|
3b39a84c
Keenan Nemetz
14d ago
|
25 | env: |
| 26 | RELEASE_KIND: tool | |
|
0f442b1c
Keenan Nemetz
17d ago
|
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) { | |
|
44530e74
Keenan Nemetz
16d ago
|
37 | t.Parallel() |
|
0f442b1c
Keenan Nemetz
17d ago
|
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 | } | |
|
3b39a84c
Keenan Nemetz
14d ago
|
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 | } | |
|
0f442b1c
Keenan Nemetz
17d ago
|
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`. | |
|
ed03b3d1
Keenan Nemetz
3h ago
|
79 | code, page := f.get("/" + f.repo + "/releases/v1.0.0") |
|
0f442b1c
Keenan Nemetz
17d ago
|
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 | } | |
|
ed03b3d1
Keenan Nemetz
3h ago
|
86 | code, body := f.get("/" + f.repo + "/releases/v1.0.0/artifacts/tool-linux") |
|
0f442b1c
Keenan Nemetz
17d ago
|
87 | if code != 200 || body != "release binary\n" { |
| 88 | t.Fatalf("artifact download = %d %q", code, body) | |
| 89 | } | |
|
b312e166
Keenan Nemetz
10d ago
|
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) | |
|
0f442b1c
Keenan Nemetz
17d ago
|
96 | } |
|
b312e166
Keenan Nemetz
10d ago
|
97 | repoDir := filepath.Join(f.cfg.ReposDir(), f.repo+".git") |
|
90fdaf39
Keenan Nemetz
16d ago
|
98 | tip, _, _ := runGit(t, repoDir, "ls-tree", "-r", "--name-only", "refs/heads/vole") |
|
b312e166
Keenan Nemetz
10d ago
|
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) | |
|
90fdaf39
Keenan Nemetz
16d ago
|
104 | } |
|
0f442b1c
Keenan Nemetz
17d ago
|
105 | // And the run page shows the tag it built for. |
|
ed03b3d1
Keenan Nemetz
3h ago
|
106 | if _, runPage := f.get("/" + f.repo + "/runs/" + done.ID); !strings.Contains(runPage, "tag v1.0.0") { |
|
0f442b1c
Keenan Nemetz
17d ago
|
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) { | |
|
44530e74
Keenan Nemetz
16d ago
|
114 | t.Parallel() |
|
0f442b1c
Keenan Nemetz
17d ago
|
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 }) | |
|
ed03b3d1
Keenan Nemetz
3h ago
|
133 | _, log := f.get("/" + f.repo + "/runs/" + failed.ID + "/log") |
|
0f442b1c
Keenan Nemetz
17d ago
|
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. | |
|
ed03b3d1
Keenan Nemetz
3h ago
|
138 | if _, page := f.get("/" + f.repo + "/releases/v2.0.0"); strings.Contains(page, "tool-linux") { |
|
0f442b1c
Keenan Nemetz
17d ago
|
139 | t.Fatalf("a lightweight tag got release artifacts:\n%s", page) |
| 140 | } | |
| 141 | } |