Skip to content
quarry Artifact [a23c3fa1b3]
Login

Artifact [a23c3fa1b3]

Artifact a23c3fa1b3e8dfceb5990642bc6829aa2936a897f5678eb29ee39b2cb6c2ebd3:


package ci

import (
	"errors"
	"log"
)

// TagStore is the record-keeping the tag walk needs beyond Store's:
// which tags it has already started (RFC 0020's `tag` event).
type TagStore interface {
	Store
	// CITagsSeen is every tag key already considered; primed is false
	// for a repository the tag walk has never looked at.
	CITagsSeen(repo string) (seen map[TagKey]bool, primed bool, err error)
	// MarkCITagsSeen records keys as considered, and the repository as
	// looked at even when keys is empty.
	MarkCITagsSeen(repo string, keys ...TagKey) error
}

// TagRepo is the read side the tag walk needs beyond Repo's.
type TagRepo interface {
	Repo
	// TagsAdded lists every tag added to a check-in the repository
	// knows, oldest card first.
	TagsAdded() ([]Tagged, error)
}

// TagKey names one T card that added a tag to a check-in: the artifact
// carrying it, the check-in, the tag's bare name ("version-0.2.0").
type TagKey struct {
	Src    string
	Target string
	Name   string
}

// Tagged is a tag added to a check-in, and the check-in it was added
// to, branch included. Which T cards count is the index's rule
// (index.TagsAdded): a symbolic tag added, not a branch's own name.
type Tagged struct {
	TagKey
	At Checkin
}

// tagRound bounds how many new tags one round starts, as the check-in
// walk bounds its own; the rest wait for the next round.
const tagRound = 20

// TriggerTags starts the workflows of tags added since the last round
// (RFC 0020's `tag` event). A tag is usually added after its check-in,
// by a control artifact, so Trigger's check-in walk never sees it.
//
// What the walk remembers is a set of tag keys, not a watermark: a tag
// is made where its author works and pushed whenever they push, so its
// card's date says nothing about when the forge received it, and a tag
// can arrive before the check-in it names, which the index lists only
// once the check-in is folded too. Remembering each key starts every
// tag once whatever order it came in, and a rebuilt index lists the
// same keys. The first round that sees a check-in remembers the tags
// there and starts nothing; a transient failure leaves its tag
// unremembered for the next round.
//
// A tag launch neither waits for a worker slot nor supersedes: a
// release is rare, deliberate, and not made stale by the next tag.
func TriggerTags(e *Engine, st TagStore, repo string, r TagRepo, cloneURL string) error {
	if !e.Enabled() {
		return nil
	}
	tags, err := r.TagsAdded()
	if err != nil {
		return err
	}
	seen, primed, err := st.CITagsSeen(repo)
	if err != nil {
		return err
	}
	if !primed {
		// Primed by the first round that sees a check-in, not the first
		// look: an empty repository, or a mirror before its first pull,
		// would otherwise take the pull's whole history for new tags.
		if tip, err := r.NewCheckins("", 1); err != nil || len(tip) == 0 {
			return err
		}
		keys := make([]TagKey, 0, len(tags))
		for _, t := range tags {
			keys = append(keys, t.TagKey)
		}
		return st.MarkCITagsSeen(repo, keys...)
	}
	started := 0
	for _, t := range tags {
		if seen[t.TagKey] {
			continue
		}
		if started == tagRound {
			break
		}
		started++
		c := t.At
		c.Tag = t.Name
		// The tag's starter is whoever carried its artifact here.
		c.Starter = st.PushedBy(repo, t.Src)
		pipelines, err := pipelinesAt(r, c)
		if err == nil {
			err = launch(e, st, repo, r, c, cloneURL, pipelines, scopeTag)
		}
		if errors.Is(err, errTransient) {
			log.Printf("ci: %s %.12s tag %s: %v — retrying next round", repo, c.Hash, t.Name, err)
			return nil
		}
		if err != nil {
			log.Printf("ci: %s %.12s tag %s: %v", repo, c.Hash, t.Name, err)
		}
		if err := st.MarkCITagsSeen(repo, t.TagKey); err != nil {
			return err
		}
	}
	return nil
}