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.
Phase 7 slice 8: a tag push builds the release
0f442b1c87ca3920f770531c9ad97277d0231f51 · Keenan Nemetz · 17d ago · parent 88e980b0
Phase 7 slice 8: a tag push builds the release on.push.tags opens CI to tag pushes; release: true on an artifact publishes it to that tag's release rather than only to the run. The runner commits the same pointers `vole release attach` writes, so the release page, the ETags, the immutability rules and the sweep needed no changes: CI gained a trigger, not a publish path. Three rules, each from something that went wrong in the first draft: - a workflow that says tags: runs ONLY on tags. Saying what you trigger on is saying it; the first version defaulted to every branch too and queued a doomed release build on every commit - a tag run never counts as a branch's required check. A tag on a branch tip produces a second run for the same sha, and ForSHA would have let a release build satisfy — or break — the gate for the commit it tags. It now filters to refs/heads/% - a lightweight tag cannot name a release: the build runs and the publish fails saying so, because a tag with no message has no notes and no tagger. The server resolves annotated-ness at enqueue (the tag object's type) rather than having the runner parse ls-remote peeled lines Tag and annotated-ness ride the run and the job payload beside the release artifact patterns, pinned at enqueue like everything else. Measured through the binary: a branch push queued nothing, a tag push queued the release job, the runner published dist/tool-linux, the release page listed it with the tag marked annotated, the download returned the bytes, and git log vole read "release: v1.0.0 artifacts from run 01M5MSX3...".
internal/server/ci.go +61−22
| @@ -45,14 +45,24 @@ func (s *Server) watchSpool(ctx context.Context) { | |||
| 45 | 45 | } | |
| 46 | 46 | } | |
| 47 | 47 | ||
| 48 | // enqueue creates one run per job of every workflow that matches the pushed | ||
| 49 | // branch, pinned to the commit and the workflow blob that were pushed. | ||
| 48 | // enqueue creates one run per job of every workflow that matches the pushed branch | ||
| 49 | // or tag, pinned to the commit and the workflow blob that were pushed. | ||
| 50 | // | ||
| 51 | // A tag push is a release build: the tag names what is being released, so a workflow | ||
| 52 | // opts into it with on.push.tags. A tag has no branch, and a branch has no tag, which | ||
| 53 | // is why the two are carried separately rather than as one "ref". | ||
| 50 | 54 | func (s *Server) enqueue(ev runs.Event) { | |
| 51 | if !strings.HasPrefix(ev.Ref, "refs/heads/") { | ||
| 52 | return // tags and the metadata branch do not run CI | ||
| 55 | var branch, tag string | ||
| 56 | switch { | ||
| 57 | case strings.HasPrefix(ev.Ref, "refs/heads/"): | ||
| 58 | branch = strings.TrimPrefix(ev.Ref, "refs/heads/") | ||
| 59 | case strings.HasPrefix(ev.Ref, "refs/tags/"): | ||
| 60 | tag = strings.TrimPrefix(ev.Ref, "refs/tags/") | ||
| 61 | default: | ||
| 62 | return // the metadata ref(s) and refs/vole/* do not run CI | ||
| 53 | 63 | } | |
| 54 | 64 | if ev.New == "" || strings.HasPrefix(ev.New, "0000000000000000000000000000000000000000") { | |
| 55 | return // a branch deletion | ||
| 65 | return // a deletion | ||
| 56 | 66 | } | |
| 57 | 67 | m, err := s.Repos.Open(ev.Repo) | |
| 58 | 68 | if err != nil { | |
| @@ -60,13 +70,26 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 60 | 70 | return | |
| 61 | 71 | } | |
| 62 | 72 | // Trust the ref, not the spool: a push that lost a race may have left an event | |
| 63 | // whose new commit never became the tip. | ||
| 64 | tip, err := m.Git().RevParse(ev.Ref) | ||
| 73 | // whose new object never became the tip. For a tag that object is the tag, so the | ||
| 74 | // comparison is against the raw ref, not the commit it peels to. | ||
| 75 | tip, err := m.Git().RevParseRaw(ev.Ref) | ||
| 65 | 76 | if err != nil || tip != ev.New { | |
| 66 | 77 | s.Log.Debug("ci: skipping a spool entry whose ref has moved", "repo", ev.Repo, "ref", ev.Ref) | |
| 67 | 78 | return | |
| 68 | 79 | } | |
| 69 | wfs, problems := workflow.At(m.Git(), ev.New) | ||
| 80 | // A run checks out a commit; a tag object is not one. | ||
| 81 | commit, err := m.Git().RevParse(ev.Ref) | ||
| 82 | if err != nil || commit == "" { | ||
| 83 | s.Log.Warn("ci: cannot resolve the pushed ref to a commit", "repo", ev.Repo, "ref", ev.Ref, "err", err) | ||
| 84 | return | ||
| 85 | } | ||
| 86 | annotated := false | ||
| 87 | if tag != "" { | ||
| 88 | if kind, err := m.Git().ObjectType(ev.Ref); err == nil { | ||
| 89 | annotated = kind == "tag" | ||
| 90 | } | ||
| 91 | } | ||
| 92 | wfs, problems := workflow.At(m.Git(), commit) | ||
| 70 | 93 | s.Log.Debug("ci: examined push", "repo", ev.Repo, "ref", ev.Ref, "workflows", len(wfs), "problems", len(problems)) | |
| 71 | 94 | if len(problems) > 0 { | |
| 72 | 95 | // pre-receive rejects these, so reaching here means a workflow was already | |
| @@ -74,24 +97,29 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 74 | 97 | s.Log.Warn("ci: workflows in a pushed commit do not validate", "repo", ev.Repo, "problems", len(problems)) | |
| 75 | 98 | return | |
| 76 | 99 | } | |
| 77 | branch := strings.TrimPrefix(ev.Ref, "refs/heads/") | ||
| 78 | 100 | for _, wf := range wfs { | |
| 79 | if !wf.Applies(branch) { | ||
| 101 | switch { | ||
| 102 | case branch != "" && !wf.Applies(branch): | ||
| 103 | continue | ||
| 104 | case tag != "" && !wf.AppliesToTag(tag): | ||
| 80 | 105 | continue | |
| 81 | 106 | } | |
| 82 | 107 | for _, job := range wf.Jobs { | |
| 83 | 108 | run := runs.Run{ | |
| 84 | Repo: ev.Repo, | ||
| 85 | Ref: ev.Ref, | ||
| 86 | Branch: branch, | ||
| 87 | SHA: ev.New, | ||
| 88 | Workflow: wf.Path, | ||
| 89 | WorkflowSHA: wf.BlobSHA, | ||
| 90 | Job: job.ID, | ||
| 91 | Steps: steps(job), | ||
| 92 | Artifacts: artifactPaths(job), | ||
| 93 | PagesPath: pagesPath(job), | ||
| 94 | Created: time.Now().UTC(), | ||
| 109 | Repo: ev.Repo, | ||
| 110 | Ref: ev.Ref, | ||
| 111 | Branch: branch, | ||
| 112 | Tag: tag, | ||
| 113 | TagAnnotated: annotated, | ||
| 114 | SHA: commit, | ||
| 115 | Workflow: wf.Path, | ||
| 116 | WorkflowSHA: wf.BlobSHA, | ||
| 117 | Job: job.ID, | ||
| 118 | Steps: steps(job), | ||
| 119 | Artifacts: artifactPaths(job), | ||
| 120 | ReleaseArtifacts: releasePaths(job), | ||
| 121 | PagesPath: pagesPath(job), | ||
| 122 | Created: time.Now().UTC(), | ||
| 95 | 123 | } | |
| 96 | 124 | run.ID, _ = id.New(time.Now()) | |
| 97 | 125 | created, err := s.Runs.Enqueue(run) | |
| @@ -100,12 +128,23 @@ func (s *Server) enqueue(ev runs.Event) { | |||
| 100 | 128 | continue | |
| 101 | 129 | } | |
| 102 | 130 | if created { | |
| 103 | s.Log.Info("ci: queued", "repo", ev.Repo, "job", job.ID, "sha", ev.New[:8], "run", run.ID) | ||
| 131 | s.Log.Info("ci: queued", "repo", ev.Repo, "job", job.ID, "sha", commit[:8], "run", run.ID) | ||
| 104 | 132 | } | |
| 105 | 133 | } | |
| 106 | 134 | } | |
| 107 | 135 | } | |
| 108 | 136 | ||
| 137 | // releasePaths picks the patterns that also publish to the tag's release. | ||
| 138 | func releasePaths(job workflow.Job) []string { | ||
| 139 | var out []string | ||
| 140 | for _, a := range job.Artifacts { | ||
| 141 | if a.Release { | ||
| 142 | out = append(out, a.Path) | ||
| 143 | } | ||
| 144 | } | ||
| 145 | return out | ||
| 146 | } | ||
| 147 | |||
| 109 | 148 | func steps(job workflow.Job) []runs.Step { | |
| 110 | 149 | out := make([]runs.Step, 0, len(job.Steps)) | |
| 111 | 150 | for _, st := range job.Steps { | |