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
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
}