Skip to content
quarry Artifact [4e56a3b7a0]
Login

Artifact [4e56a3b7a0]

Artifact 4e56a3b7a0b81102d3c83316405387de6c12da2e3f9efff2e8f22418b8c8f88b:


package ci

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"slices"
	"strings"
	"sync"
	"testing"
)

// A TAG ADDED TO A CHECK-IN STARTS ITS TAG WORKFLOWS, ONCE (RFC 0020).
//
// The run's subject is the tagged check-in, recorded as an ordinary run
// of it, so secret delivery judges it by that check-in's branch as it
// judges every run. Only workflows whose on.tag matches start: not the
// push tests, not a JSON pipeline. A tag run supersedes nothing, and a
// first sight starts nothing.
func TestATagStartsItsWorkflowsOnTheTaggedCheckin(t *testing.T) {
	var mu sync.Mutex
	var started []string
	var labels []map[string]string
	superseded := 0
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		mu.Lock()
		defer mu.Unlock()
		switch {
		case r.URL.Path == "/v1/ci/compile":
			var body struct{ Name, Spec string }
			json.NewDecoder(r.Body).Decode(&body)
			on, _, _ := strings.Cut(body.Spec, "\n")
			fmt.Fprintf(w, `{"workflow":{"name":%q},"registered":true,"on":%s}`, body.Name, on)
		case r.URL.Path == "/workflows":
			w.WriteHeader(http.StatusCreated)
			fmt.Fprint(w, `{"status":"registered"}`)
		case r.URL.Path == "/runs" && r.Method == http.MethodPost:
			var body struct {
				Input  map[string]any    `json:"input"`
				Labels map[string]string `json:"labels"`
			}
			json.NewDecoder(r.Body).Decode(&body)
			started = append(started, fmt.Sprint(body.Input["checkin"], " ", body.Input["branch"], " ", body.Input["pipeline"]))
			labels = append(labels, body.Labels)
			w.WriteHeader(http.StatusCreated)
			fmt.Fprint(w, `{"run_id":"r"}`)
		case r.URL.Path == "/runs":
			superseded++
			fmt.Fprint(w, `[]`)
		}
	}))
	defer srv.Close()
	take := func() []string {
		mu.Lock()
		defer mu.Unlock()
		out := started
		started = nil
		slices.Sort(out)
		return out
	}
	e := New(srv.URL)
	st := &fakeStore{mark: map[string]string{"alpha": "t1"}}
	repo := &fakeRepo{
		files: []string{"ci/build.json", "ci/release.yml", "ci/test.yml"},
		blobs: map[string][]byte{
			"ci/build.json":  []byte(`{"name":"build","version":"1.0","steps":[]}`),
			"ci/test.yml":    []byte(`{"push":{},"landing":{}}` + "\njobs: {}"),
			"ci/release.yml": []byte(`{"tag":{"tags":["version-*"]}}` + "\njobs: {}"),
		},
	}
	at := func(hash, branch string) Checkin { return Checkin{Hash: hash, Branch: branch, User: "dan"} }
	mark := func(date, name, target string) TagMark {
		return TagMark{Date: date, Src: "src-" + date, Target: target, Name: name}
	}
	old := Tagged{TagMark: mark("2026-09-01", "version-0.1.0", "t0"), At: at("t0", "trunk")}

	// First sight: the newest tag becomes the watermark and nothing runs.
	repo.tags = []Tagged{old}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if got := take(); len(got) != 0 {
		t.Errorf("FAIL: first sight started %v; want nothing", got)
	}
	if m, ok := st.CITagWatermark("alpha"); !ok || m != old.TagMark {
		t.Errorf("FAIL: first sight left the tag watermark at %+v, %v; want the newest tag", m, ok)
	}

	release := Tagged{TagMark: mark("2026-10-01", "version-0.2.0", "t1"), At: at("t1", "trunk")}
	moving := Tagged{TagMark: mark("2026-10-01b", "release", "t1"), At: at("t1", "trunk")}
	repo.tags = []Tagged{old, release, moving}
	superseded = 0
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if got := take(); !slices.Equal(got, []string{"t1 trunk ci/release.yml"}) {
		t.Errorf("FAIL: tagging t1 version-0.2.0 and release started %v; want the release workflow once, on t1", got)
	}
	if len(st.runs) != 1 || !strings.HasPrefix(st.runs[0], "r t1 ") {
		t.Errorf("FAIL: recorded runs %v; want one run of t1, the tagged check-in", st.runs)
	}
	if len(labels) != 1 || labels[0]["tag"] != "version-0.2.0" || labels[0]["branch"] != "trunk" || labels[0]["checkin"] != "t1" {
		t.Errorf("FAIL: the tag run's labels %v; want its tag, branch and check-in", labels)
	}
	if superseded != 0 {
		t.Errorf("FAIL: a tag run swept %d time(s) for runs to supersede; want none", superseded)
	}
	if m, _ := st.CITagWatermark("alpha"); m != moving.TagMark {
		t.Errorf("FAIL: the tag watermark is %+v; want it past the non-matching release tag too", m)
	}

	// Nothing new: nothing starts again.
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if got := take(); len(got) != 0 {
		t.Errorf("FAIL: a round with no new tag started %v", got)
	}

	// A tag on a branch is still a tag event; whether its run receives
	// secrets is the environment's branch policy, read from the branch
	// of the check-in the run records.
	feat := Tagged{TagMark: mark("2026-10-02", "version-0.3.0", "b1"), At: at("b1", "feat")}
	repo.tags = append(repo.tags, feat)
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if got := take(); !slices.Equal(got, []string{"b1 feat ci/release.yml"}) {
		t.Errorf("FAIL: a tag on feat started %v; want the release workflow on b1, branch feat", got)
	}
	t.Log("ok: a tag starts its tag workflows once, on the tagged check-in, with its branch")
}

