internal/ci/ci.go
80 lines · 2207 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 "regexp"
15 "sort"
16
17 yaml "go.yaml.in/yaml/v3"
18)
19
20// ConfigPath is where the build configuration lives in a repository.
21const ConfigPath = ".gitbay/ci.yml"
22
23const (
24 maxJobs = 10
25 maxSteps = 50
26 maxStepSize = 4096
27)
28
29var jobName = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,39}$`)
30
31type Job struct {
32 Name string
33 Steps []string
34 Schedule string // cron expression; scheduled jobs run on schedule, not on push
35}
36
37// Parse returns the jobs in name order, or an error describing the first
38// problem so the pusher can fix the file.
39func Parse(raw []byte) ([]Job, error) {
40 var doc struct {
41 Jobs map[string]struct {
42 Steps []string `yaml:"steps"`
43 Schedule string `yaml:"schedule"`
44 } `yaml:"jobs"`
45 }
46 if err := yaml.Unmarshal(raw, &doc); err != nil {
47 return nil, fmt.Errorf("parsing %s: %w", ConfigPath, err)
48 }
49 if len(doc.Jobs) == 0 {
50 return nil, fmt.Errorf("%s defines no jobs", ConfigPath)
51 }
52 if len(doc.Jobs) > maxJobs {
53 return nil, fmt.Errorf("%s defines %d jobs; max %d", ConfigPath, len(doc.Jobs), maxJobs)
54 }
55 var jobs []Job
56 for name, j := range doc.Jobs {
57 if !jobName.MatchString(name) {
58 return nil, fmt.Errorf("bad job name %q: lowercase letters, digits, - and _; max 40 chars", name)
59 }
60 if len(j.Steps) == 0 {
61 return nil, fmt.Errorf("job %q has no steps", name)
62 }
63 if len(j.Steps) > maxSteps {
64 return nil, fmt.Errorf("job %q has %d steps; max %d", name, len(j.Steps), maxSteps)
65 }
66 for _, s := range j.Steps {
67 if len(s) > maxStepSize {
68 return nil, fmt.Errorf("job %q has a step over %d bytes", name, maxStepSize)
69 }
70 }
71 if j.Schedule != "" {
72 if _, err := ParseCron(j.Schedule); err != nil {
73 return nil, fmt.Errorf("job %q: %v", name, err)
74 }
75 }
76 jobs = append(jobs, Job{Name: name, Steps: j.Steps, Schedule: j.Schedule})
77 }
78 sort.Slice(jobs, func(i, k int) bool { return jobs[i].Name < jobs[k].Name })
79 return jobs, nil
80}