internal/ci/ci.go

d6d57309d9ddb202b5c9a29ff4f4d22c000f3874
gitbay/internal/ci/ci.go history · blame · raw

117 lines · 3655 bytes

  1// Package ci parses .gitbay/ci.yml, the per-repo build configuration:
  2//
  3//	jobs:
  4//	  test:
  5//	    steps:
  6//	      - go test ./...
  7//
  8// Each job becomes one build per push; each step is a shell command the
  9// runner executes with `sh -c`, stopping at the first failure.
 10package ci
 11
 12import (
 13	"fmt"
 14	"path"
 15	"regexp"
 16	"sort"
 17
 18	yaml "go.yaml.in/yaml/v3"
 19)
 20
 21// ConfigPath is where the build configuration lives in a repository.
 22const ConfigPath = ".gitbay/ci.yml"
 23
 24const (
 25	maxJobs     = 10
 26	maxSteps    = 50
 27	maxStepSize = 4096
 28	maxPaths    = 50
 29)
 30
 31var jobName = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,39}$`)
 32
 33type Job struct {
 34	Name     string
 35	Steps    []string
 36	Schedule string // cron expression; scheduled jobs run on schedule, not on push
 37	Tags     string // tag glob (e.g. "v*"); tag jobs run on matching tag pushes only
 38	// Paths and PathsIgnore only gate a job queued on push; a scheduled
 39	// or tag job ignores them.
 40	Paths       []string // globs; the job runs only when a changed file matches one
 41	PathsIgnore []string // globs; the job is skipped when every changed file matches one
 42}
 43
 44// Parse returns the jobs in name order, or an error describing the first
 45// problem so the pusher can fix the file.
 46func Parse(raw []byte) ([]Job, error) {
 47	var doc struct {
 48		Jobs map[string]struct {
 49			Steps       []string `yaml:"steps"`
 50			Schedule    string   `yaml:"schedule"`
 51			Tags        string   `yaml:"tags"`
 52			Paths       []string `yaml:"paths"`
 53			PathsIgnore []string `yaml:"paths-ignore"`
 54		} `yaml:"jobs"`
 55	}
 56	if err := yaml.Unmarshal(raw, &doc); err != nil {
 57		return nil, fmt.Errorf("parsing %s: %w", ConfigPath, err)
 58	}
 59	if len(doc.Jobs) == 0 {
 60		return nil, fmt.Errorf("%s defines no jobs", ConfigPath)
 61	}
 62	if len(doc.Jobs) > maxJobs {
 63		return nil, fmt.Errorf("%s defines %d jobs; max %d", ConfigPath, len(doc.Jobs), maxJobs)
 64	}
 65	var jobs []Job
 66	for name, j := range doc.Jobs {
 67		if !jobName.MatchString(name) {
 68			return nil, fmt.Errorf("bad job name %q: lowercase letters, digits, - and _; max 40 chars", name)
 69		}
 70		if len(j.Steps) == 0 {
 71			return nil, fmt.Errorf("job %q has no steps", name)
 72		}
 73		if len(j.Steps) > maxSteps {
 74			return nil, fmt.Errorf("job %q has %d steps; max %d", name, len(j.Steps), maxSteps)
 75		}
 76		for _, s := range j.Steps {
 77			if len(s) > maxStepSize {
 78				return nil, fmt.Errorf("job %q has a step over %d bytes", name, maxStepSize)
 79			}
 80		}
 81		if j.Schedule != "" {
 82			if _, err := ParseCron(j.Schedule); err != nil {
 83				return nil, fmt.Errorf("job %q: %v", name, err)
 84			}
 85		}
 86		if j.Tags != "" {
 87			if _, err := path.Match(j.Tags, "x"); err != nil {
 88				return nil, fmt.Errorf("job %q: bad tag pattern %q", name, j.Tags)
 89			}
 90			if j.Schedule != "" {
 91				return nil, fmt.Errorf("job %q: schedule and tags are mutually exclusive", name)
 92			}
 93		}
 94		if len(j.Paths) > maxPaths {
 95			return nil, fmt.Errorf("job %q has %d path patterns; max %d", name, len(j.Paths), maxPaths)
 96		}
 97		for _, p := range j.Paths {
 98			if _, err := path.Match(p, "x"); err != nil {
 99				return nil, fmt.Errorf("job %q: bad path pattern %q", name, p)
100			}
101		}
102		if len(j.PathsIgnore) > maxPaths {
103			return nil, fmt.Errorf("job %q has %d paths-ignore patterns; max %d", name, len(j.PathsIgnore), maxPaths)
104		}
105		for _, p := range j.PathsIgnore {
106			if _, err := path.Match(p, "x"); err != nil {
107				return nil, fmt.Errorf("job %q: bad paths-ignore pattern %q", name, p)
108			}
109		}
110		jobs = append(jobs, Job{
111			Name: name, Steps: j.Steps, Schedule: j.Schedule, Tags: j.Tags,
112			Paths: j.Paths, PathsIgnore: j.PathsIgnore,
113		})
114	}
115	sort.Slice(jobs, func(i, k int) bool { return jobs[i].Name < jobs[k].Name })
116	return jobs, nil
117}