// A repository with no tags at first sight starts from before every
// tag, so its first tag is not mistaken for history.
func TestTheFirstTagOfAnUntaggedRepositoryStarts(t *testing.T) {
	var mu sync.Mutex
	var started []string
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		mu.Lock()
		defer mu.Unlock()
		switch {
		case r.URL.Path == "/v1/ci/compile":
			var body struct{ Name, Spec string }
			json.NewDecoder(r.Body).Decode(&body)
			on, _, _ := strings.Cut(body.Spec, "\n")
			fmt.Fprintf(w, `{"workflow":{"name":%q},"registered":true,"on":%s}`, body.Name, on)
		case r.URL.Path == "/runs" && r.Method == http.MethodPost:
			var body struct {
				Input map[string]any `json:"input"`
			}
			json.NewDecoder(r.Body).Decode(&body)
			started = append(started, fmt.Sprint(body.Input["checkin"]))
			w.WriteHeader(http.StatusCreated)
			fmt.Fprint(w, `{"run_id":"r"}`)
		case r.URL.Path == "/runs":
			fmt.Fprint(w, `[]`)
		}
	}))
	defer srv.Close()
	e := New(srv.URL)
	st := &fakeStore{mark: map[string]string{"alpha": "t1"}}
	repo := &fakeRepo{
		files: []string{"ci/release.yml"},
		blobs: map[string][]byte{"ci/release.yml": []byte(`{"tag":{}}` + "\njobs: {}")},
	}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	if m, ok := st.CITagWatermark("alpha"); !ok || m != (TagMark{}) {
		t.Fatalf("FAIL: an untagged first sight left %+v, %v; want the zero mark", m, ok)
	}
	repo.tags = []Tagged{{TagMark: TagMark{Date: "2026-10-01", Src: "s", Target: "t1", Name: "version-0.2.0"}, At: Checkin{Hash: "t1", Branch: "trunk"}}}
	if err := TriggerTags(e, st, "alpha", repo, "u"); err != nil {
		t.Fatal(err)
	}
	mu.Lock()
	defer mu.Unlock()
	if !slices.Equal(started, []string{"t1"}) {
		t.Errorf("FAIL: the first tag started %v; want t1", started)
	}
	t.Log("ok: the first tag of an untagged repository starts")
}