Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 {