internal/ci/ci.go

2e62f52baae22d14056deefc922dc1b9ec9b7bc5
gitbay/internal/ci/ci.go history · blame · raw

73 lines · 1919 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}
35
36// Parse returns the jobs in name order, or an error describing the first
37// problem so the pusher can fix the file.
38func Parse(raw []byte) ([]Job, error) {
39	var doc struct {
40		Jobs map[string]struct {
41			Steps []string `yaml:"steps"`
42		} `yaml:"jobs"`
43	}
44	if err := yaml.Unmarshal(raw, &doc); err != nil {
45		return nil, fmt.Errorf("parsing %s: %w", ConfigPath, err)
46	}
47	if len(doc.Jobs) == 0 {
48		return nil, fmt.Errorf("%s defines no jobs", ConfigPath)
49	}
50	if len(doc.Jobs) > maxJobs {
51		return nil, fmt.Errorf("%s defines %d jobs; max %d", ConfigPath, len(doc.Jobs), maxJobs)
52	}
53	var jobs []Job
54	for name, j := range doc.Jobs {
55		if !jobName.MatchString(name) {
56			return nil, fmt.Errorf("bad job name %q: lowercase letters, digits, - and _; max 40 chars", name)
57		}
58		if len(j.Steps) == 0 {
59			return nil, fmt.Errorf("job %q has no steps", name)
60		}
61		if len(j.Steps) > maxSteps {
62			return nil, fmt.Errorf("job %q has %d steps; max %d", name, len(j.Steps), maxSteps)
63		}
64		for _, s := range j.Steps {
65			if len(s) > maxStepSize {
66				return nil, fmt.Errorf("job %q has a step over %d bytes", name, maxStepSize)
67			}
68		}
69		jobs = append(jobs, Job{Name: name, Steps: j.Steps})
70	}
71	sort.Slice(jobs, func(i, k int) bool { return jobs[i].Name < jobs[k].Name })
72	return jobs, nil
73}