detached 9cf140e

feat(ci): store CI runs, jobs, checks and runners, and keep job logs in files

Add the storage schema (proto/ermite/storage/ci.proto), the CI domain
types, the ports (CIStore, CILogStore, WorkflowTrigger, CIGit and the
inbound RunnerService and RunService), the ermite-kv store with its
derived indexes (open runs, ready queue, active jobs, job tokens), run
ids and per-workflow run numbers taken under version preconditions,
repository deletion cleanup and the retention sweep, and the file log
store under $ERMITE_DATA_DIR/ci/logs with per-line timestamps and the
continued flag. ermite-actions joins go.mod and ermite-api moves to its
main.
commit
9cf140e4e72e50102fcfaaf9ad0513dd9613ab76
author
brice · · 2026-10-12 17:27 UTC
parent
fc4d564
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
17 files · +8292 -3
A gen/ermite/storage/ci.pb.go +1530 -0 generated file: collapsed
M go.mod +2 -1
@@ -12,7 +12,8 @@ tool (
1212 )
1313
1414 require (
15- bnema.dev/ermite-api v0.0.0-20261011182719-cb5699451db6
15+ bnema.dev/ermite-actions v0.0.0-20261012161738-a52e9b5cddd6
16+ bnema.dev/ermite-api v0.0.0-20261012142818-97a93650f8b7
1617 bnema.dev/ermite-git v0.1.0
1718 bnema.dev/ermite-kv v0.0.0-20261010031554-5e09b48f81c3
1819 connectrpc.com/connect v1.21.0
A internal/adapters/cilogs/cilogs.go +371 -0
@@ -0,0 +1,371 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+// Package cilogs implements ports.CILogStore with append-only files under
4+// $ERMITE_DATA_DIR/ci/logs/<run id>/<job id>/<attempt>.log.
5+//
6+// A file holds one line per log line, in GitHub's raw log shape: an RFC 3339
7+// timestamp with nanoseconds, a space, then the text, behind a one-character
8+// marker: "-" for a line, "+" for a piece that continues the line before it
9+// (it keeps its own line number):
10+//
11+// - 2026-10-12T10:00:00.123456789Z text
12+// - 2026-10-12T10:00:00.123456789Z more text of the same line
13+//
14+// The file is the truth for bytes and line counts: the metadata store is never
15+// written per chunk. Counts are cached per open log and rebuilt from the file
16+// after a restart.
17+package cilogs
18+
19+import (
20+ "bufio"
21+ "bytes"
22+ "context"
23+ "errors"
24+ "fmt"
25+ "io"
26+ "io/fs"
27+ "os"
28+ "path/filepath"
29+ "strconv"
30+ "sync"
31+ "time"
32+
33+ "bnema.dev/ermite/internal/domain"
34+ "bnema.dev/ermite/internal/ports"
35+)
36+
37+// Dir is the directory of the logs under the data directory.
38+func Dir(dataDir string) string { return filepath.Join(dataDir, "ci", "logs") }
39+
40+// timeLayout is RFC 3339 with nanoseconds, always in UTC and with all nine
41+// digits, so that every prefix has the same length.
42+const timeLayout = "2006-01-02T15:04:05.000000000Z"
43+
44+// prefixLen is the length of "- " + timestamp + " ".
45+const prefixLen = 2 + len(timeLayout) + 1
46+
47+// Store is the file-backed log store.
48+type Store struct {
49+ root string
50+
51+ mu sync.Mutex
52+ logs map[ports.CILogRef]*logState
53+}
54+
55+var _ ports.CILogStore = (*Store)(nil)
56+
57+// logState is what is known of one log file.
58+type logState struct {
59+ mu sync.Mutex
60+ loaded bool
61+ count int64
62+ size int64
63+ offsets []int64 // byte offset of every 1024th line: index i is line i*1024
64+ changed chan struct{}
65+}
66+
67+const offsetEvery = 1024
68+
69+// New returns a store rooted at dir, which need not exist yet.
70+func New(dir string) *Store {
71+ return &Store{root: dir, logs: make(map[ports.CILogRef]*logState)}
72+}
73+
74+func (s *Store) path(ref ports.CILogRef) string {
75+ return filepath.Join(s.root, strconv.FormatInt(ref.RunID, 10), ref.JobID, strconv.Itoa(int(ref.Attempt))+".log")
76+}
77+
78+func (s *Store) state(ref ports.CILogRef) *logState {
79+ s.mu.Lock()
80+ defer s.mu.Unlock()
81+ st, ok := s.logs[ref]
82+ if !ok {
83+ st = &logState{changed: make(chan struct{})}
84+ s.logs[ref] = st
85+ }
86+ return st
87+}
88+
89+// validRef rejects a reference that could escape the directory.
90+func validRef(ref ports.CILogRef) bool {
91+ if ref.RunID < 1 || ref.Attempt < 1 {
92+ return false
93+ }
94+ _, _, _, ok := domain.ParseJobID(ref.JobID)
95+ return ok
96+}
97+
98+// load reads the file to learn its line count and size, once. The caller
99+// holds st.mu.
100+func (s *Store) load(ref ports.CILogRef, st *logState) error {
101+ if st.loaded {
102+ return nil
103+ }
104+ f, err := os.Open(s.path(ref))
105+ if errors.Is(err, fs.ErrNotExist) {
106+ st.loaded = true
107+ return nil
108+ }
109+ if err != nil {
110+ return fmt.Errorf("open log: %w", err)
111+ }
112+ defer func() { _ = f.Close() }()
113+ r := bufio.NewReaderSize(f, 1<<16)
114+ var count, pos int64
115+ for {
116+ line, err := r.ReadBytes('\n')
117+ if len(line) > 0 && line[len(line)-1] == '\n' {
118+ if count%offsetEvery == 0 {
119+ st.offsets = append(st.offsets, pos)
120+ }
121+ count++
122+ pos += int64(len(line))
123+ } else if len(line) > 0 {
124+ // A torn last line from a crash: cut it, it was never acknowledged.
125+ if terr := os.Truncate(s.path(ref), pos); terr != nil {
126+ return fmt.Errorf("repair log: %w", terr)
127+ }
128+ }
129+ if err != nil {
130+ if errors.Is(err, io.EOF) {
131+ break
132+ }
133+ return fmt.Errorf("read log: %w", err)
134+ }
135+ }
136+ st.count, st.size, st.loaded = count, pos, true
137+ return nil
138+}
139+
140+func encodeLine(l domain.CILogLine) []byte {
141+ b := make([]byte, 0, prefixLen+len(l.Text)+1)
142+ if l.Continued {
143+ b = append(b, '+', ' ')
144+ } else {
145+ b = append(b, '-', ' ')
146+ }
147+ b = l.Time.UTC().AppendFormat(b, timeLayout)
148+ b = append(b, ' ')
149+ b = append(b, l.Text...)
150+ return append(b, '\n')
151+}
152+
153+func decodeLine(b []byte) (domain.CILogLine, error) {
154+ b = bytes.TrimSuffix(b, []byte{'\n'})
155+ if len(b) < prefixLen || (b[0] != '-' && b[0] != '+') {
156+ return domain.CILogLine{}, errors.New("malformed log line")
157+ }
158+ t, err := time.Parse(timeLayout, string(b[2:2+len(timeLayout)]))
159+ if err != nil {
160+ return domain.CILogLine{}, fmt.Errorf("malformed log time: %w", err)
161+ }
162+ return domain.CILogLine{Time: t, Text: string(b[prefixLen:]), Continued: b[0] == '+'}, nil
163+}
164+
165+// Append appends lines at offset.
166+func (s *Store) Append(ctx context.Context, ref ports.CILogRef, offset int64, lines []domain.CILogLine, limit int64) (int64, int, error) {
167+ return s.append(ctx, ref, offset, lines, limit, false)
168+}
169+
170+// AppendForced appends lines whatever the limit.
171+func (s *Store) AppendForced(ctx context.Context, ref ports.CILogRef, lines []domain.CILogLine) (int64, error) {
172+ next, _, err := s.append(ctx, ref, -1, lines, 0, true)
173+ return next, err
174+}
175+
176+func (s *Store) append(_ context.Context, ref ports.CILogRef, offset int64, lines []domain.CILogLine, limit int64, force bool) (int64, int, error) {
177+ if !validRef(ref) {
178+ return 0, 0, domain.ErrNotFound
179+ }
180+ st := s.state(ref)
181+ st.mu.Lock()
182+ defer st.mu.Unlock()
183+ if err := s.load(ref, st); err != nil {
184+ return 0, 0, err
185+ }
186+ if offset < 0 {
187+ offset = st.count
188+ }
189+ if offset > st.count {
190+ return 0, 0, &ports.ErrCILogGap{Count: st.count}
191+ }
192+ skip := st.count - offset
193+ if skip >= int64(len(lines)) {
194+ return st.count, 0, nil
195+ }
196+ fresh := lines[skip:]
197+ var buf bytes.Buffer
198+ for _, l := range fresh {
199+ buf.Write(encodeLine(l))
200+ }
201+ if !force && st.size+int64(buf.Len()) > limit {
202+ return st.count, 0, ports.ErrCILogLimit
203+ }
204+ if err := os.MkdirAll(filepath.Dir(s.path(ref)), 0o700); err != nil {
205+ return 0, 0, fmt.Errorf("create log directory: %w", err)
206+ }
207+ f, err := os.OpenFile(s.path(ref), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o600)
208+ if err != nil {
209+ return 0, 0, fmt.Errorf("open log: %w", err)
210+ }
211+ n, werr := f.Write(buf.Bytes())
212+ cerr := f.Close()
213+ if werr != nil || cerr != nil {
214+ // Cut what was written so that the file keeps whole lines.
215+ _ = os.Truncate(s.path(ref), st.size)
216+ return 0, 0, fmt.Errorf("write log: %w", errors.Join(werr, cerr))
217+ }
218+ pos := st.size
219+ for i, l := range fresh {
220+ if (st.count+int64(i))%offsetEvery == 0 {
221+ st.offsets = append(st.offsets, pos)
222+ }
223+ pos += int64(len(encodeLine(l)))
224+ }
225+ st.count += int64(len(fresh))
226+ st.size += int64(n)
227+ close(st.changed)
228+ st.changed = make(chan struct{})
229+ return st.count, len(fresh), nil
230+}
231+
232+// Count returns the number of lines held.
233+func (s *Store) Count(_ context.Context, ref ports.CILogRef) (int64, error) {
234+ if !validRef(ref) {
235+ return 0, nil
236+ }
237+ st := s.state(ref)
238+ st.mu.Lock()
239+ defer st.mu.Unlock()
240+ if err := s.load(ref, st); err != nil {
241+ return 0, err
242+ }
243+ return st.count, nil
244+}
245+
246+// Read returns lines from line number from.
247+func (s *Store) Read(_ context.Context, ref ports.CILogRef, from int64, maxLines, maxBytes int) ([]domain.CILogLine, error) {
248+ if !validRef(ref) || from < 0 {
249+ return nil, domain.ErrNotFound
250+ }
251+ st := s.state(ref)
252+ st.mu.Lock()
253+ if err := s.load(ref, st); err != nil {
254+ st.mu.Unlock()
255+ return nil, err
256+ }
257+ count, size := st.count, st.size
258+ var start int64
259+ var first int64
260+ if idx := from / offsetEvery; idx < int64(len(st.offsets)) {
261+ start, first = st.offsets[idx], idx*offsetEvery
262+ }
263+ st.mu.Unlock()
264+ if from > count {
265+ return nil, domain.ErrOutOfRange
266+ }
267+ if _, err := os.Stat(s.path(ref)); errors.Is(err, fs.ErrNotExist) {
268+ if from == 0 && count == 0 {
269+ return nil, domain.ErrNotFound
270+ }
271+ return nil, domain.ErrNotFound
272+ }
273+ if from == count {
274+ return nil, nil
275+ }
276+ f, err := os.Open(s.path(ref))
277+ if err != nil {
278+ return nil, fmt.Errorf("open log: %w", err)
279+ }
280+ defer func() { _ = f.Close() }()
281+ // Never read past the size seen under the lock: a concurrent append is
282+ // for the next call.
283+ r := bufio.NewReaderSize(io.NewSectionReader(f, start, size-start), 1<<16)
284+ var out []domain.CILogLine
285+ total := 0
286+ for n := first; n < count; n++ {
287+ raw, err := r.ReadBytes('\n')
288+ if err != nil {
289+ return nil, fmt.Errorf("read log: %w", err)
290+ }
291+ if n < from {
292+ continue
293+ }
294+ l, err := decodeLine(raw)
295+ if err != nil {
296+ return nil, fmt.Errorf("log line %d: %w", n, err)
297+ }
298+ if len(out) > 0 && (len(out) >= maxLines || total+len(l.Text) > maxBytes) {
299+ break
300+ }
301+ total += len(l.Text)
302+ out = append(out, l)
303+ }
304+ return out, nil
305+}
306+
307+// Changed returns a channel closed at the next append.
308+func (s *Store) Changed(ref ports.CILogRef) <-chan struct{} {
309+ st := s.state(ref)
310+ st.mu.Lock()
311+ defer st.mu.Unlock()
312+ return st.changed
313+}
314+
315+// Reset empties the log.
316+func (s *Store) Reset(_ context.Context, ref ports.CILogRef) error {
317+ if !validRef(ref) {
318+ return nil
319+ }
320+ st := s.state(ref)
321+ st.mu.Lock()
322+ defer st.mu.Unlock()
323+ if err := os.Remove(s.path(ref)); err != nil && !errors.Is(err, fs.ErrNotExist) {
324+ return fmt.Errorf("remove log: %w", err)
325+ }
326+ st.loaded, st.count, st.size, st.offsets = true, 0, 0, nil
327+ close(st.changed)
328+ st.changed = make(chan struct{})
329+ return nil
330+}
331+
332+// RemoveRuns deletes the logs of the runs.
333+func (s *Store) RemoveRuns(_ context.Context, runIDs []int64) error {
334+ var errs []error
335+ for _, id := range runIDs {
336+ if id < 1 {
337+ continue
338+ }
339+ if err := os.RemoveAll(filepath.Join(s.root, strconv.FormatInt(id, 10))); err != nil {
340+ errs = append(errs, err)
341+ }
342+ }
343+ s.mu.Lock()
344+ for ref := range s.logs {
345+ for _, id := range runIDs {
346+ if ref.RunID == id {
347+ delete(s.logs, ref)
348+ }
349+ }
350+ }
351+ s.mu.Unlock()
352+ return errors.Join(errs...)
353+}
354+
355+// RunIDs lists the runs that have a log directory.
356+func (s *Store) RunIDs(_ context.Context) ([]int64, error) {
357+ entries, err := os.ReadDir(s.root)
358+ if errors.Is(err, fs.ErrNotExist) {
359+ return nil, nil
360+ }
361+ if err != nil {
362+ return nil, fmt.Errorf("list logs: %w", err)
363+ }
364+ var ids []int64
365+ for _, e := range entries {
366+ if id, err := strconv.ParseInt(e.Name(), 10, 64); err == nil && e.IsDir() {
367+ ids = append(ids, id)
368+ }
369+ }
370+ return ids, nil
371+}
A internal/adapters/cilogs/cilogs_test.go +238 -0
@@ -0,0 +1,238 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package cilogs_test
4+
5+import (
6+ "context"
7+ "os"
8+ "path/filepath"
9+ "strings"
10+ "testing"
11+ "time"
12+
13+ "github.com/stretchr/testify/assert"
14+ "github.com/stretchr/testify/require"
15+
16+ "bnema.dev/ermite/internal/adapters/cilogs"
17+ "bnema.dev/ermite/internal/domain"
18+ "bnema.dev/ermite/internal/ports"
19+)
20+
21+var (
22+ ref0 = ports.CILogRef{RunID: 7, JobID: "7-0-0", Attempt: 1}
23+ t0 = time.Date(2026, 10, 12, 10, 0, 0, 123456789, time.UTC)
24+)
25+
26+func lines(texts ...string) []domain.CILogLine {
27+ out := make([]domain.CILogLine, len(texts))
28+ for i, s := range texts {
29+ out[i] = domain.CILogLine{Time: t0.Add(time.Duration(i) * time.Millisecond), Text: s}
30+ }
31+ return out
32+}
33+
34+const big = 1 << 30
35+
36+func TestAppendAndReadKeepTextTimeAndContinuation(t *testing.T) {
37+ ctx := context.Background()
38+ s := cilogs.New(t.TempDir())
39+ in := lines("##[group]Run echo hi", "hi", "##[endgroup]")
40+ in = append(in, domain.CILogLine{Time: t0, Text: "second half", Continued: true})
41+ next, added, err := s.Append(ctx, ref0, 0, in, big)
42+ require.NoError(t, err)
43+ assert.Equal(t, int64(4), next)
44+ assert.Equal(t, 4, added)
45+
46+ got, err := s.Read(ctx, ref0, 0, 100, 1<<20)
47+ require.NoError(t, err)
48+ require.Len(t, got, 4)
49+ assert.Equal(t, "##[group]Run echo hi", got[0].Text)
50+ assert.True(t, got[0].Time.Equal(t0), "nanosecond precision is kept: %v", got[0].Time)
51+ assert.True(t, got[3].Continued)
52+ assert.False(t, got[2].Continued)
53+
54+ tail, err := s.Read(ctx, ref0, 2, 100, 1<<20)
55+ require.NoError(t, err)
56+ assert.Equal(t, "##[endgroup]", tail[0].Text)
57+}
58+
59+func TestAppendIsIdempotentByOffset(t *testing.T) {
60+ ctx := context.Background()
61+ s := cilogs.New(t.TempDir())
62+ _, _, err := s.Append(ctx, ref0, 0, lines("a", "b", "c"), big)
63+ require.NoError(t, err)
64+
65+ // A retry of the same request changes nothing.
66+ next, added, err := s.Append(ctx, ref0, 0, lines("a", "b", "c"), big)
67+ require.NoError(t, err)
68+ assert.Equal(t, int64(3), next)
69+ assert.Zero(t, added)
70+
71+ // A request that overlaps appends only its new tail.
72+ next, added, err = s.Append(ctx, ref0, 1, lines("b", "c", "d"), big)
73+ require.NoError(t, err)
74+ assert.Equal(t, int64(4), next)
75+ assert.Equal(t, 1, added)
76+
77+ // A retry whose lines differ is silently dropped, as the contract says.
78+ next, added, err = s.Append(ctx, ref0, 0, lines("X", "Y"), big)
79+ require.NoError(t, err)
80+ assert.Equal(t, int64(4), next)
81+ assert.Zero(t, added)
82+ got, _ := s.Read(ctx, ref0, 0, 10, 1<<20)
83+ assert.Equal(t, "a", got[0].Text)
84+}
85+
86+func TestAppendAboveTheCountIsAGap(t *testing.T) {
87+ ctx := context.Background()
88+ s := cilogs.New(t.TempDir())
89+ _, _, err := s.Append(ctx, ref0, 0, lines("a"), big)
90+ require.NoError(t, err)
91+ _, _, err = s.Append(ctx, ref0, 5, lines("z"), big)
92+ require.ErrorIs(t, err, domain.ErrOutOfRange)
93+ var gap *ports.ErrCILogGap
94+ require.ErrorAs(t, err, &gap)
95+ assert.Equal(t, int64(1), gap.Count)
96+}
97+
98+func TestTheLimitRefusesTheWholeRequestUnlessForced(t *testing.T) {
99+ ctx := context.Background()
100+ s := cilogs.New(t.TempDir())
101+ _, _, err := s.Append(ctx, ref0, 0, lines("aaaa"), 200)
102+ require.NoError(t, err)
103+ _, _, err = s.Append(ctx, ref0, 1, lines(strings.Repeat("b", 300)), 200)
104+ require.ErrorIs(t, err, ports.ErrCILogLimit)
105+ n, _ := s.Count(ctx, ref0)
106+ assert.Equal(t, int64(1), n, "nothing of the refused request was appended")
107+
108+ next, err := s.AppendForced(ctx, ref0, lines("limit reached"))
109+ require.NoError(t, err)
110+ assert.Equal(t, int64(2), next)
111+}
112+
113+func TestReadBoundsLinesAndBytesAndReportsMissingLogs(t *testing.T) {
114+ ctx := context.Background()
115+ s := cilogs.New(t.TempDir())
116+ _, err := s.Read(ctx, ref0, 0, 10, 100)
117+ require.ErrorIs(t, err, domain.ErrNotFound)
118+
119+ _, _, err = s.Append(ctx, ref0, 0, lines("12345", "12345", "12345", "12345"), big)
120+ require.NoError(t, err)
121+ got, err := s.Read(ctx, ref0, 0, 3, 1<<20)
122+ require.NoError(t, err)
123+ assert.Len(t, got, 3)
124+ got, err = s.Read(ctx, ref0, 0, 100, 12)
125+ require.NoError(t, err)
126+ assert.Len(t, got, 2, "the text of a batch is bounded")
127+ got, err = s.Read(ctx, ref0, 4, 10, 100)
128+ require.NoError(t, err)
129+ assert.Empty(t, got, "reading at the end is empty, not an error")
130+ _, err = s.Read(ctx, ref0, 5, 10, 100)
131+ assert.ErrorIs(t, err, domain.ErrOutOfRange)
132+}
133+
134+func TestReadFindsLinesDeepInALargeLog(t *testing.T) {
135+ ctx := context.Background()
136+ s := cilogs.New(t.TempDir())
137+ var all []domain.CILogLine
138+ for i := range 3000 {
139+ all = append(all, domain.CILogLine{Time: t0, Text: "line " + itoa(i)})
140+ }
141+ for off := 0; off < len(all); off += 1000 {
142+ _, _, err := s.Append(ctx, ref0, int64(off), all[off:off+1000], big)
143+ require.NoError(t, err)
144+ }
145+ got, err := s.Read(ctx, ref0, 2500, 2, 1<<20)
146+ require.NoError(t, err)
147+ require.Len(t, got, 2)
148+ assert.Equal(t, "line 2500", got[0].Text)
149+ assert.Equal(t, "line 2501", got[1].Text)
150+}
151+
152+func itoa(n int) string {
153+ if n == 0 {
154+ return "0"
155+ }
156+ var b []byte
157+ for n > 0 {
158+ b = append([]byte{byte('0' + n%10)}, b...)
159+ n /= 10
160+ }
161+ return string(b)
162+}
163+
164+func TestCountSurvivesARestartAndAChangedChannelFiresOnAppend(t *testing.T) {
165+ ctx := context.Background()
166+ dir := t.TempDir()
167+ s := cilogs.New(dir)
168+ ch := s.Changed(ref0)
169+ _, _, err := s.Append(ctx, ref0, 0, lines("a", "b"), big)
170+ require.NoError(t, err)
171+ select {
172+ case <-ch:
173+ default:
174+ t.Fatal("Changed did not fire on append")
175+ }
176+
177+ s2 := cilogs.New(dir)
178+ n, err := s2.Count(ctx, ref0)
179+ require.NoError(t, err)
180+ assert.Equal(t, int64(2), n)
181+ _, _, err = s2.Append(ctx, ref0, 2, lines("c"), big)
182+ require.NoError(t, err)
183+ got, _ := s2.Read(ctx, ref0, 0, 10, 1<<20)
184+ assert.Len(t, got, 3)
185+}
186+
187+func TestATornLastLineIsCutOnLoad(t *testing.T) {
188+ ctx := context.Background()
189+ dir := t.TempDir()
190+ s := cilogs.New(dir)
191+ _, _, err := s.Append(ctx, ref0, 0, lines("a", "b"), big)
192+ require.NoError(t, err)
193+ path := filepath.Join(dir, "7", "7-0-0", "1.log")
194+ f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0)
195+ require.NoError(t, err)
196+ _, err = f.WriteString("- 2026-10-12T10:00:00.1234")
197+ require.NoError(t, err)
198+ require.NoError(t, f.Close())
199+
200+ s2 := cilogs.New(dir)
201+ n, err := s2.Count(ctx, ref0)
202+ require.NoError(t, err)
203+ assert.Equal(t, int64(2), n)
204+ _, _, err = s2.Append(ctx, ref0, 2, lines("c"), big)
205+ require.NoError(t, err)
206+ got, err := s2.Read(ctx, ref0, 0, 10, 1<<20)
207+ require.NoError(t, err)
208+ assert.Len(t, got, 3)
209+}
210+
211+func TestRemoveRunsAndResetAndRunIDs(t *testing.T) {
212+ ctx := context.Background()
213+ s := cilogs.New(t.TempDir())
214+ other := ports.CILogRef{RunID: 9, JobID: "9-0-0", Attempt: 1}
215+ _, _, _ = s.Append(ctx, ref0, 0, lines("a"), big)
216+ _, _, _ = s.Append(ctx, other, 0, lines("b"), big)
217+ ids, err := s.RunIDs(ctx)
218+ require.NoError(t, err)
219+ assert.ElementsMatch(t, []int64{7, 9}, ids)
220+
221+ require.NoError(t, s.Reset(ctx, ref0))
222+ n, _ := s.Count(ctx, ref0)
223+ assert.Zero(t, n)
224+
225+ require.NoError(t, s.RemoveRuns(ctx, []int64{9}))
226+ _, err = s.Read(ctx, other, 0, 10, 100)
227+ assert.ErrorIs(t, err, domain.ErrNotFound)
228+}
229+
230+func TestARefThatCouldEscapeTheDirectoryIsRefused(t *testing.T) {
231+ ctx := context.Background()
232+ dir := t.TempDir()
233+ s := cilogs.New(dir)
234+ _, _, err := s.Append(ctx, ports.CILogRef{RunID: 1, JobID: "../../etc", Attempt: 1}, 0, lines("x"), big)
235+ assert.ErrorIs(t, err, domain.ErrNotFound)
236+ _, _, err = s.Append(ctx, ports.CILogRef{RunID: 1, JobID: "1-0-0", Attempt: 0}, 0, lines("x"), big)
237+ assert.ErrorIs(t, err, domain.ErrNotFound)
238+}
A internal/adapters/kvstore/ci.go +1013 -0
@@ -0,0 +1,1013 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package kvstore
4+
5+import (
6+ "context"
7+ "errors"
8+ "fmt"
9+ "slices"
10+ "strconv"
11+ "strings"
12+ "time"
13+
14+ kv "bnema.dev/ermite-kv"
15+ "google.golang.org/protobuf/proto"
16+
17+ storagepb "bnema.dev/ermite/gen/ermite/storage"
18+ "bnema.dev/ermite/internal/domain"
19+ "bnema.dev/ermite/internal/ports"
20+)
21+
22+// Key layout of CI. Repository names hold no '/', so a prefix ending in "/"
23+// never matches another repository; ids are zero-padded so that keys sort
24+// numerically. Job records are keyed by run, attempt of the run and job id.
25+//
26+// run/<repo>/<id:020d> CIRun
27+// runsrc/<id:020d> CIRunSource
28+// job/<run:020d>/<attempt:010d>/<id> CIJob
29+// check/<repo>/<sha>/<job id> CICheck
30+// runseq Sequence: last run id
31+// runnum/<repo>/<workflow path> Sequence: last run number
32+// runner/<id>, runnername/<name>, runnertoken/<hash>
33+//
34+// Indexes, derived from the job and run records by every write:
35+//
36+// ciopen/<id:020d> run not completed (value: repo)
37+// ciready/<ready_at:020d>/<job key> job waiting for a runner
38+// ciactive/<job key> job assigned or running
39+// jobtoken/<hash> job token (value: job key)
40+const (
41+ runPrefix = "run/"
42+ runSrcPrefix = "runsrc/"
43+ jobPrefix = "job/"
44+ checkPrefix = "check/"
45+ runSeqKey = "runseq"
46+ runNumPrefix = "runnum/"
47+ runnerPrefix = "runner/"
48+ runnerName = "runnername/"
49+ runnerTokPref = "runnertoken/"
50+ openPrefix = "ciopen/"
51+ readyPrefix = "ciready/"
52+ activePrefix = "ciactive/"
53+ jobTokPrefix = "jobtoken/"
54+ idFormat = "%020d"
55+)
56+
57+// CI is the ermite-kv backed ports.CIStore. It shares the database of the
58+// Store it comes from.
59+type CI struct {
60+ db *kv.DB
61+}
62+
63+var _ ports.CIStore = (*CI)(nil)
64+
65+// CI returns the CI store over the same database. It is valid until the Store
66+// is closed.
67+func (s *Store) CI() *CI { return &CI{db: s.db} }
68+
69+func runKey(repo domain.RepoName, id int64) []byte {
70+ return []byte(runPrefix + repo.String() + "/" + fmt.Sprintf(idFormat, id))
71+}
72+func runsPrefix(repo domain.RepoName) string { return runPrefix + repo.String() + "/" }
73+func runSrcKey(id int64) []byte { return []byte(runSrcPrefix + fmt.Sprintf(idFormat, id)) }
74+func openKey(id int64) []byte { return []byte(openPrefix + fmt.Sprintf(idFormat, id)) }
75+func jobsPrefix(runID int64) string { return jobPrefix + fmt.Sprintf(idFormat, runID) + "/" }
76+func jobAttemptPrefix(runID int64, attempt int32) string {
77+ return jobsPrefix(runID) + fmt.Sprintf("%010d", attempt) + "/"
78+}
79+func jobRef(runID int64, attempt int32, id string) string {
80+ return jobAttemptPrefix(runID, attempt) + id
81+}
82+func jobKey(runID int64, attempt int32, id string) []byte { return []byte(jobRef(runID, attempt, id)) }
83+func checksPrefix(repo domain.RepoName, sha string) string {
84+ return checkPrefix + repo.String() + "/" + sha + "/"
85+}
86+func checkKey(repo domain.RepoName, sha, jobID string) []byte {
87+ return []byte(checksPrefix(repo, sha) + jobID)
88+}
89+func runNumKey(repo domain.RepoName, workflow string) []byte {
90+ return []byte(runNumPrefix + repo.String() + "/" + workflow)
91+}
92+func readyKey(at time.Time, ref string) []byte {
93+ return []byte(readyPrefix + fmt.Sprintf(idFormat, at.UnixNano()) + "/" + ref)
94+}
95+func activeKey(ref string) []byte { return []byte(activePrefix + ref) }
96+func jobTokenKey(h domain.TokenHash) []byte {
97+ return []byte(jobTokPrefix + h.String())
98+}
99+
100+// --- encoding ---
101+
102+func encodeStatus(s domain.CIStatus) storagepb.CIStatus {
103+ switch s {
104+ case domain.CIQueued:
105+ return storagepb.CIStatus_CI_STATUS_QUEUED
106+ case domain.CIInProgress:
107+ return storagepb.CIStatus_CI_STATUS_IN_PROGRESS
108+ case domain.CICompleted:
109+ return storagepb.CIStatus_CI_STATUS_COMPLETED
110+ }
111+ return storagepb.CIStatus_CI_STATUS_UNSPECIFIED
112+}
113+
114+func decodeStatus(s storagepb.CIStatus) domain.CIStatus {
115+ switch s {
116+ case storagepb.CIStatus_CI_STATUS_IN_PROGRESS:
117+ return domain.CIInProgress
118+ case storagepb.CIStatus_CI_STATUS_COMPLETED:
119+ return domain.CICompleted
120+ }
121+ return domain.CIQueued
122+}
123+
124+var conclusions = []struct {
125+ d domain.CIConclusion
126+ p storagepb.CIConclusion
127+}{
128+ {domain.CISuccess, storagepb.CIConclusion_CI_CONCLUSION_SUCCESS},
129+ {domain.CIFailure, storagepb.CIConclusion_CI_CONCLUSION_FAILURE},
130+ {domain.CICancelled, storagepb.CIConclusion_CI_CONCLUSION_CANCELLED},
131+ {domain.CISkipped, storagepb.CIConclusion_CI_CONCLUSION_SKIPPED},
132+ {domain.CITimedOut, storagepb.CIConclusion_CI_CONCLUSION_TIMED_OUT},
133+ {domain.CINeutral, storagepb.CIConclusion_CI_CONCLUSION_NEUTRAL},
134+ {domain.CIActionRequired, storagepb.CIConclusion_CI_CONCLUSION_ACTION_REQUIRED},
135+}
136+
137+func encodeConclusion(c domain.CIConclusion) storagepb.CIConclusion {
138+ for _, e := range conclusions {
139+ if e.d == c {
140+ return e.p
141+ }
142+ }
143+ return storagepb.CIConclusion_CI_CONCLUSION_UNSPECIFIED
144+}
145+
146+func decodeConclusion(c storagepb.CIConclusion) domain.CIConclusion {
147+ for _, e := range conclusions {
148+ if e.p == c {
149+ return e.d
150+ }
151+ }
152+ return domain.CINoConclusion
153+}
154+
155+var phases = []struct {
156+ d domain.CIJobPhase
157+ p storagepb.CIJobPhase
158+}{
159+ {domain.CIPhaseWaiting, storagepb.CIJobPhase_CI_JOB_PHASE_WAITING},
160+ {domain.CIPhaseReady, storagepb.CIJobPhase_CI_JOB_PHASE_READY},
161+ {domain.CIPhaseAssigned, storagepb.CIJobPhase_CI_JOB_PHASE_ASSIGNED},
162+ {domain.CIPhaseRunning, storagepb.CIJobPhase_CI_JOB_PHASE_RUNNING},
163+ {domain.CIPhaseEnded, storagepb.CIJobPhase_CI_JOB_PHASE_ENDED},
164+}
165+
166+func encodePhase(p domain.CIJobPhase) (storagepb.CIJobPhase, error) {
167+ for _, e := range phases {
168+ if e.d == p {
169+ return e.p, nil
170+ }
171+ }
172+ return 0, fmt.Errorf("invalid job phase %q", p)
173+}
174+
175+func decodePhase(p storagepb.CIJobPhase) domain.CIJobPhase {
176+ for _, e := range phases {
177+ if e.p == p {
178+ return e.d
179+ }
180+ }
181+ return domain.CIPhaseWaiting
182+}
183+
184+var isolations = []struct {
185+ d domain.CIIsolation
186+ p storagepb.CIIsolation
187+}{
188+ {domain.CIIsolationVM, storagepb.CIIsolation_CI_ISOLATION_VM},
189+ {domain.CIIsolationContainerSysbox, storagepb.CIIsolation_CI_ISOLATION_CONTAINER_SYSBOX},
190+ {domain.CIIsolationContainerPrivileged, storagepb.CIIsolation_CI_ISOLATION_CONTAINER_PRIVILEGED},
191+}
192+
193+func encodeIsolation(i domain.CIIsolation) storagepb.CIIsolation {
194+ for _, e := range isolations {
195+ if e.d == i {
196+ return e.p
197+ }
198+ }
199+ return storagepb.CIIsolation_CI_ISOLATION_UNSPECIFIED
200+}
201+
202+func decodeIsolation(i storagepb.CIIsolation) domain.CIIsolation {
203+ for _, e := range isolations {
204+ if e.p == i {
205+ return e.d
206+ }
207+ }
208+ return domain.CIIsolationNone
209+}
210+
211+var stepStates = []struct {
212+ d domain.CIStepState
213+ p storagepb.CIStepState
214+}{
215+ {domain.CIStepPending, storagepb.CIStepState_CI_STEP_STATE_PENDING},
216+ {domain.CIStepInProgress, storagepb.CIStepState_CI_STEP_STATE_IN_PROGRESS},
217+ {domain.CIStepCompleted, storagepb.CIStepState_CI_STEP_STATE_COMPLETED},
218+}
219+
220+func encodeStepState(s domain.CIStepState) storagepb.CIStepState {
221+ for _, e := range stepStates {
222+ if e.d == s {
223+ return e.p
224+ }
225+ }
226+ return storagepb.CIStepState_CI_STEP_STATE_UNSPECIFIED
227+}
228+
229+func decodeStepState(s storagepb.CIStepState) domain.CIStepState {
230+ for _, e := range stepStates {
231+ if e.p == s {
232+ return e.d
233+ }
234+ }
235+ return domain.CIStepPending
236+}
237+
238+var cancelReasons = []domain.CICancelReason{domain.CICancelNone, domain.CICancelUser, domain.CICancelFailFast, domain.CICancelTimeout}
239+
240+func encodeRun(r domain.CIRun) ([]byte, error) {
241+ if r.Repo.IsZero() {
242+ return nil, fmt.Errorf("%w: empty name", domain.ErrInvalidName)
243+ }
244+ return mustMarshal(&storagepb.CIRun{
245+ Repo: r.Repo.String(), Id: r.ID, Number: r.Number, Attempt: r.Attempt,
246+ WorkflowPath: r.WorkflowPath, WorkflowName: r.WorkflowName, Title: r.Title,
247+ Event: r.Event, Ref: r.Ref, Sha: r.Sha, Actor: r.Actor,
248+ Status: encodeStatus(r.Status), Conclusion: encodeConclusion(r.Conclusion), Error: r.Error,
249+ CreatedAt: encodeTime(r.CreatedAt), StartedAt: encodeTime(r.StartedAt), CompletedAt: encodeTime(r.CompletedAt),
250+ CancelRequested: r.CancelRequested, CancelRequestedAt: encodeTime(r.CancelRequestedAt),
251+ }), nil
252+}
253+
254+func decodeRun(item kv.Item) (domain.CIRun, error) {
255+ var pb storagepb.CIRun
256+ if err := proto.Unmarshal(item.Value, &pb); err != nil {
257+ return domain.CIRun{}, fmt.Errorf("decode run: %w", err)
258+ }
259+ repo, err := domain.ParseRepoName(pb.GetRepo())
260+ if err != nil {
261+ return domain.CIRun{}, fmt.Errorf("decode run: %w", err)
262+ }
263+ return domain.CIRun{
264+ Repo: repo, ID: pb.GetId(), Number: pb.GetNumber(), Attempt: pb.GetAttempt(),
265+ WorkflowPath: pb.GetWorkflowPath(), WorkflowName: pb.GetWorkflowName(), Title: pb.GetTitle(),
266+ Event: pb.GetEvent(), Ref: pb.GetRef(), Sha: pb.GetSha(), Actor: pb.GetActor(),
267+ Status: decodeStatus(pb.GetStatus()), Conclusion: decodeConclusion(pb.GetConclusion()), Error: pb.GetError(),
268+ CreatedAt: decodeTime(pb.GetCreatedAt()), StartedAt: decodeTime(pb.GetStartedAt()), CompletedAt: decodeTime(pb.GetCompletedAt()),
269+ CancelRequested: pb.GetCancelRequested(), CancelRequestedAt: decodeTime(pb.GetCancelRequestedAt()),
270+ Version: item.Version,
271+ }, nil
272+}
273+
274+func encodeJob(j domain.CIJob) ([]byte, error) {
275+ if j.Repo.IsZero() {
276+ return nil, fmt.Errorf("%w: empty name", domain.ErrInvalidName)
277+ }
278+ phase, err := encodePhase(j.Phase)
279+ if err != nil {
280+ return nil, err
281+ }
282+ steps := make([]*storagepb.CIStep, 0, len(j.Steps))
283+ for _, s := range j.Steps {
284+ steps = append(steps, &storagepb.CIStep{
285+ Number: s.Number, Name: s.Name, State: encodeStepState(s.State), Conclusion: encodeConclusion(s.Conclusion),
286+ StartedAt: encodeTime(s.StartedAt), CompletedAt: encodeTime(s.CompletedAt),
287+ LogStart: s.LogStart, HasLines: s.HasLines,
288+ })
289+ }
290+ reason := int32(slices.Index(cancelReasons, j.CancelReason))
291+ pb := &storagepb.CIJob{
292+ Id: j.ID, RunId: j.RunID, Attempt: j.Attempt, RunAttempt: j.RunAttempt, Repo: j.Repo.String(),
293+ Key: j.Key, Name: j.Name, Phase: phase, Conclusion: encodeConclusion(j.Conclusion), Error: j.Error,
294+ Labels: j.Labels, Needs: j.Needs, RunnerId: j.RunnerID, RunnerName: j.RunnerName,
295+ Isolation: encodeIsolation(j.Isolation), Steps: steps,
296+ CreatedAt: encodeTime(j.CreatedAt), StartedAt: encodeTime(j.StartedAt), CompletedAt: encodeTime(j.CompletedAt),
297+ Index: int32(j.Index), Combo: int32(j.Combo), MatrixJson: j.MatrixJSON, ComboTotal: int32(j.ComboTotal), //nolint:gosec // bounded by the 256 job limit
298+ TimeoutMinutes: j.TimeoutMinutes, FailFast: j.FailFast, MaxParallel: int32(j.MaxParallel), //nolint:gosec // validated at planning
299+ Outputs: j.Outputs, ReadyAt: encodeTime(j.ReadyAt), AssignedAt: encodeTime(j.AssignedAt),
300+ AckedAt: encodeTime(j.AckedAt), AssignCancels: int32(j.AssignCancels), //nolint:gosec // at most 3
301+ TokenValidUntil: encodeTime(j.TokenValidUntil), CancelReason: max(reason, 0), CancelMessage: j.CancelMessage,
302+ CancelRequestedAt: encodeTime(j.CancelRequestedAt), Planned: j.Planned,
303+ }
304+ if j.HasToken() {
305+ pb.TokenHash = j.TokenHash[:]
306+ }
307+ return mustMarshal(pb), nil
308+}
309+
310+//hexcheck:ignore suspicious-business-logic-in-adapter maps the stored record to the domain job field by field: no rule is decided here
311+func decodeJob(item kv.Item) (domain.CIJob, error) {
312+ var pb storagepb.CIJob
313+ if err := proto.Unmarshal(item.Value, &pb); err != nil {
314+ return domain.CIJob{}, fmt.Errorf("decode job: %w", err)
315+ }
316+ repo, err := domain.ParseRepoName(pb.GetRepo())
317+ if err != nil {
318+ return domain.CIJob{}, fmt.Errorf("decode job: %w", err)
319+ }
320+ j := domain.CIJob{
321+ ID: pb.GetId(), RunID: pb.GetRunId(), Attempt: pb.GetAttempt(), RunAttempt: pb.GetRunAttempt(), Repo: repo,
322+ Key: pb.GetKey(), Name: pb.GetName(), Phase: decodePhase(pb.GetPhase()), Conclusion: decodeConclusion(pb.GetConclusion()),
323+ Error: pb.GetError(), Labels: pb.GetLabels(), Needs: pb.GetNeeds(), RunnerID: pb.GetRunnerId(), RunnerName: pb.GetRunnerName(),
324+ Isolation: decodeIsolation(pb.GetIsolation()),
325+ CreatedAt: decodeTime(pb.GetCreatedAt()), StartedAt: decodeTime(pb.GetStartedAt()), CompletedAt: decodeTime(pb.GetCompletedAt()),
326+ Index: int(pb.GetIndex()), Combo: int(pb.GetCombo()), Planned: pb.GetPlanned(), MatrixJSON: pb.GetMatrixJson(),
327+ ComboTotal: int(pb.GetComboTotal()), TimeoutMinutes: pb.GetTimeoutMinutes(), FailFast: pb.GetFailFast(),
328+ MaxParallel: int(pb.GetMaxParallel()), Outputs: pb.GetOutputs(),
329+ TokenValidUntil: decodeTime(pb.GetTokenValidUntil()), ReadyAt: decodeTime(pb.GetReadyAt()),
330+ AssignedAt: decodeTime(pb.GetAssignedAt()), AckedAt: decodeTime(pb.GetAckedAt()), AssignCancels: int(pb.GetAssignCancels()),
331+ CancelMessage: pb.GetCancelMessage(), CancelRequestedAt: decodeTime(pb.GetCancelRequestedAt()),
332+ Version: item.Version,
333+ }
334+ if r := int(pb.GetCancelReason()); r >= 0 && r < len(cancelReasons) {
335+ j.CancelReason = cancelReasons[r]
336+ }
337+ copy(j.TokenHash[:], pb.GetTokenHash())
338+ for _, s := range pb.GetSteps() {
339+ j.Steps = append(j.Steps, domain.CIStep{
340+ Number: s.GetNumber(), Name: s.GetName(), State: decodeStepState(s.GetState()), Conclusion: decodeConclusion(s.GetConclusion()),
341+ StartedAt: decodeTime(s.GetStartedAt()), CompletedAt: decodeTime(s.GetCompletedAt()),
342+ LogStart: s.GetLogStart(), HasLines: s.GetHasLines(),
343+ })
344+ }
345+ return j, nil
346+}
347+
348+func encodeCheck(c domain.CICheck) []byte {
349+ return mustMarshal(&storagepb.CICheck{
350+ Id: c.ID, Repo: c.Repo.String(), Sha: c.Sha, Name: c.Name, Status: encodeStatus(c.Status),
351+ Conclusion: encodeConclusion(c.Conclusion), RunId: c.RunID, JobId: c.JobID, Attempt: c.Attempt,
352+ WorkflowName: c.WorkflowName, CreatedAt: encodeTime(c.CreatedAt), StartedAt: encodeTime(c.StartedAt),
353+ CompletedAt: encodeTime(c.CompletedAt),
354+ })
355+}
356+
357+func decodeCheck(item kv.Item) (domain.CICheck, error) {
358+ var pb storagepb.CICheck
359+ if err := proto.Unmarshal(item.Value, &pb); err != nil {
360+ return domain.CICheck{}, fmt.Errorf("decode check: %w", err)
361+ }
362+ repo, err := domain.ParseRepoName(pb.GetRepo())
363+ if err != nil {
364+ return domain.CICheck{}, fmt.Errorf("decode check: %w", err)
365+ }
366+ return domain.CICheck{
367+ ID: pb.GetId(), Repo: repo, Sha: pb.GetSha(), Name: pb.GetName(), Status: decodeStatus(pb.GetStatus()),
368+ Conclusion: decodeConclusion(pb.GetConclusion()), RunID: pb.GetRunId(), JobID: pb.GetJobId(), Attempt: pb.GetAttempt(),
369+ WorkflowName: pb.GetWorkflowName(), CreatedAt: decodeTime(pb.GetCreatedAt()), StartedAt: decodeTime(pb.GetStartedAt()),
370+ CompletedAt: decodeTime(pb.GetCompletedAt()), Version: item.Version,
371+ }, nil
372+}
373+
374+func encodeRunner(r domain.CIRunner) []byte {
375+ return mustMarshal(&storagepb.CIRunner{
376+ Id: r.ID, Name: r.Name, Labels: r.Labels, Isolation: encodeIsolation(r.Isolation), Version: r.Version,
377+ Arch: r.Arch, Os: r.OS, RegisteredAt: encodeTime(r.RegisteredAt), TokenHash: r.TokenHash[:],
378+ })
379+}
380+
381+func decodeRunner(val []byte) (domain.CIRunner, error) {
382+ var pb storagepb.CIRunner
383+ if err := proto.Unmarshal(val, &pb); err != nil {
384+ return domain.CIRunner{}, fmt.Errorf("decode runner: %w", err)
385+ }
386+ r := domain.CIRunner{
387+ ID: pb.GetId(), Name: pb.GetName(), Labels: pb.GetLabels(), Isolation: decodeIsolation(pb.GetIsolation()),
388+ Version: pb.GetVersion(), Arch: pb.GetArch(), OS: pb.GetOs(), RegisteredAt: decodeTime(pb.GetRegisteredAt()),
389+ }
390+ copy(r.TokenHash[:], pb.GetTokenHash())
391+ return r, nil
392+}
393+
394+// --- writes ---
395+
396+// stage adds the preconditions and writes of one record to the batch.
397+func stage(b *kv.Batch, key, val []byte, version uint64) {
398+ if version == 0 {
399+ b.RequireAbsent(key)
400+ } else {
401+ b.RequireVersion(key, version)
402+ }
403+ b.Put(key, val)
404+}
405+
406+// runIndexes stages the open-run index of the run.
407+func runIndexes(b *kv.Batch, r domain.CIRun) {
408+ if r.Status == domain.CICompleted {
409+ b.Delete(openKey(r.ID))
410+ return
411+ }
412+ b.Put(openKey(r.ID), []byte(r.Repo.String()))
413+}
414+
415+// jobIndexes stages the ready, active and token indexes of the job. old is
416+// the job as stored before, if any: its token index goes when the token
417+// changed.
418+func (c *CI) jobIndexes(b *kv.Batch, j domain.CIJob, old *domain.CIJob) {
419+ ref := jobRef(j.RunID, j.RunAttempt, j.ID)
420+ if j.Phase == domain.CIPhaseReady {
421+ b.Put(readyKey(j.ReadyAt, ref), nil)
422+ } else {
423+ b.Delete(readyKey(j.ReadyAt, ref))
424+ }
425+ if old != nil && old.Phase == domain.CIPhaseReady && (j.Phase != domain.CIPhaseReady || !old.ReadyAt.Equal(j.ReadyAt)) {
426+ b.Delete(readyKey(old.ReadyAt, ref))
427+ }
428+ if j.Active() {
429+ b.Put(activeKey(ref), nil)
430+ } else {
431+ b.Delete(activeKey(ref))
432+ }
433+ if old != nil && old.HasToken() && old.TokenHash != j.TokenHash {
434+ b.Delete(jobTokenKey(old.TokenHash))
435+ }
436+ if j.HasToken() {
437+ b.Put(jobTokenKey(j.TokenHash), []byte(ref))
438+ }
439+}
440+
441+// guardRepo makes the batch depend on the repository, like
442+// MergeRequests.guardRepo, for the writes that create keys under it.
443+func (c *CI) guardRepo(ctx context.Context, b *kv.Batch, repo domain.RepoName) error {
444+ item, ok := c.db.Get(ctx, repoKey(repo))
445+ if !ok {
446+ return domain.ErrNotFound
447+ }
448+ b.RequireVersion(repoKey(repo), item.Version)
449+ b.Put(repoKey(repo), item.Value)
450+ return nil
451+}
452+
453+// stageRecords stages the writes of w. guarded collects the repositories that
454+// need guarding.
455+func (c *CI) stageRecords(ctx context.Context, b *kv.Batch, w ports.CIWrite) (guarded map[domain.RepoName]bool, err error) {
456+ guarded = map[domain.RepoName]bool{}
457+ for _, r := range w.Runs {
458+ val, err := encodeRun(r)
459+ if err != nil {
460+ return nil, err
461+ }
462+ stage(b, runKey(r.Repo, r.ID), val, r.Version)
463+ runIndexes(b, r)
464+ if r.Version == 0 {
465+ guarded[r.Repo] = true
466+ }
467+ }
468+ for _, j := range w.Jobs {
469+ val, err := encodeJob(j)
470+ if err != nil {
471+ return nil, err
472+ }
473+ key := jobKey(j.RunID, j.RunAttempt, j.ID)
474+ var old *domain.CIJob
475+ if j.Version != 0 {
476+ item, ok := c.db.Get(ctx, key)
477+ if !ok {
478+ return nil, domain.ErrConflict
479+ }
480+ if o, err := decodeJob(item); err == nil {
481+ old = &o
482+ }
483+ }
484+ stage(b, key, val, j.Version)
485+ c.jobIndexes(b, j, old)
486+ if j.Version == 0 {
487+ guarded[j.Repo] = true
488+ }
489+ }
490+ for _, ck := range w.Checks {
491+ stage(b, checkKey(ck.Repo, ck.Sha, ck.JobID), encodeCheck(ck), ck.Version)
492+ if ck.Version == 0 {
493+ guarded[ck.Repo] = true
494+ }
495+ }
496+ return guarded, nil
497+}
498+
499+// Apply writes the records in one batch.
500+//
501+//hexcheck:ignore suspicious-business-logic-in-adapter stages records and their derived indexes under version preconditions: a storage concern, no policy
502+func (c *CI) Apply(ctx context.Context, w ports.CIWrite) (uint64, error) {
503+ var b kv.Batch
504+ guarded, err := c.stageRecords(ctx, &b, w)
505+ if err != nil {
506+ return 0, err
507+ }
508+ for repo := range guarded {
509+ if err := c.guardRepo(ctx, &b, repo); err != nil {
510+ return 0, err
511+ }
512+ }
513+ v, err := c.db.Apply(ctx, &b)
514+ if errors.Is(err, kv.ErrConflict) {
515+ return 0, domain.ErrConflict
516+ }
517+ if err != nil {
518+ return 0, fmt.Errorf("write CI records: %w", err)
519+ }
520+ return v, nil
521+}
522+
523+func (c *CI) readSeq(ctx context.Context, key []byte) (last int64, item kv.Item, ok bool, err error) {
524+ item, ok = c.db.Get(ctx, key)
525+ if !ok {
526+ return 0, kv.Item{}, false, nil
527+ }
528+ var seq storagepb.Sequence
529+ if err := proto.Unmarshal(item.Value, &seq); err != nil {
530+ return 0, kv.Item{}, false, fmt.Errorf("decode sequence %s: %w", key, err)
531+ }
532+ return seq.GetLast(), item, true, nil
533+}
534+
535+func stageSeq(b *kv.Batch, key []byte, item kv.Item, ok bool, last int64) {
536+ if ok {
537+ b.RequireVersion(key, item.Version)
538+ } else {
539+ b.RequireAbsent(key)
540+ }
541+ b.Put(key, mustMarshal(&storagepb.Sequence{Last: last}))
542+}
543+
544+const createRunTries = 20
545+
546+// CreateRun takes the next run id and number and stores the draft.
547+//
548+//hexcheck:ignore suspicious-business-logic-in-adapter takes the next numbers under version preconditions: a storage concern, no policy
549+func (c *CI) CreateRun(ctx context.Context, repo domain.RepoName, workflowPath string, build func(id, number int64) ports.CIRunDraft) (ports.CIRunDraft, error) {
550+ for range createRunTries {
551+ lastID, idItem, idOK, err := c.readSeq(ctx, []byte(runSeqKey))
552+ if err != nil {
553+ return ports.CIRunDraft{}, err
554+ }
555+ numKey := runNumKey(repo, workflowPath)
556+ lastNum, numItem, numOK, err := c.readSeq(ctx, numKey)
557+ if err != nil {
558+ return ports.CIRunDraft{}, err
559+ }
560+ draft := build(lastID+1, lastNum+1)
561+ var b kv.Batch
562+ stageSeq(&b, []byte(runSeqKey), idItem, idOK, lastID+1)
563+ stageSeq(&b, numKey, numItem, numOK, lastNum+1)
564+ w := ports.CIWrite{Runs: []domain.CIRun{draft.Run}, Jobs: draft.Jobs, Checks: draft.Checks}
565+ guarded, err := c.stageRecords(ctx, &b, w)
566+ if err != nil {
567+ return ports.CIRunDraft{}, err
568+ }
569+ src := mustMarshal(&storagepb.CIRunSource{
570+ RunId: draft.Run.ID, Repo: repo.String(), WorkflowYaml: draft.Source.WorkflowYAML, GithubJson: draft.Source.GithubJSON,
571+ })
572+ b.RequireAbsent(runSrcKey(draft.Run.ID))
573+ b.Put(runSrcKey(draft.Run.ID), src)
574+ for r := range guarded {
575+ if err := c.guardRepo(ctx, &b, r); err != nil {
576+ return ports.CIRunDraft{}, err
577+ }
578+ }
579+ v, err := c.db.Apply(ctx, &b)
580+ if errors.Is(err, kv.ErrConflict) {
581+ continue
582+ }
583+ if err != nil {
584+ return ports.CIRunDraft{}, fmt.Errorf("create run of %q: %w", repo, err)
585+ }
586+ draft.Run.Version = v
587+ for i := range draft.Jobs {
588+ draft.Jobs[i].Version = v
589+ }
590+ for i := range draft.Checks {
591+ draft.Checks[i].Version = v
592+ }
593+ return draft, nil
594+ }
595+ return ports.CIRunDraft{}, fmt.Errorf("create run of %q: %w", repo, domain.ErrConflict)
596+}
597+
598+// --- reads ---
599+
600+// GetRun reads run/<repo>/<id>.
601+func (c *CI) GetRun(ctx context.Context, repo domain.RepoName, id int64) (domain.CIRun, error) {
602+ item, ok := c.db.Get(ctx, runKey(repo, id))
603+ if !ok || id < 1 {
604+ return domain.CIRun{}, domain.ErrNotFound
605+ }
606+ return decodeRun(item)
607+}
608+
609+// GetRunSource reads runsrc/<id>.
610+func (c *CI) GetRunSource(ctx context.Context, runID int64) (domain.CIRunSource, error) {
611+ item, ok := c.db.Get(ctx, runSrcKey(runID))
612+ if !ok || runID < 1 {
613+ return domain.CIRunSource{}, domain.ErrNotFound
614+ }
615+ var pb storagepb.CIRunSource
616+ if err := proto.Unmarshal(item.Value, &pb); err != nil {
617+ return domain.CIRunSource{}, fmt.Errorf("decode run source: %w", err)
618+ }
619+ repo, err := domain.ParseRepoName(pb.GetRepo())
620+ if err != nil {
621+ return domain.CIRunSource{}, fmt.Errorf("decode run source: %w", err)
622+ }
623+ return domain.CIRunSource{RunID: pb.GetRunId(), Repo: repo, WorkflowYAML: pb.GetWorkflowYaml(), GithubJSON: pb.GetGithubJson()}, nil
624+}
625+
626+// ListRuns returns the runs of the repository, newest first.
627+//
628+//hexcheck:ignore suspicious-business-logic-in-adapter port method name only: reads a key range and reverses it
629+func (c *CI) ListRuns(ctx context.Context, repo domain.RepoName, before int64, limit int) ([]domain.CIRun, error) {
630+ start := []byte(runsPrefix(repo))
631+ end := []byte(runPrefix + repo.String() + "0") // '/' + 1
632+ if before > 0 {
633+ end = runKey(repo, before)
634+ }
635+ // Collect then reverse: the snapshot iterates in key order only.
636+ var all []domain.CIRun
637+ for key, item := range c.db.Snapshot(ctx).Range(ctx, start, end) {
638+ r, err := decodeRun(item)
639+ if err != nil {
640+ return nil, fmt.Errorf("decode %s: %w", key, err)
641+ }
642+ all = append(all, r)
643+ }
644+ slices.Reverse(all)
645+ if limit > 0 && len(all) > limit {
646+ all = all[:limit]
647+ }
648+ return all, nil
649+}
650+
651+// RunIDs returns the ids of the runs of the repository.
652+func (c *CI) RunIDs(ctx context.Context, repo domain.RepoName) ([]int64, error) {
653+ var ids []int64
654+ prefix := runsPrefix(repo)
655+ for key := range c.db.Snapshot(ctx).Prefix(ctx, []byte(prefix)) {
656+ id, err := strconv.ParseInt(strings.TrimPrefix(string(key), prefix), 10, 64)
657+ if err != nil {
658+ return nil, fmt.Errorf("decode %s: %w", key, err)
659+ }
660+ ids = append(ids, id)
661+ }
662+ return ids, nil
663+}
664+
665+// ListOpenRuns returns the runs that have not completed.
666+func (c *CI) ListOpenRuns(ctx context.Context) ([]domain.CIRun, error) {
667+ snap := c.db.Snapshot(ctx)
668+ var out []domain.CIRun
669+ for key, item := range snap.Prefix(ctx, []byte(openPrefix)) {
670+ id, err := strconv.ParseInt(strings.TrimPrefix(string(key), openPrefix), 10, 64)
671+ if err != nil {
672+ return nil, fmt.Errorf("decode %s: %w", key, err)
673+ }
674+ repo, err := domain.ParseRepoName(string(item.Value))
675+ if err != nil {
676+ return nil, err
677+ }
678+ it, ok := snap.Get(ctx, runKey(repo, id))
679+ if !ok {
680+ continue
681+ }
682+ r, err := decodeRun(it)
683+ if err != nil {
684+ return nil, err
685+ }
686+ out = append(out, r)
687+ }
688+ return out, nil
689+}
690+
691+// GetJob reads one job record.
692+func (c *CI) GetJob(ctx context.Context, runID int64, runAttempt int32, jobID string) (domain.CIJob, error) {
693+ item, ok := c.db.Get(ctx, jobKey(runID, runAttempt, jobID))
694+ if !ok {
695+ return domain.CIJob{}, domain.ErrNotFound
696+ }
697+ return decodeJob(item)
698+}
699+
700+// ListJobs returns the jobs of a run at an attempt, ordered by index then
701+// combination.
702+func (c *CI) ListJobs(ctx context.Context, runID int64, runAttempt int32) ([]domain.CIJob, error) {
703+ var out []domain.CIJob
704+ for key, item := range c.db.Snapshot(ctx).Prefix(ctx, []byte(jobAttemptPrefix(runID, runAttempt))) {
705+ j, err := decodeJob(item)
706+ if err != nil {
707+ return nil, fmt.Errorf("decode %s: %w", key, err)
708+ }
709+ out = append(out, j)
710+ }
711+ slices.SortFunc(out, func(a, b domain.CIJob) int {
712+ if a.Index != b.Index {
713+ return a.Index - b.Index
714+ }
715+ return a.Combo - b.Combo
716+ })
717+ return out, nil
718+}
719+
720+func (c *CI) jobByRef(snap *kv.Snapshot, ctx context.Context, ref string) (domain.CIJob, error) {
721+ item, ok := snap.Get(ctx, []byte(ref))
722+ if !ok {
723+ return domain.CIJob{}, domain.ErrNotFound
724+ }
725+ return decodeJob(item)
726+}
727+
728+// FindJobByToken resolves the token index, then reads the job.
729+func (c *CI) FindJobByToken(ctx context.Context, hash domain.TokenHash, now time.Time) (domain.CIJob, error) {
730+ snap := c.db.Snapshot(ctx)
731+ idx, ok := snap.Get(ctx, jobTokenKey(hash))
732+ if !ok {
733+ return domain.CIJob{}, domain.ErrNotFound
734+ }
735+ j, err := c.jobByRef(snap, ctx, string(idx.Value))
736+ if err != nil {
737+ return domain.CIJob{}, err
738+ }
739+ if j.TokenHash != hash || (!j.TokenValidUntil.IsZero() && !now.Before(j.TokenValidUntil)) {
740+ return domain.CIJob{}, domain.ErrNotFound
741+ }
742+ return j, nil
743+}
744+
745+// ListReadyJobs returns the jobs waiting for a runner, oldest first.
746+func (c *CI) ListReadyJobs(ctx context.Context, limit int) ([]domain.CIJob, error) {
747+ snap := c.db.Snapshot(ctx)
748+ var out []domain.CIJob
749+ for key := range snap.Prefix(ctx, []byte(readyPrefix)) {
750+ ref := string(key)[len(readyPrefix)+len(fmt.Sprintf(idFormat, 0))+1:]
751+ j, err := c.jobByRef(snap, ctx, ref)
752+ if errors.Is(err, domain.ErrNotFound) {
753+ continue
754+ }
755+ if err != nil {
756+ return nil, err
757+ }
758+ out = append(out, j)
759+ if limit > 0 && len(out) >= limit {
760+ break
761+ }
762+ }
763+ return out, nil
764+}
765+
766+// ListActiveJobs returns the jobs given to a runner and not over.
767+func (c *CI) ListActiveJobs(ctx context.Context) ([]domain.CIJob, error) {
768+ snap := c.db.Snapshot(ctx)
769+ var out []domain.CIJob
770+ for key := range snap.Prefix(ctx, []byte(activePrefix)) {
771+ j, err := c.jobByRef(snap, ctx, strings.TrimPrefix(string(key), activePrefix))
772+ if errors.Is(err, domain.ErrNotFound) {
773+ continue
774+ }
775+ if err != nil {
776+ return nil, err
777+ }
778+ out = append(out, j)
779+ }
780+ return out, nil
781+}
782+
783+// ListChecks returns the checks of a commit ordered by creation time then
784+// name.
785+func (c *CI) ListChecks(ctx context.Context, repo domain.RepoName, sha string, limit int) ([]domain.CICheck, bool, error) {
786+ var out []domain.CICheck
787+ for key, item := range c.db.Snapshot(ctx).Prefix(ctx, []byte(checksPrefix(repo, sha))) {
788+ ck, err := decodeCheck(item)
789+ if err != nil {
790+ return nil, false, fmt.Errorf("decode %s: %w", key, err)
791+ }
792+ out = append(out, ck)
793+ }
794+ slices.SortStableFunc(out, func(a, b domain.CICheck) int {
795+ if d := a.CreatedAt.Compare(b.CreatedAt); d != 0 {
796+ return d
797+ }
798+ return strings.Compare(a.Name, b.Name)
799+ })
800+ more := limit > 0 && len(out) > limit
801+ if more {
802+ out = out[:limit]
803+ }
804+ return out, more, nil
805+}
806+
807+// --- retention and deletion ---
808+
809+// runKeys lists every key of a run except its record: source, jobs, indexes.
810+func (c *CI) runKeys(ctx context.Context, snap *kv.Snapshot, repo domain.RepoName, runID int64) [][]byte {
811+ keys := [][]byte{runKey(repo, runID), runSrcKey(runID), openKey(runID)}
812+ for key, item := range snap.Prefix(ctx, []byte(jobsPrefix(runID))) {
813+ keys = append(keys, slices.Clone(key))
814+ if j, err := decodeJob(item); err == nil {
815+ ref := jobRef(j.RunID, j.RunAttempt, j.ID)
816+ keys = append(keys, readyKey(j.ReadyAt, ref), activeKey(ref))
817+ if j.HasToken() {
818+ keys = append(keys, jobTokenKey(j.TokenHash))
819+ }
820+ }
821+ }
822+ return keys
823+}
824+
825+// ciKeys lists every key of the snapshot that belongs to the repository's CI
826+// runs, checks and numbers.
827+func ciKeys(ctx context.Context, snap *kv.Snapshot, repo domain.RepoName) [][]byte {
828+ c := &CI{}
829+ var keys [][]byte
830+ for key, item := range snap.Prefix(ctx, []byte(runsPrefix(repo))) {
831+ if r, err := decodeRun(item); err == nil {
832+ keys = append(keys, c.runKeys(ctx, snap, repo, r.ID)...)
833+ }
834+ keys = append(keys, slices.Clone(key))
835+ }
836+ for _, p := range []string{checkPrefix + repo.String() + "/", runNumPrefix + repo.String() + "/"} {
837+ for k := range snap.Prefix(ctx, []byte(p)) {
838+ keys = append(keys, slices.Clone(k))
839+ }
840+ }
841+ return keys
842+}
843+
844+const sweepChunk = 500
845+
846+// Sweep removes the runs completed before the time.
847+//
848+//hexcheck:ignore suspicious-business-logic-in-adapter chunked delete of expired records: a storage concern, the retention is decided by the caller
849+func (c *CI) Sweep(ctx context.Context, before time.Time) ([]int64, error) {
850+ snap := c.db.Snapshot(ctx)
851+ var ids []int64
852+ var keys [][]byte
853+ for _, rp := range []string{runPrefix} {
854+ for _, item := range snap.Prefix(ctx, []byte(rp)) {
855+ r, err := decodeRun(item)
856+ if err != nil {
857+ return nil, err
858+ }
859+ if r.Status != domain.CICompleted || r.CompletedAt.IsZero() || !r.CompletedAt.Before(before) {
860+ continue
861+ }
862+ ids = append(ids, r.ID)
863+ keys = append(keys, c.runKeys(ctx, snap, r.Repo, r.ID)...)
864+ for ck := range snap.Prefix(ctx, []byte(checkPrefix+r.Repo.String()+"/"+r.Sha+"/")) {
865+ // A check belongs to the run only while it is the latest one
866+ // for its job: leave it when another run replaced it.
867+ if it, ok := snap.Get(ctx, ck); ok {
868+ if chk, err := decodeCheck(it); err == nil && chk.RunID == r.ID {
869+ keys = append(keys, slices.Clone(ck))
870+ }
871+ }
872+ }
873+ }
874+ }
875+ for len(keys) > 0 {
876+ n := min(sweepChunk, len(keys))
877+ var b kv.Batch
878+ for _, k := range keys[:n] {
879+ b.Delete(k)
880+ }
881+ if _, err := c.db.Apply(ctx, &b); err != nil {
882+ return nil, fmt.Errorf("sweep CI runs: %w", err)
883+ }
884+ keys = keys[n:]
885+ }
886+ return ids, nil
887+}
888+
889+// --- runners ---
890+
891+func runnerKey(id string) []byte { return []byte(runnerPrefix + id) }
892+func runnerNameKey(n string) []byte { return []byte(runnerName + strings.ToLower(n)) }
893+func runnerTokenKey(h domain.TokenHash) []byte { return []byte(runnerTokPref + h.String()) }
894+
895+// CreateRunner stores the runner, its name index and its token index.
896+//
897+//hexcheck:ignore suspicious-business-logic-in-adapter replaces a runner of the same name in one batch: a storage concern, the rule is decided by the caller
898+func (c *CI) CreateRunner(ctx context.Context, r domain.CIRunner, replace bool) ([]domain.CIRunner, error) {
899+ for range createRunTries {
900+ var b kv.Batch
901+ var removed []domain.CIRunner
902+ if idx, ok := c.db.Get(ctx, runnerNameKey(r.Name)); ok {
903+ if !replace {
904+ return nil, domain.ErrAlreadyExists
905+ }
906+ old, err := c.GetRunner(ctx, string(idx.Value))
907+ if err != nil && !errors.Is(err, domain.ErrNotFound) {
908+ return nil, err
909+ }
910+ b.RequireVersion(runnerNameKey(r.Name), idx.Version)
911+ if err == nil {
912+ removed = append(removed, old)
913+ b.Delete(runnerKey(old.ID))
914+ b.Delete(runnerTokenKey(old.TokenHash))
915+ }
916+ } else {
917+ b.RequireAbsent(runnerNameKey(r.Name))
918+ }
919+ b.RequireAbsent(runnerKey(r.ID))
920+ b.RequireAbsent(runnerTokenKey(r.TokenHash))
921+ b.Put(runnerKey(r.ID), encodeRunner(r))
922+ b.Put(runnerNameKey(r.Name), []byte(r.ID))
923+ b.Put(runnerTokenKey(r.TokenHash), []byte(r.ID))
924+ _, err := c.db.Apply(ctx, &b)
925+ if errors.Is(err, kv.ErrConflict) {
926+ if !replace {
927+ if _, ok := c.db.Get(ctx, runnerNameKey(r.Name)); ok {
928+ return nil, domain.ErrAlreadyExists
929+ }
930+ }
931+ continue
932+ }
933+ if err != nil {
934+ return nil, fmt.Errorf("create runner %q: %w", r.Name, err)
935+ }
936+ return removed, nil
937+ }
938+ return nil, fmt.Errorf("create runner %q: %w", r.Name, domain.ErrConflict)
939+}
940+
941+// GetRunner reads runner/<id>.
942+func (c *CI) GetRunner(ctx context.Context, id string) (domain.CIRunner, error) {
943+ item, ok := c.db.Get(ctx, runnerKey(id))
944+ if !ok || id == "" || strings.Contains(id, "/") {
945+ return domain.CIRunner{}, domain.ErrNotFound
946+ }
947+ return decodeRunner(item.Value)
948+}
949+
950+// FindRunnerByToken resolves the token index, then reads the runner.
951+func (c *CI) FindRunnerByToken(ctx context.Context, hash domain.TokenHash) (domain.CIRunner, error) {
952+ snap := c.db.Snapshot(ctx)
953+ idx, ok := snap.Get(ctx, runnerTokenKey(hash))
954+ if !ok {
955+ return domain.CIRunner{}, domain.ErrNotFound
956+ }
957+ item, ok := snap.Get(ctx, runnerKey(string(idx.Value)))
958+ if !ok {
959+ return domain.CIRunner{}, domain.ErrNotFound
960+ }
961+ r, err := decodeRunner(item.Value)
962+ if err != nil {
963+ return domain.CIRunner{}, err
964+ }
965+ if r.TokenHash != hash {
966+ return domain.CIRunner{}, fmt.Errorf("runner %q: token index does not match the stored hash", r.ID)
967+ }
968+ return r, nil
969+}
970+
971+// ListRunners returns every runner ordered by name.
972+func (c *CI) ListRunners(ctx context.Context) ([]domain.CIRunner, error) {
973+ var out []domain.CIRunner
974+ for key, item := range c.db.Snapshot(ctx).Prefix(ctx, []byte(runnerPrefix)) {
975+ r, err := decodeRunner(item.Value)
976+ if err != nil {
977+ return nil, fmt.Errorf("decode %s: %w", key, err)
978+ }
979+ out = append(out, r)
980+ }
981+ slices.SortFunc(out, func(a, b domain.CIRunner) int {
982+ return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
983+ })
984+ return out, nil
985+}
986+
987+// RemoveRunner removes the runner, its name and its token.
988+func (c *CI) RemoveRunner(ctx context.Context, id string) (domain.CIRunner, error) {
989+ for range createRunTries {
990+ item, ok := c.db.Get(ctx, runnerKey(id))
991+ if !ok || id == "" || strings.Contains(id, "/") {
992+ return domain.CIRunner{}, domain.ErrNotFound
993+ }
994+ r, err := decodeRunner(item.Value)
995+ if err != nil {
996+ return domain.CIRunner{}, err
997+ }
998+ var b kv.Batch
999+ b.RequireVersion(runnerKey(id), item.Version)
1000+ b.Delete(runnerKey(id))
1001+ b.Delete(runnerNameKey(r.Name))
1002+ b.Delete(runnerTokenKey(r.TokenHash))
1003+ _, err = c.db.Apply(ctx, &b)
1004+ if errors.Is(err, kv.ErrConflict) {
1005+ continue
1006+ }
1007+ if err != nil {
1008+ return domain.CIRunner{}, fmt.Errorf("remove runner %q: %w", id, err)
1009+ }
1010+ return r, nil
1011+ }
1012+ return domain.CIRunner{}, fmt.Errorf("remove runner %q: %w", id, domain.ErrConflict)
1013+}
A internal/adapters/kvstore/ci_test.go +358 -0
@@ -0,0 +1,358 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package kvstore_test
4+
5+import (
6+ "context"
7+ "sync"
8+ "testing"
9+ "time"
10+
11+ "github.com/stretchr/testify/assert"
12+ "github.com/stretchr/testify/require"
13+
14+ "bnema.dev/ermite/internal/adapters/kvstore"
15+ "bnema.dev/ermite/internal/domain"
16+ "bnema.dev/ermite/internal/ports"
17+)
18+
19+// ciRepo opens a store holding repository "proj".
20+func ciRepo(t *testing.T) (*kvstore.Store, *kvstore.CI, domain.RepoName) {
21+ t.Helper()
22+ s := openStore(t, t.TempDir())
23+ n := name(t, "proj")
24+ require.NoError(t, s.Create(context.Background(), domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: created}))
25+ return s, s.CI(), n
26+}
27+
28+func draftOf(repo domain.RepoName, workflow string, jobs int) func(id, number int64) ports.CIRunDraft {
29+ return func(id, number int64) ports.CIRunDraft {
30+ d := ports.CIRunDraft{
31+ Run: domain.CIRun{
32+ Repo: repo, ID: id, Number: number, Attempt: 1, WorkflowPath: workflow, WorkflowName: "ci", Event: "push",
33+ Ref: "refs/heads/main", Sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Status: domain.CIQueued, CreatedAt: created,
34+ },
35+ Source: domain.CIRunSource{WorkflowYAML: "on: push", GithubJSON: "{}"},
36+ }
37+ for i := range jobs {
38+ j := domain.CIJob{
39+ ID: domain.NewJobID(id, i, 0), RunID: id, Attempt: 1, RunAttempt: 1, Repo: repo, Key: "build", Name: "build",
40+ Phase: domain.CIPhaseReady, ReadyAt: created.Add(time.Duration(i) * time.Second), CreatedAt: created,
41+ Labels: []string{"self-hosted"}, Index: i, TimeoutMinutes: 360, Planned: true,
42+ }
43+ d.Jobs = append(d.Jobs, j)
44+ d.Checks = append(d.Checks, domain.CICheck{
45+ ID: j.ID, Repo: repo, Sha: d.Run.Sha, Name: j.Name, Status: domain.CIQueued, RunID: id, JobID: j.ID, Attempt: 1,
46+ WorkflowName: "ci", CreatedAt: created,
47+ })
48+ }
49+ return d
50+ }
51+}
52+
53+func TestCICreateRunTakesIncreasingIdsAndPerWorkflowNumbers(t *testing.T) {
54+ ctx := context.Background()
55+ _, ci, n := ciRepo(t)
56+
57+ a, err := ci.CreateRun(ctx, n, ".github/workflows/ci.yml", draftOf(n, ".github/workflows/ci.yml", 1))
58+ require.NoError(t, err)
59+ b, err := ci.CreateRun(ctx, n, ".github/workflows/ci.yml", draftOf(n, ".github/workflows/ci.yml", 1))
60+ require.NoError(t, err)
61+ c, err := ci.CreateRun(ctx, n, ".github/workflows/lint.yml", draftOf(n, ".github/workflows/lint.yml", 0))
62+ require.NoError(t, err)
63+
64+ assert.Equal(t, []int64{1, 2, 3}, []int64{a.Run.ID, b.Run.ID, c.Run.ID})
65+ assert.Equal(t, []int64{1, 2, 1}, []int64{a.Run.Number, b.Run.Number, c.Run.Number})
66+ assert.NotZero(t, a.Run.Version)
67+}
68+
69+func TestCIConcurrentCreateRunNeverSharesAnId(t *testing.T) {
70+ ctx := context.Background()
71+ _, ci, n := ciRepo(t)
72+ var mu sync.Mutex
73+ ids := map[int64]bool{}
74+ var wg sync.WaitGroup
75+ for range 12 {
76+ wg.Add(1)
77+ go func() {
78+ defer wg.Done()
79+ d, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 1))
80+ if assert.NoError(t, err) {
81+ mu.Lock()
82+ defer mu.Unlock()
83+ assert.False(t, ids[d.Run.ID], "id %d handed out twice", d.Run.ID)
84+ ids[d.Run.ID] = true
85+ }
86+ }()
87+ }
88+ wg.Wait()
89+ assert.Len(t, ids, 12)
90+}
91+
92+func TestCICreateRunFailsOnAMissingRepository(t *testing.T) {
93+ _, ci, _ := ciRepo(t)
94+ ghost := name(t, "ghost")
95+ _, err := ci.CreateRun(context.Background(), ghost, "w.yml", draftOf(ghost, "w.yml", 1))
96+ assert.ErrorIs(t, err, domain.ErrNotFound)
97+}
98+
99+func TestCIJobIndexesFollowThePhase(t *testing.T) {
100+ ctx := context.Background()
101+ _, ci, n := ciRepo(t)
102+ d, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 2))
103+ require.NoError(t, err)
104+
105+ ready, err := ci.ListReadyJobs(ctx, 10)
106+ require.NoError(t, err)
107+ require.Len(t, ready, 2)
108+ assert.Equal(t, d.Jobs[0].ID, ready[0].ID, "oldest first")
109+
110+ open, err := ci.ListOpenRuns(ctx)
111+ require.NoError(t, err)
112+ assert.Len(t, open, 1)
113+
114+ // Assign the first job: it leaves the queue, enters the active set and its
115+ // token becomes findable.
116+ j := ready[0]
117+ hash := domain.HashTokenSecret("ermitejob_x")
118+ j.Phase, j.TokenHash, j.AssignedAt = domain.CIPhaseAssigned, hash, created.Add(time.Minute)
119+ j.TokenValidUntil = created.Add(48 * time.Hour)
120+ _, err = ci.Apply(ctx, ports.CIWrite{Jobs: []domain.CIJob{j}})
121+ require.NoError(t, err)
122+
123+ ready, _ = ci.ListReadyJobs(ctx, 10)
124+ assert.Len(t, ready, 1)
125+ active, err := ci.ListActiveJobs(ctx)
126+ require.NoError(t, err)
127+ require.Len(t, active, 1)
128+ assert.Equal(t, j.ID, active[0].ID)
129+ found, err := ci.FindJobByToken(ctx, hash, created.Add(2*time.Minute))
130+ require.NoError(t, err)
131+ assert.Equal(t, j.ID, found.ID)
132+ _, err = ci.FindJobByToken(ctx, hash, created.Add(72*time.Hour))
133+ assert.ErrorIs(t, err, domain.ErrNotFound, "an expired token is not found")
134+
135+ // A new token replaces the old one.
136+ j2, err := ci.GetJob(ctx, j.RunID, j.RunAttempt, j.ID)
137+ require.NoError(t, err)
138+ j2.TokenHash = domain.HashTokenSecret("ermitejob_y")
139+ _, err = ci.Apply(ctx, ports.CIWrite{Jobs: []domain.CIJob{j2}})
140+ require.NoError(t, err)
141+ _, err = ci.FindJobByToken(ctx, hash, created.Add(2*time.Minute))
142+ assert.ErrorIs(t, err, domain.ErrNotFound, "the replaced token is gone")
143+
144+ // End it.
145+ j3, _ := ci.GetJob(ctx, j.RunID, j.RunAttempt, j.ID)
146+ j3.Phase, j3.Conclusion = domain.CIPhaseEnded, domain.CISuccess
147+ _, err = ci.Apply(ctx, ports.CIWrite{Jobs: []domain.CIJob{j3}})
148+ require.NoError(t, err)
149+ active, _ = ci.ListActiveJobs(ctx)
150+ assert.Empty(t, active)
151+}
152+
153+func TestCIApplyChecksVersions(t *testing.T) {
154+ ctx := context.Background()
155+ _, ci, n := ciRepo(t)
156+ d, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 1))
157+ require.NoError(t, err)
158+
159+ a, err := ci.GetRun(ctx, n, d.Run.ID)
160+ require.NoError(t, err)
161+ b := a
162+ a.Status = domain.CIInProgress
163+ _, err = ci.Apply(ctx, ports.CIWrite{Runs: []domain.CIRun{a}})
164+ require.NoError(t, err)
165+ b.Status = domain.CICompleted
166+ _, err = ci.Apply(ctx, ports.CIWrite{Runs: []domain.CIRun{b}})
167+ assert.ErrorIs(t, err, domain.ErrConflict, "a write on a stale copy loses")
168+}
169+
170+func TestCIChecksAreOrderedAndLimited(t *testing.T) {
171+ ctx := context.Background()
172+ _, ci, n := ciRepo(t)
173+ d, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 3))
174+ require.NoError(t, err)
175+
176+ checks, more, err := ci.ListChecks(ctx, n, d.Run.Sha, 10)
177+ require.NoError(t, err)
178+ assert.Len(t, checks, 3)
179+ assert.False(t, more)
180+ checks, more, err = ci.ListChecks(ctx, n, d.Run.Sha, 2)
181+ require.NoError(t, err)
182+ assert.Len(t, checks, 2)
183+ assert.True(t, more)
184+ none, _, err := ci.ListChecks(ctx, n, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 10)
185+ require.NoError(t, err)
186+ assert.Empty(t, none)
187+}
188+
189+func TestCIListRunsNewestFirstAndBounded(t *testing.T) {
190+ ctx := context.Background()
191+ s, ci, n := ciRepo(t)
192+ other := name(t, "other")
193+ require.NoError(t, s.Create(ctx, domain.Repo{Name: other, Visibility: domain.VisibilityPublic, CreatedAt: created}))
194+ _, err := ci.CreateRun(ctx, other, "w.yml", draftOf(other, "w.yml", 0))
195+ require.NoError(t, err)
196+ for range 5 {
197+ _, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 0))
198+ require.NoError(t, err)
199+ }
200+ runs, err := ci.ListRuns(ctx, n, 0, 3)
201+ require.NoError(t, err)
202+ require.Len(t, runs, 3)
203+ assert.Equal(t, []int64{6, 5, 4}, []int64{runs[0].ID, runs[1].ID, runs[2].ID})
204+ runs, err = ci.ListRuns(ctx, n, 4, 10)
205+ require.NoError(t, err)
206+ assert.Equal(t, []int64{3, 2}, []int64{runs[0].ID, runs[1].ID})
207+ ids, err := ci.RunIDs(ctx, n)
208+ require.NoError(t, err)
209+ assert.Len(t, ids, 5)
210+}
211+
212+func TestCIRunnersCreateReplaceFindRemove(t *testing.T) {
213+ ctx := context.Background()
214+ _, ci, _ := ciRepo(t)
215+ h1, h2 := domain.HashTokenSecret("ermiterun_1"), domain.HashTokenSecret("ermiterun_2")
216+ r1 := domain.CIRunner{ID: "r1", Name: "Alpha", Labels: []string{"linux"}, Isolation: domain.CIIsolationVM, TokenHash: h1, RegisteredAt: created}
217+ _, err := ci.CreateRunner(ctx, r1, false)
218+ require.NoError(t, err)
219+
220+ r2 := domain.CIRunner{ID: "r2", Name: "alpha", TokenHash: h2, RegisteredAt: created}
221+ _, err = ci.CreateRunner(ctx, r2, false)
222+ assert.ErrorIs(t, err, domain.ErrAlreadyExists, "names are case-insensitive")
223+
224+ removed, err := ci.CreateRunner(ctx, r2, true)
225+ require.NoError(t, err)
226+ require.Len(t, removed, 1)
227+ assert.Equal(t, "r1", removed[0].ID)
228+ _, err = ci.FindRunnerByToken(ctx, h1)
229+ assert.ErrorIs(t, err, domain.ErrNotFound, "the replaced runner's token stops working")
230+ got, err := ci.FindRunnerByToken(ctx, h2)
231+ require.NoError(t, err)
232+ assert.Equal(t, "r2", got.ID)
233+
234+ list, err := ci.ListRunners(ctx)
235+ require.NoError(t, err)
236+ assert.Len(t, list, 1)
237+
238+ _, err = ci.RemoveRunner(ctx, "r2")
239+ require.NoError(t, err)
240+ _, err = ci.RemoveRunner(ctx, "r2")
241+ assert.ErrorIs(t, err, domain.ErrNotFound)
242+ _, err = ci.FindRunnerByToken(ctx, h2)
243+ assert.ErrorIs(t, err, domain.ErrNotFound)
244+ // The name is free again.
245+ _, err = ci.CreateRunner(ctx, domain.CIRunner{ID: "r3", Name: "alpha", TokenHash: domain.HashTokenSecret("ermiterun_3")}, false)
246+ assert.NoError(t, err)
247+}
248+
249+func TestCISweepRemovesOldCompletedRunsOnly(t *testing.T) {
250+ ctx := context.Background()
251+ _, ci, n := ciRepo(t)
252+ oldRun, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 1))
253+ require.NoError(t, err)
254+ newRun, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 1))
255+ require.NoError(t, err)
256+ openRun, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 1))
257+ require.NoError(t, err)
258+
259+ for _, tc := range []struct {
260+ d ports.CIRunDraft
261+ at time.Time
262+ }{{oldRun, created}, {newRun, created.Add(100 * 24 * time.Hour)}} {
263+ run, _ := ci.GetRun(ctx, n, tc.d.Run.ID)
264+ run.Status, run.Conclusion, run.CompletedAt = domain.CICompleted, domain.CISuccess, tc.at
265+ job, _ := ci.GetJob(ctx, run.ID, 1, tc.d.Jobs[0].ID)
266+ job.Phase, job.Conclusion = domain.CIPhaseEnded, domain.CISuccess
267+ _, err := ci.Apply(ctx, ports.CIWrite{Runs: []domain.CIRun{run}, Jobs: []domain.CIJob{job}})
268+ require.NoError(t, err)
269+ }
270+
271+ ids, err := ci.Sweep(ctx, created.Add(90*24*time.Hour))
272+ require.NoError(t, err)
273+ assert.Equal(t, []int64{oldRun.Run.ID}, ids)
274+
275+ _, err = ci.GetRun(ctx, n, oldRun.Run.ID)
276+ assert.ErrorIs(t, err, domain.ErrNotFound)
277+ _, err = ci.GetRunSource(ctx, oldRun.Run.ID)
278+ assert.ErrorIs(t, err, domain.ErrNotFound)
279+ jobs, _ := ci.ListJobs(ctx, oldRun.Run.ID, 1)
280+ assert.Empty(t, jobs)
281+ _, err = ci.GetRun(ctx, n, newRun.Run.ID)
282+ assert.NoError(t, err)
283+ _, err = ci.GetRun(ctx, n, openRun.Run.ID)
284+ assert.NoError(t, err, "an open run is never swept")
285+
286+ // Numbers are never reused.
287+ next, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 0))
288+ require.NoError(t, err)
289+ assert.Equal(t, int64(4), next.Run.Number)
290+}
291+
292+func TestCIRepositoryDeletionRemovesEverythingAndRestartsNumbers(t *testing.T) {
293+ ctx := context.Background()
294+ s, ci, n := ciRepo(t)
295+ d, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 2))
296+ require.NoError(t, err)
297+ _, err = ci.CreateRunner(ctx, domain.CIRunner{ID: "r1", Name: "a", TokenHash: domain.HashTokenSecret("x")}, false)
298+ require.NoError(t, err)
299+
300+ require.NoError(t, s.Delete(ctx, n))
301+
302+ _, err = ci.GetRun(ctx, n, d.Run.ID)
303+ assert.ErrorIs(t, err, domain.ErrNotFound)
304+ _, err = ci.GetRunSource(ctx, d.Run.ID)
305+ assert.ErrorIs(t, err, domain.ErrNotFound)
306+ jobs, _ := ci.ListJobs(ctx, d.Run.ID, 1)
307+ assert.Empty(t, jobs)
308+ checks, _, _ := ci.ListChecks(ctx, n, d.Run.Sha, 10)
309+ assert.Empty(t, checks)
310+ ready, _ := ci.ListReadyJobs(ctx, 10)
311+ assert.Empty(t, ready)
312+ open, _ := ci.ListOpenRuns(ctx)
313+ assert.Empty(t, open)
314+ runners, _ := ci.ListRunners(ctx)
315+ assert.Len(t, runners, 1, "runners do not belong to a repository")
316+
317+ // A repository created again under the name starts its run numbers at 1,
318+ // while ids are never reused.
319+ require.NoError(t, s.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: created}))
320+ again, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 0))
321+ require.NoError(t, err)
322+ assert.Equal(t, int64(1), again.Run.Number)
323+ assert.Equal(t, int64(2), again.Run.ID)
324+}
325+
326+func TestCIStateSurvivesARestart(t *testing.T) {
327+ ctx := context.Background()
328+ dir := t.TempDir()
329+ s, err := kvstore.Open(ctx, dir)
330+ require.NoError(t, err)
331+ n := name(t, "proj")
332+ require.NoError(t, s.Create(ctx, domain.Repo{Name: n, Visibility: domain.VisibilityPrivate, CreatedAt: created}))
333+ ci := s.CI()
334+ d, err := ci.CreateRun(ctx, n, "w.yml", draftOf(n, "w.yml", 1))
335+ require.NoError(t, err)
336+ j := d.Jobs[0]
337+ hash := domain.HashTokenSecret("ermitejob_z")
338+ j.Phase, j.TokenHash, j.RunnerID, j.RunnerName = domain.CIPhaseRunning, hash, "r1", "alpha"
339+ j.Steps = []domain.CIStep{{Number: 1, Name: "Set up job", State: domain.CIStepCompleted, Conclusion: domain.CISuccess, HasLines: true, LogStart: 0}}
340+ j.Outputs = map[string]string{"a": "b"}
341+ _, err = ci.Apply(ctx, ports.CIWrite{Jobs: []domain.CIJob{j}})
342+ require.NoError(t, err)
343+ require.NoError(t, s.Close(ctx))
344+
345+ s2, err := kvstore.Open(ctx, dir)
346+ require.NoError(t, err)
347+ defer func() { _ = s2.Close(ctx) }()
348+ ci2 := s2.CI()
349+ got, err := ci2.FindJobByToken(ctx, hash, time.Time{})
350+ require.NoError(t, err)
351+ assert.Equal(t, domain.CIPhaseRunning, got.Phase)
352+ assert.Equal(t, "alpha", got.RunnerName)
353+ assert.Equal(t, map[string]string{"a": "b"}, got.Outputs)
354+ require.Len(t, got.Steps, 1)
355+ assert.True(t, got.Steps[0].HasLines)
356+ active, _ := ci2.ListActiveJobs(ctx)
357+ assert.Len(t, active, 1)
358+}
M internal/adapters/kvstore/repos.go +3 -2
@@ -99,7 +99,8 @@ const deleteChunk = 1000
9999 const deleteTries = 32
100100
101101 // Delete removes the repository and everything keyed to it: its merge
102-// requests, their comments and read positions, and its number sequence. The
102+// requests, their comments and read positions, and its number sequence, and
103+// its CI runs, jobs, checks and run numbers. The
103104 // Git data is moved to the trash by the caller; the merge requests are not
104105 // kept anywhere, like the repository metadata, so a repository created again
105106 // under the same name starts clean, with its numbers starting at 1. A missing
@@ -119,7 +120,7 @@ func (s *Store) Delete(ctx context.Context, name domain.RepoName) error {
119120 for range deleteTries {
120121 snap := s.db.Snapshot(ctx)
121122 repoItem, repoOK := snap.Get(ctx, repoKey(name))
122- keys := mergeRequestKeys(ctx, snap, name)
123+ keys := append(mergeRequestKeys(ctx, snap, name), ciKeys(ctx, snap, name)...)
123124 for len(keys) > deleteChunk {
124125 var b kv.Batch
125126 for _, k := range keys[:deleteChunk] {
A internal/domain/ci.go +548 -0
@@ -0,0 +1,548 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package domain
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "regexp"
9+ "slices"
10+ "strconv"
11+ "strings"
12+ "time"
13+)
14+
15+// CIStatus is where a run, a job or a check is in its life (GitHub's status).
16+type CIStatus string
17+
18+// Statuses.
19+const (
20+ CIQueued CIStatus = "queued"
21+ CIInProgress CIStatus = "in_progress"
22+ CICompleted CIStatus = "completed"
23+)
24+
25+// CIConclusion is how a run, a job, a step or a check ended, with GitHub's
26+// values. It is empty until the status is completed.
27+type CIConclusion string
28+
29+// Conclusions.
30+const (
31+ CINoConclusion CIConclusion = ""
32+ CISuccess CIConclusion = "success"
33+ CIFailure CIConclusion = "failure"
34+ CICancelled CIConclusion = "cancelled"
35+ CISkipped CIConclusion = "skipped"
36+ CITimedOut CIConclusion = "timed_out"
37+ CINeutral CIConclusion = "neutral"
38+ CIActionRequired CIConclusion = "action_required"
39+)
40+
41+// PassesMerge reports whether a check that ended this way lets a merge go
42+// through under FailingChecksBlock: success, skipped and neutral do.
43+func (c CIConclusion) PassesMerge() bool {
44+ return c == CISuccess || c == CISkipped || c == CINeutral
45+}
46+
47+// IsFailure reports whether the conclusion is a failure or a timeout.
48+func (c CIConclusion) IsFailure() bool { return c == CIFailure || c == CITimedOut }
49+
50+// CIStepState is where a step is in its life.
51+type CIStepState string
52+
53+// Step states.
54+const (
55+ CIStepPending CIStepState = "pending"
56+ CIStepInProgress CIStepState = "in_progress"
57+ CIStepCompleted CIStepState = "completed"
58+)
59+
60+// CIJobPhase is where a job is for the server, finer than the status owners
61+// see.
62+type CIJobPhase string
63+
64+// Job phases.
65+const (
66+ // CIPhaseWaiting: waits for the jobs it needs or for a max-parallel slot.
67+ CIPhaseWaiting CIJobPhase = "waiting"
68+ // CIPhaseReady: planned, waits for a runner.
69+ CIPhaseReady CIJobPhase = "ready"
70+ // CIPhaseAssigned: given to a runner that has not acknowledged it.
71+ CIPhaseAssigned CIJobPhase = "assigned"
72+ // CIPhaseRunning: acknowledged by its runner.
73+ CIPhaseRunning CIJobPhase = "running"
74+ // CIPhaseEnded: over.
75+ CIPhaseEnded CIJobPhase = "ended"
76+)
77+
78+// CIIsolation is how a runner isolates jobs.
79+type CIIsolation string
80+
81+// Isolations.
82+const (
83+ CIIsolationNone CIIsolation = ""
84+ CIIsolationVM CIIsolation = "vm"
85+ CIIsolationContainerSysbox CIIsolation = "container-sysbox"
86+ CIIsolationContainerPrivileged CIIsolation = "container-privileged"
87+)
88+
89+// CICancelReason says why a job is asked to stop.
90+type CICancelReason string
91+
92+// Cancel reasons.
93+const (
94+ CICancelNone CICancelReason = ""
95+ CICancelUser CICancelReason = "user"
96+ CICancelFailFast CICancelReason = "fail-fast"
97+ CICancelTimeout CICancelReason = "timeout"
98+)
99+
100+// Limits and durations of the CI protocol (runner.proto).
101+const (
102+ // CIMaxLogBytes is the size of a job log, as stored (512 MiB).
103+ CIMaxLogBytes = 512 << 20
104+ // CIMaxOutputBytes bounds one output and all outputs together (1 MiB).
105+ CIMaxOutputBytes = 1 << 20
106+ // CIMaxLogLines and CIMaxLogChunkBytes bound one AppendLog request.
107+ CIMaxLogLines = 1000
108+ CIMaxLogChunkBytes = 64 << 10
109+ // CIMaxSteps bounds the steps of a job.
110+ CIMaxSteps = 1000
111+ // CIMaxStepName bounds a step name, in characters.
112+ CIMaxStepName = 1024
113+ // CIMaxLabels and CIMaxLabelLen bound the labels of a runner.
114+ CIMaxLabels = 64
115+ CIMaxLabelLen = 64
116+ // CIDefaultTimeoutMinutes is the timeout of a job without timeout-minutes.
117+ CIDefaultTimeoutMinutes = 360
118+ // CIMaxAssignCancels is how many times in a row an assignment may be
119+ // cancelled before the job fails.
120+ CIMaxAssignCancels = 3
121+ // CIRetentionDays is the retention of runs, jobs, checks and logs.
122+ CIRetentionDays = 90
123+)
124+
125+// Durations of the CI protocol.
126+const (
127+ // CIAckTimeout is how long a runner has to send its first heartbeat.
128+ CIAckTimeout = 30 * time.Second
129+ // CIHeartbeatDeadline is how long a running job may go without heartbeat.
130+ CIHeartbeatDeadline = 2 * time.Minute
131+ // CICancelGrace is how long a runner has to conclude after a cancel request.
132+ CICancelGrace = 60 * time.Second
133+ // CITimeoutSlack is added to timeout-minutes by the server.
134+ CITimeoutSlack = 60 * time.Second
135+ // CIQueueTimeout is how long a job waits for a runner.
136+ CIQueueTimeout = 24 * time.Hour
137+ // CITokenGrace is how long a job token is recognised after the job ended.
138+ CITokenGrace = 24 * time.Hour
139+)
140+
141+// Messages of jobs ended by the server, GitHub's where it has them.
142+const (
143+ // CIQueueTimeoutMessage is GitHub's message for a job that no runner took.
144+ CIQueueTimeoutMessage = "The job was not acquired by Runner of type self-hosted even after multiple attempts"
145+ // CIShutdownMessage is GitHub's message for a runner that stops.
146+ CIShutdownMessage = "The runner has received a shutdown signal. This can happen when the runner service is stopped, or a manually started runner is canceled."
147+ // CILogLimitMessage ends a log that reached its limit.
148+ CILogLimitMessage = "The log of this job reached the limit of 512 MiB; the job is stopped."
149+)
150+
151+// CILostMessage is GitHub's message for a runner that stopped sending heartbeats.
152+func CILostMessage(runner string) string {
153+ return "The self-hosted runner: " + runner + " lost communication with the server. Verify the machine is running and has a healthy network connection. Anything in your workflow that terminates the runner process, starves it for CPU/Memory, or blocks its network access can cause this error."
154+}
155+
156+// CIRun is one execution of one workflow file for one event.
157+type CIRun struct {
158+ Repo RepoName
159+ ID int64
160+ Number int64
161+ Attempt int32
162+ WorkflowPath string
163+ WorkflowName string
164+ Title string
165+ Event string
166+ Ref string
167+ Sha string
168+ Actor string
169+ Status CIStatus
170+ Conclusion CIConclusion
171+ Error string
172+ CreatedAt time.Time
173+ StartedAt time.Time
174+ CompletedAt time.Time
175+ CancelRequested bool
176+ CancelRequestedAt time.Time
177+ // Version is the version of the stored record, which grows at every
178+ // change and never goes back.
179+ Version uint64
180+}
181+
182+// CIRunSource is what a run was planned from: the workflow file at the sha of
183+// the run, and the "github" context without the parts that depend on the job
184+// and the attempt.
185+type CIRunSource struct {
186+ RunID int64
187+ Repo RepoName
188+ WorkflowYAML string
189+ GithubJSON string
190+}
191+
192+// CIStep is a step of a job with where its lines are in the job log.
193+type CIStep struct {
194+ Number int32
195+ Name string
196+ State CIStepState
197+ Conclusion CIConclusion
198+ StartedAt time.Time
199+ CompletedAt time.Time
200+ LogStart int64
201+ // HasLines is set once a line of the step reached the log; LogStart is
202+ // then the number of its first line. LogEnd is not stored: see StepRanges.
203+ HasLines bool
204+}
205+
206+// CIJob is one job instance (one matrix combination) in one attempt.
207+type CIJob struct {
208+ ID string
209+ RunID int64
210+ // Attempt is the attempt in which the job executed, RunAttempt the attempt
211+ // of the run the record belongs to: they differ for a job kept by a re-run
212+ // of the failed jobs.
213+ Attempt int32
214+ RunAttempt int32
215+ Repo RepoName
216+ Key string
217+ Name string
218+ Phase CIJobPhase
219+ Conclusion CIConclusion
220+ Error string
221+ Labels []string
222+ Needs []string
223+ RunnerID string
224+ RunnerName string
225+ Isolation CIIsolation
226+ Steps []CIStep
227+
228+ CreatedAt time.Time
229+ StartedAt time.Time
230+ CompletedAt time.Time
231+
232+ // Index is the position of the job in the workflow file, Combo the
233+ // position of the instance in its matrix.
234+ Index int
235+ Combo int
236+ // Planned is set once runs-on, the name and the contexts are evaluated.
237+ // A job that is not planned has not been expanded yet.
238+ Planned bool
239+ MatrixJSON string
240+ // ComboTotal is the number of instances of the job's matrix.
241+ ComboTotal int
242+ // TimeoutMinutes is the timeout of the job, at least 1.
243+ TimeoutMinutes int32
244+ FailFast bool
245+ // MaxParallel is the max-parallel of the strategy, 0 for no limit.
246+ MaxParallel int
247+ Outputs map[string]string
248+
249+ // TokenHash is the hash of the job token of the current assignment; zero
250+ // when there is none.
251+ TokenHash TokenHash
252+ TokenValidUntil time.Time
253+ ReadyAt time.Time
254+ AssignedAt time.Time
255+ AckedAt time.Time
256+ // AssignCancels counts the assignments cancelled in a row.
257+ AssignCancels int
258+
259+ CancelReason CICancelReason
260+ CancelMessage string
261+ CancelRequestedAt time.Time
262+
263+ Version uint64
264+}
265+
266+// Status is the status owners see: a job is queued until its runner
267+// acknowledged it.
268+func (j CIJob) Status() CIStatus {
269+ switch j.Phase {
270+ case CIPhaseRunning:
271+ return CIInProgress
272+ case CIPhaseEnded:
273+ return CICompleted
274+ }
275+ return CIQueued
276+}
277+
278+// Active reports whether the job is given to a runner and not over.
279+func (j CIJob) Active() bool { return j.Phase == CIPhaseAssigned || j.Phase == CIPhaseRunning }
280+
281+// Ended reports whether the job is over.
282+func (j CIJob) Ended() bool { return j.Phase == CIPhaseEnded }
283+
284+// HasToken reports whether a job token is set.
285+func (j CIJob) HasToken() bool { return j.TokenHash != TokenHash{} }
286+
287+// CICheck is the result of one job on one commit.
288+type CICheck struct {
289+ ID string
290+ Repo RepoName
291+ Sha string
292+ Name string
293+ Status CIStatus
294+ Conclusion CIConclusion
295+ RunID int64
296+ JobID string
297+ Attempt int32
298+ WorkflowName string
299+ CreatedAt time.Time
300+ StartedAt time.Time
301+ CompletedAt time.Time
302+ Version uint64
303+}
304+
305+// CIRunner is a registered CI runner. TokenHash is never sent to clients.
306+type CIRunner struct {
307+ ID string
308+ Name string
309+ Labels []string
310+ Isolation CIIsolation
311+ Version string
312+ Arch string
313+ OS string
314+ RegisteredAt time.Time
315+ TokenHash TokenHash
316+}
317+
318+// RunnerTokenPrefix and JobTokenPrefix start the secrets of runners and jobs.
319+const (
320+ RunnerTokenPrefix = "ermiterun_"
321+ JobTokenPrefix = "ermitejob_"
322+ // CITokenRandomLen is the length of the base64url part of both.
323+ CITokenRandomLen = 43
324+)
325+
326+// IsRunnerToken and IsJobToken report whether s has the shape of the secret.
327+func IsRunnerToken(s string) bool { return hasTokenShape(s, RunnerTokenPrefix) }
328+func IsJobToken(s string) bool { return hasTokenShape(s, JobTokenPrefix) }
329+
330+func hasTokenShape(s, prefix string) bool {
331+ rest, ok := strings.CutPrefix(s, prefix)
332+ if !ok || len(rest) != CITokenRandomLen {
333+ return false
334+ }
335+ for _, r := range rest {
336+ switch {
337+ case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_':
338+ default:
339+ return false
340+ }
341+ }
342+ return true
343+}
344+
345+// NewJobID names a job instance: "<run>-<index>-<combo>". It is the same in
346+// every attempt of the run.
347+func NewJobID(runID int64, index, combo int) string {
348+ return fmt.Sprintf("%d-%d-%d", runID, index, combo)
349+}
350+
351+// ParseJobID returns the run of a job id, or ok false when s is not one.
352+func ParseJobID(s string) (runID int64, index, combo int, ok bool) {
353+ parts := strings.Split(s, "-")
354+ if len(parts) != 3 {
355+ return 0, 0, 0, false
356+ }
357+ r, err1 := strconv.ParseInt(parts[0], 10, 64)
358+ i, err2 := strconv.Atoi(parts[1])
359+ c, err3 := strconv.Atoi(parts[2])
360+ if err1 != nil || err2 != nil || err3 != nil || r < 1 || i < 0 || c < 0 || NewJobID(r, i, c) != s {
361+ return 0, 0, 0, false
362+ }
363+ return r, i, c, true
364+}
365+
366+var labelRE = regexp.MustCompile(`^[A-Za-z0-9._-]{1,64}$`)
367+
368+// ParseRunnerName validates a runner name.
369+func ParseRunnerName(s string) error {
370+ if !labelRE.MatchString(s) {
371+ return fmt.Errorf("%w: a runner name has 1 to 64 letters, digits, '.', '_' or '-'", ErrInvalid)
372+ }
373+ return nil
374+}
375+
376+// ParseRunnerLabels validates labels and returns them lowercased, without
377+// duplicates, in order of first appearance.
378+func ParseRunnerLabels(labels []string) ([]string, error) {
379+ if len(labels) > CIMaxLabels {
380+ return nil, fmt.Errorf("%w: at most %d labels", ErrInvalid, CIMaxLabels)
381+ }
382+ var out []string
383+ for _, l := range labels {
384+ if !labelRE.MatchString(l) {
385+ return nil, fmt.Errorf("%w: label %q: 1 to %d letters, digits, '.', '_' or '-'", ErrInvalid, l, CIMaxLabelLen)
386+ }
387+ l = strings.ToLower(l)
388+ if !slices.Contains(out, l) {
389+ out = append(out, l)
390+ }
391+ }
392+ return out, nil
393+}
394+
395+// RunnerLabels returns the labels a runner matches: the ones it chose plus
396+// the implicit "self-hosted", its os and its arch, lowercase and sorted.
397+func RunnerLabels(chosen []string, os, arch string) []string {
398+ out := slices.Clone(chosen)
399+ for _, l := range []string{"self-hosted", strings.ToLower(os), strings.ToLower(arch)} {
400+ if !slices.Contains(out, l) {
401+ out = append(out, l)
402+ }
403+ }
404+ slices.Sort(out)
405+ return out
406+}
407+
408+// LabelsMatch reports whether every wanted label is among have, ignoring case.
409+func LabelsMatch(want, have []string) bool {
410+ for _, w := range want {
411+ if !slices.ContainsFunc(have, func(h string) bool { return strings.EqualFold(h, w) }) {
412+ return false
413+ }
414+ }
415+ return true
416+}
417+
418+var outputNameRE = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_-]{0,254}$`)
419+
420+// CheckOutputs validates the outputs of a job: names, case-insensitive
421+// uniqueness and the 1 MiB limits.
422+func CheckOutputs(outputs map[string]string) error {
423+ seen := make(map[string]bool, len(outputs))
424+ total := 0
425+ for k, v := range outputs {
426+ if !outputNameRE.MatchString(k) {
427+ return fmt.Errorf("%w: output name %q: 1 to 255 letters, digits, '_' or '-', starting with a letter or '_'", ErrInvalid, k)
428+ }
429+ lk := strings.ToLower(k)
430+ if seen[lk] {
431+ return fmt.Errorf("%w: output names differ only by case: %q", ErrInvalid, k)
432+ }
433+ seen[lk] = true
434+ if len(v) > CIMaxOutputBytes {
435+ return fmt.Errorf("%w: output %q is larger than 1 MiB", ErrInvalid, k)
436+ }
437+ total += len(k) + len(v)
438+ }
439+ if total > CIMaxOutputBytes {
440+ return fmt.Errorf("%w: the outputs are larger than 1 MiB together", ErrInvalid)
441+ }
442+ return nil
443+}
444+
445+// JobEndedError is returned for a call that needs a running job when the job
446+// has ended. It matches ErrPreconditionFailed; the API adds the JobEnded
447+// detail.
448+type JobEndedError struct {
449+ Conclusion CIConclusion
450+ Message string
451+}
452+
453+// Error implements error.
454+func (e *JobEndedError) Error() string {
455+ return fmt.Sprintf("%v: the job has ended (%s)", ErrPreconditionFailed, e.Conclusion)
456+}
457+
458+// Is makes errors.Is(err, ErrPreconditionFailed) true.
459+func (e *JobEndedError) Is(target error) bool { return target == ErrPreconditionFailed }
460+
461+// IsJobEnded returns the JobEndedError in err.
462+func IsJobEnded(err error) (*JobEndedError, bool) {
463+ var e *JobEndedError
464+ if errors.As(err, &e) {
465+ return e, true
466+ }
467+ return nil, false
468+}
469+
470+// ErrOutOfRange means an offset is beyond what the server holds.
471+var ErrOutOfRange = errors.New("out of range")
472+
473+// CILogLine is one line of a job log.
474+type CILogLine struct {
475+ Time time.Time
476+ // Text has no line feed.
477+ Text string
478+ // Continued marks a piece of a line that was too long to send whole.
479+ Continued bool
480+}
481+
482+// StepRange is where the lines of a step are in the job log.
483+type StepRange struct {
484+ Start, End int64
485+}
486+
487+// StepRanges returns the range of every step of the job for a log of lines
488+// lines: a step with lines ends where the next step with lines begins (the
489+// last ends at the end of the log); a step without lines is empty, placed
490+// where the previous step ends.
491+func StepRanges(steps []CIStep, lines int64) []StepRange {
492+ out := make([]StepRange, len(steps))
493+ var pos int64
494+ for i, s := range steps {
495+ if !s.HasLines {
496+ out[i] = StepRange{Start: pos, End: pos}
497+ continue
498+ }
499+ end := lines
500+ for _, n := range steps[i+1:] {
501+ if n.HasLines {
502+ end = n.LogStart
503+ break
504+ }
505+ }
506+ out[i] = StepRange{Start: s.LogStart, End: max(end, s.LogStart)}
507+ pos = out[i].End
508+ }
509+ return out
510+}
511+
512+// StepOfLine returns the number of the step a log line belongs to, 0 when no
513+// step has that line.
514+func StepOfLine(steps []CIStep, line int64) int32 {
515+ var found int32
516+ for _, s := range steps {
517+ if s.HasLines && s.LogStart <= line {
518+ found = s.Number
519+ }
520+ }
521+ return found
522+}
523+
524+// RunUpdated is published when a CI run is created or changes. It carries the
525+// visibility and the creation time of the repository, like PushEvent.
526+type RunUpdated struct {
527+ Run CIRun
528+ At time.Time
529+ Visibility Visibility
530+ CreatedAt time.Time
531+}
532+
533+// EventKind implements Event.
534+func (RunUpdated) EventKind() string { return "ci.run_updated" }
535+
536+// JobUpdated is published when a CI job is created or changes (status,
537+// conclusion, runner or steps; never per log line). Lines is the number of
538+// lines of the job log at that time, for the end of the step in progress.
539+type JobUpdated struct {
540+ Job CIJob
541+ Lines int64
542+ At time.Time
543+ Visibility Visibility
544+ CreatedAt time.Time
545+}
546+
547+// EventKind implements Event.
548+func (JobUpdated) EventKind() string { return "ci.job_updated" }
A internal/ports/ci.go +354 -0
@@ -0,0 +1,354 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+package ports
4+
5+import (
6+ "context"
7+ "errors"
8+ "time"
9+
10+ "bnema.dev/ermite/internal/domain"
11+)
12+
13+// CIRunDraft is what CreateRun stores: a run with its source, its jobs and
14+// their checks, built once the store has chosen the ids.
15+type CIRunDraft struct {
16+ Run domain.CIRun
17+ Source domain.CIRunSource
18+ Jobs []domain.CIJob
19+ Checks []domain.CICheck
20+}
21+
22+// CIWrite is a set of records written in one atomic batch. A record with
23+// Version 0 must not exist yet; any other must be unchanged since it was read.
24+// Apply returns domain.ErrConflict otherwise and domain.ErrNotFound when the
25+// repository no longer exists.
26+type CIWrite struct {
27+ Runs []domain.CIRun
28+ Jobs []domain.CIJob
29+ Checks []domain.CICheck
30+}
31+
32+// CIStore keeps CI runs, jobs and checks. The store derives its own indexes
33+// (the queue of ready jobs, the active jobs, the open runs, the job tokens)
34+// from the records it writes.
35+type CIStore interface {
36+ // CreateRun takes the next run id and the next number of the workflow in
37+ // the repository (both never reused), calls build with them and stores
38+ // the draft it returns, in one batch that fails with domain.ErrNotFound
39+ // once the repository is deleted. The records come back with their
40+ // versions.
41+ CreateRun(ctx context.Context, repo domain.RepoName, workflowPath string, build func(id, number int64) CIRunDraft) (CIRunDraft, error)
42+ // Apply writes the records in one batch and returns the version they all
43+ // have afterwards.
44+ Apply(ctx context.Context, w CIWrite) (version uint64, err error)
45+ // GetRun returns the run, or domain.ErrNotFound.
46+ GetRun(ctx context.Context, repo domain.RepoName, id int64) (domain.CIRun, error)
47+ // GetRunSource returns what the run was planned from, or
48+ // domain.ErrNotFound. It is how a run is found from its id alone.
49+ GetRunSource(ctx context.Context, runID int64) (domain.CIRunSource, error)
50+ // ListRuns returns the runs of the repository with an id below before (0
51+ // for no bound), newest first, at most limit.
52+ ListRuns(ctx context.Context, repo domain.RepoName, before int64, limit int) ([]domain.CIRun, error)
53+ // RunIDs returns the ids of the runs of the repository.
54+ RunIDs(ctx context.Context, repo domain.RepoName) ([]int64, error)
55+ // ListOpenRuns returns the runs that have not completed.
56+ ListOpenRuns(ctx context.Context) ([]domain.CIRun, error)
57+ // GetJob returns a job of a run at an attempt of the run, or
58+ // domain.ErrNotFound.
59+ GetJob(ctx context.Context, runID int64, runAttempt int32, jobID string) (domain.CIJob, error)
60+ // ListJobs returns the jobs of a run at an attempt, ordered by index then
61+ // combination.
62+ ListJobs(ctx context.Context, runID int64, runAttempt int32) ([]domain.CIJob, error)
63+ // FindJobByToken returns the job whose current token has that hash and
64+ // whose token is still valid at now, or domain.ErrNotFound.
65+ FindJobByToken(ctx context.Context, hash domain.TokenHash, now time.Time) (domain.CIJob, error)
66+ // ListReadyJobs returns the jobs that wait for a runner, oldest first, at
67+ // most limit.
68+ ListReadyJobs(ctx context.Context, limit int) ([]domain.CIJob, error)
69+ // ListActiveJobs returns the jobs given to a runner and not over.
70+ ListActiveJobs(ctx context.Context) ([]domain.CIJob, error)
71+ // ListChecks returns the checks of a commit, at most limit; more is true
72+ // when there are more.
73+ ListChecks(ctx context.Context, repo domain.RepoName, sha string, limit int) (checks []domain.CICheck, more bool, err error)
74+ // Sweep removes the runs completed before the time, with their jobs,
75+ // checks and sources (not their numbers), and returns their ids so that
76+ // the logs can go too.
77+ Sweep(ctx context.Context, before time.Time) ([]int64, error)
78+ // CreateRunner stores the runner and the index of its token. A name in use
79+ // is domain.ErrAlreadyExists, or with replace the old runner is removed in
80+ // the same batch and returned.
81+ CreateRunner(ctx context.Context, r domain.CIRunner, replace bool) (removed []domain.CIRunner, err error)
82+ // GetRunner returns the runner, or domain.ErrNotFound.
83+ GetRunner(ctx context.Context, id string) (domain.CIRunner, error)
84+ // FindRunnerByToken returns the runner whose token has that hash, or
85+ // domain.ErrNotFound.
86+ FindRunnerByToken(ctx context.Context, hash domain.TokenHash) (domain.CIRunner, error)
87+ // ListRunners returns every runner ordered by name.
88+ ListRunners(ctx context.Context) ([]domain.CIRunner, error)
89+ // RemoveRunner removes the runner and its token, or returns
90+ // domain.ErrNotFound.
91+ RemoveRunner(ctx context.Context, id string) (domain.CIRunner, error)
92+}
93+
94+// CIChecksReader is the read side of the checks, for the merge rule.
95+type CIChecksReader interface {
96+ ListChecks(ctx context.Context, repo domain.RepoName, sha string, limit int) (checks []domain.CICheck, more bool, err error)
97+}
98+
99+// CILogRef names the log of one execution of a job.
100+type CILogRef struct {
101+ RunID int64
102+ JobID string
103+ Attempt int32
104+}
105+
106+// ErrCILogLimit is returned by CILogStore.Append when the lines do not fit in
107+// the log limit; nothing was appended.
108+var ErrCILogLimit = errors.New("log limit reached")
109+
110+// ErrCILogGap wraps domain.ErrOutOfRange when an offset is above the count.
111+type ErrCILogGap struct{ Count int64 }
112+
113+// Error implements error.
114+func (e *ErrCILogGap) Error() string {
115+ return "out of range: the log holds lines up to the offset of " + itoa(e.Count)
116+}
117+
118+// Is makes errors.Is(err, domain.ErrOutOfRange) true.
119+func (e *ErrCILogGap) Is(target error) bool { return target == domain.ErrOutOfRange }
120+
121+func itoa(n int64) string {
122+ if n == 0 {
123+ return "0"
124+ }
125+ var b [20]byte
126+ i := len(b)
127+ for n > 0 {
128+ i--
129+ b[i] = byte('0' + n%10)
130+ n /= 10
131+ }
132+ return string(b[i:])
133+}
134+
135+// CILogStore keeps the logs of the jobs: append-only files, one line per log
136+// line, never in the metadata store. Lines are numbered from 0.
137+type CILogStore interface {
138+ // Append appends the lines at offset (the number of the first line), with
139+ // the idempotence of AppendLog: lines already stored are skipped, a gap is
140+ // an *ErrCILogGap. It returns the number of lines after the call and how
141+ // many of the request were new. A log that would exceed limit bytes gets
142+ // nothing and ErrCILogLimit.
143+ Append(ctx context.Context, ref CILogRef, offset int64, lines []domain.CILogLine, limit int64) (next int64, added int, err error)
144+ // AppendForced appends lines whatever the limit (the last line of a log
145+ // that reached it).
146+ AppendForced(ctx context.Context, ref CILogRef, lines []domain.CILogLine) (next int64, err error)
147+ // Count returns the number of lines held, 0 for a log that does not exist.
148+ Count(ctx context.Context, ref CILogRef) (int64, error)
149+ // Read returns lines from the one numbered from, at most maxLines lines and
150+ // about maxBytes of text. It returns domain.ErrNotFound when the log does
151+ // not exist and domain.ErrOutOfRange when from is above the count.
152+ Read(ctx context.Context, ref CILogRef, from int64, maxLines, maxBytes int) ([]domain.CILogLine, error)
153+ // Changed returns a channel that is closed at the next append to the log.
154+ Changed(ref CILogRef) <-chan struct{}
155+ // Reset empties the log.
156+ Reset(ctx context.Context, ref CILogRef) error
157+ // RemoveRuns deletes the logs of the runs.
158+ RemoveRuns(ctx context.Context, runIDs []int64) error
159+ // RunIDs lists the runs that have a log directory, for the orphan sweep.
160+ RunIDs(ctx context.Context) ([]int64, error)
161+}
162+
163+// WorkflowTrigger starts the workflows that match pushed refs. OnPush only
164+// queues the work and returns: planning happens in the background and its
165+// errors never reach the push.
166+type WorkflowTrigger interface {
167+ OnPush(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, pusher domain.Principal)
168+}
169+
170+// CIGit is what planning needs from Git beyond RepoReader and MergeGit.
171+type CIGit interface {
172+ // ChangedPaths returns the paths changed from base to head (a zero base
173+ // means the empty tree), in the order of the diff and without renames
174+ // detection, at most max; truncated is true when there were more.
175+ ChangedPaths(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, max int) (paths []string, truncated bool, err error)
176+}
177+
178+// --- inbound ---
179+
180+// RegisterRunnerParams describe a runner to register.
181+type RegisterRunnerParams struct {
182+ Name string
183+ Labels []string
184+ Version string
185+ Isolation domain.CIIsolation
186+ Arch, OS string
187+ Replace bool
188+}
189+
190+// RegisteredRunner is a new runner with its secret, shown once.
191+type RegisteredRunner struct {
192+ Runner domain.CIRunner
193+ Token string
194+}
195+
196+// RunnerJob is a job given to a runner.
197+type RunnerJob struct {
198+ Job domain.CIJob
199+ Token string
200+ Repo domain.RepoName
201+ Sha, Ref string
202+ WorkflowPath string
203+ WorkflowYAML string
204+}
205+
206+// FetchJobParams selects the job a runner asks for.
207+type FetchJobParams struct {
208+ Labels []string
209+ PollID string
210+}
211+
212+// FetchJobResult is the answer of a poll.
213+type FetchJobResult struct {
214+ Job *RunnerJob
215+ // RetryAfter is how long the runner waits before polling again.
216+ RetryAfter time.Duration
217+}
218+
219+// StepUpdate is the state of one step as a runner reports it.
220+type StepUpdate struct {
221+ Number int32
222+ Name string
223+ State domain.CIStepState
224+ Conclusion domain.CIConclusion
225+ StartedAt time.Time
226+ CompletedAt time.Time
227+}
228+
229+// CompleteJobParams end a job.
230+type CompleteJobParams struct {
231+ JobID string
232+ Conclusion domain.CIConclusion
233+ Outputs map[string]string
234+ RunnerShutdown bool
235+ Steps []StepUpdate
236+}
237+
238+// CompleteJobResult is the stored result of a job.
239+type CompleteJobResult struct {
240+ Conclusion domain.CIConclusion
241+ Error string
242+ Applied bool
243+}
244+
245+// JobSignal is one message of WatchJob.
246+type JobSignal struct {
247+ Keepalive bool
248+ // Cancel is set for a cancel request.
249+ Cancel *JobCancel
250+ // Ended is set when the job has ended.
251+ Ended *domain.JobEndedError
252+}
253+
254+// JobCancel is a cancel request for a running job.
255+type JobCancel struct {
256+ Reason domain.CICancelReason
257+ Message string
258+ RequestedAt time.Time
259+}
260+
261+// RunnerService is what runners call. The secret is the bearer secret of the
262+// call: the runner token for FetchJob, the job token for the other calls.
263+// A bad secret is domain.ErrUnauthenticated; a job token of another job than
264+// the one named is domain.ErrForbidden. *usecase.CIRunner implements it.
265+type RunnerService interface {
266+ Register(ctx context.Context, p domain.Principal, params RegisterRunnerParams) (RegisteredRunner, error)
267+ FetchJob(ctx context.Context, secret string, params FetchJobParams) (FetchJobResult, error)
268+ UpdateJob(ctx context.Context, secret, jobID string, steps []StepUpdate) error
269+ AppendLog(ctx context.Context, secret, jobID string, step int32, offset int64, lines []domain.CILogLine) (next int64, err error)
270+ Heartbeat(ctx context.Context, secret, jobID string) error
271+ WatchJob(ctx context.Context, secret, jobID string, send func(JobSignal) error) error
272+ CompleteJob(ctx context.Context, secret string, params CompleteJobParams) (CompleteJobResult, error)
273+}
274+
275+// RunStep is a step with its place in the log.
276+type RunStep struct {
277+ domain.CIStep
278+ LogEnd int64
279+}
280+
281+// JobView is a job as owners see it.
282+type JobView struct {
283+ domain.CIJob
284+ // Lines is the number of lines of its log now.
285+ Lines int64
286+ // RunnerGone is set when the runner of the job was removed since.
287+ RunnerGone bool
288+}
289+
290+// Steps returns the steps with their log ranges.
291+func (v JobView) StepRanges() []domain.StepRange { return domain.StepRanges(v.Steps, v.Lines) }
292+
293+// ListRunsParams select a page of runs.
294+type ListRunsParams struct {
295+ PageSize int
296+ PageToken string
297+}
298+
299+// RunPage is a page of runs, newest first.
300+type RunPage struct {
301+ Runs []domain.CIRun
302+ NextPageToken string
303+}
304+
305+// RunDetail is a run with its jobs.
306+type RunDetail struct {
307+ Run domain.CIRun
308+ Jobs []JobView
309+ LatestAttempt int32
310+}
311+
312+// LogBatch is a message of a log stream: consecutive lines from Offset.
313+type LogBatch struct {
314+ Offset int64
315+ Lines []JobLogLine
316+}
317+
318+// JobLogLine is a log line with its step.
319+type JobLogLine struct {
320+ Step int32
321+ domain.CILogLine
322+}
323+
324+// ChecksResult are the checks of a commit.
325+type ChecksResult struct {
326+ Checks []domain.CICheck
327+ Truncated bool
328+}
329+
330+// RunnerPage is a page of runners.
331+type RunnerPage struct {
332+ Runners []RunnerView
333+ NextPageToken string
334+}
335+
336+// RunnerView is a runner with what is known of it now.
337+type RunnerView struct {
338+ domain.CIRunner
339+ LastSeen time.Time
340+ Busy bool
341+}
342+
343+// RunService is the owner side of CI. *usecase.CIRuns implements it.
344+type RunService interface {
345+ ListRuns(ctx context.Context, p domain.Principal, repo domain.RepoName, params ListRunsParams) (RunPage, error)
346+ GetRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32) (RunDetail, error)
347+ GetJob(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32) (JobView, error)
348+ GetJobLog(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32, offset int64, follow bool, send func(LogBatch) error) error
349+ CancelRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64) (domain.CIRun, error)
350+ RerunRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, failedOnly bool) (domain.CIRun, error)
351+ ListChecks(ctx context.Context, p domain.Principal, repo domain.RepoName, sha string) (ChecksResult, error)
352+ ListRunners(ctx context.Context, p domain.Principal, size int, token string) (RunnerPage, error)
353+ RemoveRunner(ctx context.Context, p domain.Principal, id string) error
354+}
A internal/ports/mocks/CIChecksReader.go +134 -0
@@ -0,0 +1,134 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewCIChecksReader creates a new instance of CIChecksReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewCIChecksReader(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *CIChecksReader {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &CIChecksReader{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// CIChecksReader is an autogenerated mock type for the CIChecksReader type
38+type CIChecksReader struct {
39+ mock.Mock
40+}
41+
42+type CIChecksReader_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *CIChecksReader) EXPECT() *CIChecksReader_Expecter {
47+ return &CIChecksReader_Expecter{mock: &_m.Mock}
48+}
49+
50+// ListChecks provides a mock function for the type CIChecksReader
51+func (_mock *CIChecksReader) ListChecks(ctx context.Context, repo domain.RepoName, sha string, limit int) ([]domain.CICheck, bool, error) {
52+ ret := _mock.Called(ctx, repo, sha, limit)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for ListChecks")
56+ }
57+
58+ var r0 []domain.CICheck
59+ var r1 bool
60+ var r2 error
61+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string, int) ([]domain.CICheck, bool, error)); ok {
62+ return returnFunc(ctx, repo, sha, limit)
63+ }
64+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string, int) []domain.CICheck); ok {
65+ r0 = returnFunc(ctx, repo, sha, limit)
66+ } else {
67+ if ret.Get(0) != nil {
68+ r0 = ret.Get(0).([]domain.CICheck)
69+ }
70+ }
71+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, string, int) bool); ok {
72+ r1 = returnFunc(ctx, repo, sha, limit)
73+ } else {
74+ r1 = ret.Get(1).(bool)
75+ }
76+ if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName, string, int) error); ok {
77+ r2 = returnFunc(ctx, repo, sha, limit)
78+ } else {
79+ r2 = ret.Error(2)
80+ }
81+ return r0, r1, r2
82+}
83+
84+// CIChecksReader_ListChecks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListChecks'
85+type CIChecksReader_ListChecks_Call struct {
86+ *mock.Call
87+}
88+
89+// ListChecks is a helper method to define mock.On call
90+// - ctx context.Context
91+// - repo domain.RepoName
92+// - sha string
93+// - limit int
94+func (_e *CIChecksReader_Expecter) ListChecks(ctx any, repo any, sha any, limit any) *CIChecksReader_ListChecks_Call {
95+ return &CIChecksReader_ListChecks_Call{Call: _e.mock.On("ListChecks", ctx, repo, sha, limit)}
96+}
97+
98+func (_c *CIChecksReader_ListChecks_Call) Run(run func(ctx context.Context, repo domain.RepoName, sha string, limit int)) *CIChecksReader_ListChecks_Call {
99+ _c.Call.Run(func(args mock.Arguments) {
100+ var arg0 context.Context
101+ if args[0] != nil {
102+ arg0 = args[0].(context.Context)
103+ }
104+ var arg1 domain.RepoName
105+ if args[1] != nil {
106+ arg1 = args[1].(domain.RepoName)
107+ }
108+ var arg2 string
109+ if args[2] != nil {
110+ arg2 = args[2].(string)
111+ }
112+ var arg3 int
113+ if args[3] != nil {
114+ arg3 = args[3].(int)
115+ }
116+ run(
117+ arg0,
118+ arg1,
119+ arg2,
120+ arg3,
121+ )
122+ })
123+ return _c
124+}
125+
126+func (_c *CIChecksReader_ListChecks_Call) Return(checks []domain.CICheck, more bool, err error) *CIChecksReader_ListChecks_Call {
127+ _c.Call.Return(checks, more, err)
128+ return _c
129+}
130+
131+func (_c *CIChecksReader_ListChecks_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, sha string, limit int) ([]domain.CICheck, bool, error)) *CIChecksReader_ListChecks_Call {
132+ _c.Call.Return(run)
133+ return _c
134+}
A internal/ports/mocks/CIGit.go +140 -0
@@ -0,0 +1,140 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewCIGit creates a new instance of CIGit. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewCIGit(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *CIGit {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &CIGit{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// CIGit is an autogenerated mock type for the CIGit type
38+type CIGit struct {
39+ mock.Mock
40+}
41+
42+type CIGit_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *CIGit) EXPECT() *CIGit_Expecter {
47+ return &CIGit_Expecter{mock: &_m.Mock}
48+}
49+
50+// ChangedPaths provides a mock function for the type CIGit
51+func (_mock *CIGit) ChangedPaths(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, max int) ([]string, bool, error) {
52+ ret := _mock.Called(ctx, repo, base, head, max)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for ChangedPaths")
56+ }
57+
58+ var r0 []string
59+ var r1 bool
60+ var r2 error
61+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) ([]string, bool, error)); ok {
62+ return returnFunc(ctx, repo, base, head, max)
63+ }
64+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) []string); ok {
65+ r0 = returnFunc(ctx, repo, base, head, max)
66+ } else {
67+ if ret.Get(0) != nil {
68+ r0 = ret.Get(0).([]string)
69+ }
70+ }
71+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) bool); ok {
72+ r1 = returnFunc(ctx, repo, base, head, max)
73+ } else {
74+ r1 = ret.Get(1).(bool)
75+ }
76+ if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName, domain.ObjectID, domain.ObjectID, int) error); ok {
77+ r2 = returnFunc(ctx, repo, base, head, max)
78+ } else {
79+ r2 = ret.Error(2)
80+ }
81+ return r0, r1, r2
82+}
83+
84+// CIGit_ChangedPaths_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChangedPaths'
85+type CIGit_ChangedPaths_Call struct {
86+ *mock.Call
87+}
88+
89+// ChangedPaths is a helper method to define mock.On call
90+// - ctx context.Context
91+// - repo domain.RepoName
92+// - base domain.ObjectID
93+// - head domain.ObjectID
94+// - max int
95+func (_e *CIGit_Expecter) ChangedPaths(ctx any, repo any, base any, head any, max any) *CIGit_ChangedPaths_Call {
96+ return &CIGit_ChangedPaths_Call{Call: _e.mock.On("ChangedPaths", ctx, repo, base, head, max)}
97+}
98+
99+func (_c *CIGit_ChangedPaths_Call) Run(run func(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, max int)) *CIGit_ChangedPaths_Call {
100+ _c.Call.Run(func(args mock.Arguments) {
101+ var arg0 context.Context
102+ if args[0] != nil {
103+ arg0 = args[0].(context.Context)
104+ }
105+ var arg1 domain.RepoName
106+ if args[1] != nil {
107+ arg1 = args[1].(domain.RepoName)
108+ }
109+ var arg2 domain.ObjectID
110+ if args[2] != nil {
111+ arg2 = args[2].(domain.ObjectID)
112+ }
113+ var arg3 domain.ObjectID
114+ if args[3] != nil {
115+ arg3 = args[3].(domain.ObjectID)
116+ }
117+ var arg4 int
118+ if args[4] != nil {
119+ arg4 = args[4].(int)
120+ }
121+ run(
122+ arg0,
123+ arg1,
124+ arg2,
125+ arg3,
126+ arg4,
127+ )
128+ })
129+ return _c
130+}
131+
132+func (_c *CIGit_ChangedPaths_Call) Return(paths []string, truncated bool, err error) *CIGit_ChangedPaths_Call {
133+ _c.Call.Return(paths, truncated, err)
134+ return _c
135+}
136+
137+func (_c *CIGit_ChangedPaths_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, base domain.ObjectID, head domain.ObjectID, max int) ([]string, bool, error)) *CIGit_ChangedPaths_Call {
138+ _c.Call.Return(run)
139+ return _c
140+}
A internal/ports/mocks/CILogStore.go +592 -0
@@ -0,0 +1,592 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+ mock "github.com/stretchr/testify/mock"
13+)
14+
15+// NewCILogStore creates a new instance of CILogStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
16+// The first argument is typically a *testing.T value.
17+func NewCILogStore(t interface {
18+ mock.TestingT
19+ Cleanup(func())
20+}) *CILogStore {
21+ if helper, ok := t.(interface{ Helper() }); ok {
22+ helper.Helper()
23+ }
24+
25+ mock := &CILogStore{}
26+ mock.Mock.Test(t)
27+
28+ t.Cleanup(func() {
29+ if helper, ok := t.(interface{ Helper() }); ok {
30+ helper.Helper()
31+ }
32+ mock.AssertExpectations(t)
33+ })
34+
35+ return mock
36+}
37+
38+// CILogStore is an autogenerated mock type for the CILogStore type
39+type CILogStore struct {
40+ mock.Mock
41+}
42+
43+type CILogStore_Expecter struct {
44+ mock *mock.Mock
45+}
46+
47+func (_m *CILogStore) EXPECT() *CILogStore_Expecter {
48+ return &CILogStore_Expecter{mock: &_m.Mock}
49+}
50+
51+// Append provides a mock function for the type CILogStore
52+func (_mock *CILogStore) Append(ctx context.Context, ref ports.CILogRef, offset int64, lines []domain.CILogLine, limit int64) (int64, int, error) {
53+ ret := _mock.Called(ctx, ref, offset, lines, limit)
54+
55+ if len(ret) == 0 {
56+ panic("no return value specified for Append")
57+ }
58+
59+ var r0 int64
60+ var r1 int
61+ var r2 error
62+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.CILogRef, int64, []domain.CILogLine, int64) (int64, int, error)); ok {
63+ return returnFunc(ctx, ref, offset, lines, limit)
64+ }
65+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.CILogRef, int64, []domain.CILogLine, int64) int64); ok {
66+ r0 = returnFunc(ctx, ref, offset, lines, limit)
67+ } else {
68+ r0 = ret.Get(0).(int64)
69+ }
70+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.CILogRef, int64, []domain.CILogLine, int64) int); ok {
71+ r1 = returnFunc(ctx, ref, offset, lines, limit)
72+ } else {
73+ r1 = ret.Get(1).(int)
74+ }
75+ if returnFunc, ok := ret.Get(2).(func(context.Context, ports.CILogRef, int64, []domain.CILogLine, int64) error); ok {
76+ r2 = returnFunc(ctx, ref, offset, lines, limit)
77+ } else {
78+ r2 = ret.Error(2)
79+ }
80+ return r0, r1, r2
81+}
82+
83+// CILogStore_Append_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Append'
84+type CILogStore_Append_Call struct {
85+ *mock.Call
86+}
87+
88+// Append is a helper method to define mock.On call
89+// - ctx context.Context
90+// - ref ports.CILogRef
91+// - offset int64
92+// - lines []domain.CILogLine
93+// - limit int64
94+func (_e *CILogStore_Expecter) Append(ctx any, ref any, offset any, lines any, limit any) *CILogStore_Append_Call {
95+ return &CILogStore_Append_Call{Call: _e.mock.On("Append", ctx, ref, offset, lines, limit)}
96+}
97+
98+func (_c *CILogStore_Append_Call) Run(run func(ctx context.Context, ref ports.CILogRef, offset int64, lines []domain.CILogLine, limit int64)) *CILogStore_Append_Call {
99+ _c.Call.Run(func(args mock.Arguments) {
100+ var arg0 context.Context
101+ if args[0] != nil {
102+ arg0 = args[0].(context.Context)
103+ }
104+ var arg1 ports.CILogRef
105+ if args[1] != nil {
106+ arg1 = args[1].(ports.CILogRef)
107+ }
108+ var arg2 int64
109+ if args[2] != nil {
110+ arg2 = args[2].(int64)
111+ }
112+ var arg3 []domain.CILogLine
113+ if args[3] != nil {
114+ arg3 = args[3].([]domain.CILogLine)
115+ }
116+ var arg4 int64
117+ if args[4] != nil {
118+ arg4 = args[4].(int64)
119+ }
120+ run(
121+ arg0,
122+ arg1,
123+ arg2,
124+ arg3,
125+ arg4,
126+ )
127+ })
128+ return _c
129+}
130+
131+func (_c *CILogStore_Append_Call) Return(next int64, added int, err error) *CILogStore_Append_Call {
132+ _c.Call.Return(next, added, err)
133+ return _c
134+}
135+
136+func (_c *CILogStore_Append_Call) RunAndReturn(run func(ctx context.Context, ref ports.CILogRef, offset int64, lines []domain.CILogLine, limit int64) (int64, int, error)) *CILogStore_Append_Call {
137+ _c.Call.Return(run)
138+ return _c
139+}
140+
141+// AppendForced provides a mock function for the type CILogStore
142+func (_mock *CILogStore) AppendForced(ctx context.Context, ref ports.CILogRef, lines []domain.CILogLine) (int64, error) {
143+ ret := _mock.Called(ctx, ref, lines)
144+
145+ if len(ret) == 0 {
146+ panic("no return value specified for AppendForced")
147+ }
148+
149+ var r0 int64
150+ var r1 error
151+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.CILogRef, []domain.CILogLine) (int64, error)); ok {
152+ return returnFunc(ctx, ref, lines)
153+ }
154+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.CILogRef, []domain.CILogLine) int64); ok {
155+ r0 = returnFunc(ctx, ref, lines)
156+ } else {
157+ r0 = ret.Get(0).(int64)
158+ }
159+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.CILogRef, []domain.CILogLine) error); ok {
160+ r1 = returnFunc(ctx, ref, lines)
161+ } else {
162+ r1 = ret.Error(1)
163+ }
164+ return r0, r1
165+}
166+
167+// CILogStore_AppendForced_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppendForced'
168+type CILogStore_AppendForced_Call struct {
169+ *mock.Call
170+}
171+
172+// AppendForced is a helper method to define mock.On call
173+// - ctx context.Context
174+// - ref ports.CILogRef
175+// - lines []domain.CILogLine
176+func (_e *CILogStore_Expecter) AppendForced(ctx any, ref any, lines any) *CILogStore_AppendForced_Call {
177+ return &CILogStore_AppendForced_Call{Call: _e.mock.On("AppendForced", ctx, ref, lines)}
178+}
179+
180+func (_c *CILogStore_AppendForced_Call) Run(run func(ctx context.Context, ref ports.CILogRef, lines []domain.CILogLine)) *CILogStore_AppendForced_Call {
181+ _c.Call.Run(func(args mock.Arguments) {
182+ var arg0 context.Context
183+ if args[0] != nil {
184+ arg0 = args[0].(context.Context)
185+ }
186+ var arg1 ports.CILogRef
187+ if args[1] != nil {
188+ arg1 = args[1].(ports.CILogRef)
189+ }
190+ var arg2 []domain.CILogLine
191+ if args[2] != nil {
192+ arg2 = args[2].([]domain.CILogLine)
193+ }
194+ run(
195+ arg0,
196+ arg1,
197+ arg2,
198+ )
199+ })
200+ return _c
201+}
202+
203+func (_c *CILogStore_AppendForced_Call) Return(next int64, err error) *CILogStore_AppendForced_Call {
204+ _c.Call.Return(next, err)
205+ return _c
206+}
207+
208+func (_c *CILogStore_AppendForced_Call) RunAndReturn(run func(ctx context.Context, ref ports.CILogRef, lines []domain.CILogLine) (int64, error)) *CILogStore_AppendForced_Call {
209+ _c.Call.Return(run)
210+ return _c
211+}
212+
213+// Changed provides a mock function for the type CILogStore
214+func (_mock *CILogStore) Changed(ref ports.CILogRef) <-chan struct{} {
215+ ret := _mock.Called(ref)
216+
217+ if len(ret) == 0 {
218+ panic("no return value specified for Changed")
219+ }
220+
221+ var r0 <-chan struct{}
222+ if returnFunc, ok := ret.Get(0).(func(ports.CILogRef) <-chan struct{}); ok {
223+ r0 = returnFunc(ref)
224+ } else {
225+ if ret.Get(0) != nil {
226+ r0 = ret.Get(0).(<-chan struct{})
227+ }
228+ }
229+ return r0
230+}
231+
232+// CILogStore_Changed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Changed'
233+type CILogStore_Changed_Call struct {
234+ *mock.Call
235+}
236+
237+// Changed is a helper method to define mock.On call
238+// - ref ports.CILogRef
239+func (_e *CILogStore_Expecter) Changed(ref any) *CILogStore_Changed_Call {
240+ return &CILogStore_Changed_Call{Call: _e.mock.On("Changed", ref)}
241+}
242+
243+func (_c *CILogStore_Changed_Call) Run(run func(ref ports.CILogRef)) *CILogStore_Changed_Call {
244+ _c.Call.Run(func(args mock.Arguments) {
245+ var arg0 ports.CILogRef
246+ if args[0] != nil {
247+ arg0 = args[0].(ports.CILogRef)
248+ }
249+ run(
250+ arg0,
251+ )
252+ })
253+ return _c
254+}
255+
256+func (_c *CILogStore_Changed_Call) Return(valCh <-chan struct{}) *CILogStore_Changed_Call {
257+ _c.Call.Return(valCh)
258+ return _c
259+}
260+
261+func (_c *CILogStore_Changed_Call) RunAndReturn(run func(ref ports.CILogRef) <-chan struct{}) *CILogStore_Changed_Call {
262+ _c.Call.Return(run)
263+ return _c
264+}
265+
266+// Count provides a mock function for the type CILogStore
267+func (_mock *CILogStore) Count(ctx context.Context, ref ports.CILogRef) (int64, error) {
268+ ret := _mock.Called(ctx, ref)
269+
270+ if len(ret) == 0 {
271+ panic("no return value specified for Count")
272+ }
273+
274+ var r0 int64
275+ var r1 error
276+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.CILogRef) (int64, error)); ok {
277+ return returnFunc(ctx, ref)
278+ }
279+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.CILogRef) int64); ok {
280+ r0 = returnFunc(ctx, ref)
281+ } else {
282+ r0 = ret.Get(0).(int64)
283+ }
284+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.CILogRef) error); ok {
285+ r1 = returnFunc(ctx, ref)
286+ } else {
287+ r1 = ret.Error(1)
288+ }
289+ return r0, r1
290+}
291+
292+// CILogStore_Count_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Count'
293+type CILogStore_Count_Call struct {
294+ *mock.Call
295+}
296+
297+// Count is a helper method to define mock.On call
298+// - ctx context.Context
299+// - ref ports.CILogRef
300+func (_e *CILogStore_Expecter) Count(ctx any, ref any) *CILogStore_Count_Call {
301+ return &CILogStore_Count_Call{Call: _e.mock.On("Count", ctx, ref)}
302+}
303+
304+func (_c *CILogStore_Count_Call) Run(run func(ctx context.Context, ref ports.CILogRef)) *CILogStore_Count_Call {
305+ _c.Call.Run(func(args mock.Arguments) {
306+ var arg0 context.Context
307+ if args[0] != nil {
308+ arg0 = args[0].(context.Context)
309+ }
310+ var arg1 ports.CILogRef
311+ if args[1] != nil {
312+ arg1 = args[1].(ports.CILogRef)
313+ }
314+ run(
315+ arg0,
316+ arg1,
317+ )
318+ })
319+ return _c
320+}
321+
322+func (_c *CILogStore_Count_Call) Return(n int64, err error) *CILogStore_Count_Call {
323+ _c.Call.Return(n, err)
324+ return _c
325+}
326+
327+func (_c *CILogStore_Count_Call) RunAndReturn(run func(ctx context.Context, ref ports.CILogRef) (int64, error)) *CILogStore_Count_Call {
328+ _c.Call.Return(run)
329+ return _c
330+}
331+
332+// Read provides a mock function for the type CILogStore
333+func (_mock *CILogStore) Read(ctx context.Context, ref ports.CILogRef, from int64, maxLines int, maxBytes int) ([]domain.CILogLine, error) {
334+ ret := _mock.Called(ctx, ref, from, maxLines, maxBytes)
335+
336+ if len(ret) == 0 {
337+ panic("no return value specified for Read")
338+ }
339+
340+ var r0 []domain.CILogLine
341+ var r1 error
342+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.CILogRef, int64, int, int) ([]domain.CILogLine, error)); ok {
343+ return returnFunc(ctx, ref, from, maxLines, maxBytes)
344+ }
345+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.CILogRef, int64, int, int) []domain.CILogLine); ok {
346+ r0 = returnFunc(ctx, ref, from, maxLines, maxBytes)
347+ } else {
348+ if ret.Get(0) != nil {
349+ r0 = ret.Get(0).([]domain.CILogLine)
350+ }
351+ }
352+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.CILogRef, int64, int, int) error); ok {
353+ r1 = returnFunc(ctx, ref, from, maxLines, maxBytes)
354+ } else {
355+ r1 = ret.Error(1)
356+ }
357+ return r0, r1
358+}
359+
360+// CILogStore_Read_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Read'
361+type CILogStore_Read_Call struct {
362+ *mock.Call
363+}
364+
365+// Read is a helper method to define mock.On call
366+// - ctx context.Context
367+// - ref ports.CILogRef
368+// - from int64
369+// - maxLines int
370+// - maxBytes int
371+func (_e *CILogStore_Expecter) Read(ctx any, ref any, from any, maxLines any, maxBytes any) *CILogStore_Read_Call {
372+ return &CILogStore_Read_Call{Call: _e.mock.On("Read", ctx, ref, from, maxLines, maxBytes)}
373+}
374+
375+func (_c *CILogStore_Read_Call) Run(run func(ctx context.Context, ref ports.CILogRef, from int64, maxLines int, maxBytes int)) *CILogStore_Read_Call {
376+ _c.Call.Run(func(args mock.Arguments) {
377+ var arg0 context.Context
378+ if args[0] != nil {
379+ arg0 = args[0].(context.Context)
380+ }
381+ var arg1 ports.CILogRef
382+ if args[1] != nil {
383+ arg1 = args[1].(ports.CILogRef)
384+ }
385+ var arg2 int64
386+ if args[2] != nil {
387+ arg2 = args[2].(int64)
388+ }
389+ var arg3 int
390+ if args[3] != nil {
391+ arg3 = args[3].(int)
392+ }
393+ var arg4 int
394+ if args[4] != nil {
395+ arg4 = args[4].(int)
396+ }
397+ run(
398+ arg0,
399+ arg1,
400+ arg2,
401+ arg3,
402+ arg4,
403+ )
404+ })
405+ return _c
406+}
407+
408+func (_c *CILogStore_Read_Call) Return(cILogLines []domain.CILogLine, err error) *CILogStore_Read_Call {
409+ _c.Call.Return(cILogLines, err)
410+ return _c
411+}
412+
413+func (_c *CILogStore_Read_Call) RunAndReturn(run func(ctx context.Context, ref ports.CILogRef, from int64, maxLines int, maxBytes int) ([]domain.CILogLine, error)) *CILogStore_Read_Call {
414+ _c.Call.Return(run)
415+ return _c
416+}
417+
418+// RemoveRuns provides a mock function for the type CILogStore
419+func (_mock *CILogStore) RemoveRuns(ctx context.Context, runIDs []int64) error {
420+ ret := _mock.Called(ctx, runIDs)
421+
422+ if len(ret) == 0 {
423+ panic("no return value specified for RemoveRuns")
424+ }
425+
426+ var r0 error
427+ if returnFunc, ok := ret.Get(0).(func(context.Context, []int64) error); ok {
428+ r0 = returnFunc(ctx, runIDs)
429+ } else {
430+ r0 = ret.Error(0)
431+ }
432+ return r0
433+}
434+
435+// CILogStore_RemoveRuns_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveRuns'
436+type CILogStore_RemoveRuns_Call struct {
437+ *mock.Call
438+}
439+
440+// RemoveRuns is a helper method to define mock.On call
441+// - ctx context.Context
442+// - runIDs []int64
443+func (_e *CILogStore_Expecter) RemoveRuns(ctx any, runIDs any) *CILogStore_RemoveRuns_Call {
444+ return &CILogStore_RemoveRuns_Call{Call: _e.mock.On("RemoveRuns", ctx, runIDs)}
445+}
446+
447+func (_c *CILogStore_RemoveRuns_Call) Run(run func(ctx context.Context, runIDs []int64)) *CILogStore_RemoveRuns_Call {
448+ _c.Call.Run(func(args mock.Arguments) {
449+ var arg0 context.Context
450+ if args[0] != nil {
451+ arg0 = args[0].(context.Context)
452+ }
453+ var arg1 []int64
454+ if args[1] != nil {
455+ arg1 = args[1].([]int64)
456+ }
457+ run(
458+ arg0,
459+ arg1,
460+ )
461+ })
462+ return _c
463+}
464+
465+func (_c *CILogStore_RemoveRuns_Call) Return(err error) *CILogStore_RemoveRuns_Call {
466+ _c.Call.Return(err)
467+ return _c
468+}
469+
470+func (_c *CILogStore_RemoveRuns_Call) RunAndReturn(run func(ctx context.Context, runIDs []int64) error) *CILogStore_RemoveRuns_Call {
471+ _c.Call.Return(run)
472+ return _c
473+}
474+
475+// Reset provides a mock function for the type CILogStore
476+func (_mock *CILogStore) Reset(ctx context.Context, ref ports.CILogRef) error {
477+ ret := _mock.Called(ctx, ref)
478+
479+ if len(ret) == 0 {
480+ panic("no return value specified for Reset")
481+ }
482+
483+ var r0 error
484+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.CILogRef) error); ok {
485+ r0 = returnFunc(ctx, ref)
486+ } else {
487+ r0 = ret.Error(0)
488+ }
489+ return r0
490+}
491+
492+// CILogStore_Reset_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Reset'
493+type CILogStore_Reset_Call struct {
494+ *mock.Call
495+}
496+
497+// Reset is a helper method to define mock.On call
498+// - ctx context.Context
499+// - ref ports.CILogRef
500+func (_e *CILogStore_Expecter) Reset(ctx any, ref any) *CILogStore_Reset_Call {
501+ return &CILogStore_Reset_Call{Call: _e.mock.On("Reset", ctx, ref)}
502+}
503+
504+func (_c *CILogStore_Reset_Call) Run(run func(ctx context.Context, ref ports.CILogRef)) *CILogStore_Reset_Call {
505+ _c.Call.Run(func(args mock.Arguments) {
506+ var arg0 context.Context
507+ if args[0] != nil {
508+ arg0 = args[0].(context.Context)
509+ }
510+ var arg1 ports.CILogRef
511+ if args[1] != nil {
512+ arg1 = args[1].(ports.CILogRef)
513+ }
514+ run(
515+ arg0,
516+ arg1,
517+ )
518+ })
519+ return _c
520+}
521+
522+func (_c *CILogStore_Reset_Call) Return(err error) *CILogStore_Reset_Call {
523+ _c.Call.Return(err)
524+ return _c
525+}
526+
527+func (_c *CILogStore_Reset_Call) RunAndReturn(run func(ctx context.Context, ref ports.CILogRef) error) *CILogStore_Reset_Call {
528+ _c.Call.Return(run)
529+ return _c
530+}
531+
532+// RunIDs provides a mock function for the type CILogStore
533+func (_mock *CILogStore) RunIDs(ctx context.Context) ([]int64, error) {
534+ ret := _mock.Called(ctx)
535+
536+ if len(ret) == 0 {
537+ panic("no return value specified for RunIDs")
538+ }
539+
540+ var r0 []int64
541+ var r1 error
542+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]int64, error)); ok {
543+ return returnFunc(ctx)
544+ }
545+ if returnFunc, ok := ret.Get(0).(func(context.Context) []int64); ok {
546+ r0 = returnFunc(ctx)
547+ } else {
548+ if ret.Get(0) != nil {
549+ r0 = ret.Get(0).([]int64)
550+ }
551+ }
552+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
553+ r1 = returnFunc(ctx)
554+ } else {
555+ r1 = ret.Error(1)
556+ }
557+ return r0, r1
558+}
559+
560+// CILogStore_RunIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RunIDs'
561+type CILogStore_RunIDs_Call struct {
562+ *mock.Call
563+}
564+
565+// RunIDs is a helper method to define mock.On call
566+// - ctx context.Context
567+func (_e *CILogStore_Expecter) RunIDs(ctx any) *CILogStore_RunIDs_Call {
568+ return &CILogStore_RunIDs_Call{Call: _e.mock.On("RunIDs", ctx)}
569+}
570+
571+func (_c *CILogStore_RunIDs_Call) Run(run func(ctx context.Context)) *CILogStore_RunIDs_Call {
572+ _c.Call.Run(func(args mock.Arguments) {
573+ var arg0 context.Context
574+ if args[0] != nil {
575+ arg0 = args[0].(context.Context)
576+ }
577+ run(
578+ arg0,
579+ )
580+ })
581+ return _c
582+}
583+
584+func (_c *CILogStore_RunIDs_Call) Return(int64s []int64, err error) *CILogStore_RunIDs_Call {
585+ _c.Call.Return(int64s, err)
586+ return _c
587+}
588+
589+func (_c *CILogStore_RunIDs_Call) RunAndReturn(run func(ctx context.Context) ([]int64, error)) *CILogStore_RunIDs_Call {
590+ _c.Call.Return(run)
591+ return _c
592+}
A internal/ports/mocks/CIStore.go +1384 -0
@@ -0,0 +1,1384 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+ "time"
10+
11+ "bnema.dev/ermite/internal/domain"
12+ "bnema.dev/ermite/internal/ports"
13+ mock "github.com/stretchr/testify/mock"
14+)
15+
16+// NewCIStore creates a new instance of CIStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
17+// The first argument is typically a *testing.T value.
18+func NewCIStore(t interface {
19+ mock.TestingT
20+ Cleanup(func())
21+}) *CIStore {
22+ if helper, ok := t.(interface{ Helper() }); ok {
23+ helper.Helper()
24+ }
25+
26+ mock := &CIStore{}
27+ mock.Mock.Test(t)
28+
29+ t.Cleanup(func() {
30+ if helper, ok := t.(interface{ Helper() }); ok {
31+ helper.Helper()
32+ }
33+ mock.AssertExpectations(t)
34+ })
35+
36+ return mock
37+}
38+
39+// CIStore is an autogenerated mock type for the CIStore type
40+type CIStore struct {
41+ mock.Mock
42+}
43+
44+type CIStore_Expecter struct {
45+ mock *mock.Mock
46+}
47+
48+func (_m *CIStore) EXPECT() *CIStore_Expecter {
49+ return &CIStore_Expecter{mock: &_m.Mock}
50+}
51+
52+// Apply provides a mock function for the type CIStore
53+func (_mock *CIStore) Apply(ctx context.Context, w ports.CIWrite) (uint64, error) {
54+ ret := _mock.Called(ctx, w)
55+
56+ if len(ret) == 0 {
57+ panic("no return value specified for Apply")
58+ }
59+
60+ var r0 uint64
61+ var r1 error
62+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.CIWrite) (uint64, error)); ok {
63+ return returnFunc(ctx, w)
64+ }
65+ if returnFunc, ok := ret.Get(0).(func(context.Context, ports.CIWrite) uint64); ok {
66+ r0 = returnFunc(ctx, w)
67+ } else {
68+ r0 = ret.Get(0).(uint64)
69+ }
70+ if returnFunc, ok := ret.Get(1).(func(context.Context, ports.CIWrite) error); ok {
71+ r1 = returnFunc(ctx, w)
72+ } else {
73+ r1 = ret.Error(1)
74+ }
75+ return r0, r1
76+}
77+
78+// CIStore_Apply_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Apply'
79+type CIStore_Apply_Call struct {
80+ *mock.Call
81+}
82+
83+// Apply is a helper method to define mock.On call
84+// - ctx context.Context
85+// - w ports.CIWrite
86+func (_e *CIStore_Expecter) Apply(ctx any, w any) *CIStore_Apply_Call {
87+ return &CIStore_Apply_Call{Call: _e.mock.On("Apply", ctx, w)}
88+}
89+
90+func (_c *CIStore_Apply_Call) Run(run func(ctx context.Context, w ports.CIWrite)) *CIStore_Apply_Call {
91+ _c.Call.Run(func(args mock.Arguments) {
92+ var arg0 context.Context
93+ if args[0] != nil {
94+ arg0 = args[0].(context.Context)
95+ }
96+ var arg1 ports.CIWrite
97+ if args[1] != nil {
98+ arg1 = args[1].(ports.CIWrite)
99+ }
100+ run(
101+ arg0,
102+ arg1,
103+ )
104+ })
105+ return _c
106+}
107+
108+func (_c *CIStore_Apply_Call) Return(version uint64, err error) *CIStore_Apply_Call {
109+ _c.Call.Return(version, err)
110+ return _c
111+}
112+
113+func (_c *CIStore_Apply_Call) RunAndReturn(run func(ctx context.Context, w ports.CIWrite) (uint64, error)) *CIStore_Apply_Call {
114+ _c.Call.Return(run)
115+ return _c
116+}
117+
118+// CreateRun provides a mock function for the type CIStore
119+func (_mock *CIStore) CreateRun(ctx context.Context, repo domain.RepoName, workflowPath string, build func(id int64, number int64) ports.CIRunDraft) (ports.CIRunDraft, error) {
120+ ret := _mock.Called(ctx, repo, workflowPath, build)
121+
122+ if len(ret) == 0 {
123+ panic("no return value specified for CreateRun")
124+ }
125+
126+ var r0 ports.CIRunDraft
127+ var r1 error
128+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string, func(id int64, number int64) ports.CIRunDraft) (ports.CIRunDraft, error)); ok {
129+ return returnFunc(ctx, repo, workflowPath, build)
130+ }
131+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string, func(id int64, number int64) ports.CIRunDraft) ports.CIRunDraft); ok {
132+ r0 = returnFunc(ctx, repo, workflowPath, build)
133+ } else {
134+ r0 = ret.Get(0).(ports.CIRunDraft)
135+ }
136+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, string, func(id int64, number int64) ports.CIRunDraft) error); ok {
137+ r1 = returnFunc(ctx, repo, workflowPath, build)
138+ } else {
139+ r1 = ret.Error(1)
140+ }
141+ return r0, r1
142+}
143+
144+// CIStore_CreateRun_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRun'
145+type CIStore_CreateRun_Call struct {
146+ *mock.Call
147+}
148+
149+// CreateRun is a helper method to define mock.On call
150+// - ctx context.Context
151+// - repo domain.RepoName
152+// - workflowPath string
153+// - build func(id int64, number int64) ports.CIRunDraft
154+func (_e *CIStore_Expecter) CreateRun(ctx any, repo any, workflowPath any, build any) *CIStore_CreateRun_Call {
155+ return &CIStore_CreateRun_Call{Call: _e.mock.On("CreateRun", ctx, repo, workflowPath, build)}
156+}
157+
158+func (_c *CIStore_CreateRun_Call) Run(run func(ctx context.Context, repo domain.RepoName, workflowPath string, build func(id int64, number int64) ports.CIRunDraft)) *CIStore_CreateRun_Call {
159+ _c.Call.Run(func(args mock.Arguments) {
160+ var arg0 context.Context
161+ if args[0] != nil {
162+ arg0 = args[0].(context.Context)
163+ }
164+ var arg1 domain.RepoName
165+ if args[1] != nil {
166+ arg1 = args[1].(domain.RepoName)
167+ }
168+ var arg2 string
169+ if args[2] != nil {
170+ arg2 = args[2].(string)
171+ }
172+ var arg3 func(id int64, number int64) ports.CIRunDraft
173+ if args[3] != nil {
174+ arg3 = args[3].(func(id int64, number int64) ports.CIRunDraft)
175+ }
176+ run(
177+ arg0,
178+ arg1,
179+ arg2,
180+ arg3,
181+ )
182+ })
183+ return _c
184+}
185+
186+func (_c *CIStore_CreateRun_Call) Return(cIRunDraft ports.CIRunDraft, err error) *CIStore_CreateRun_Call {
187+ _c.Call.Return(cIRunDraft, err)
188+ return _c
189+}
190+
191+func (_c *CIStore_CreateRun_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, workflowPath string, build func(id int64, number int64) ports.CIRunDraft) (ports.CIRunDraft, error)) *CIStore_CreateRun_Call {
192+ _c.Call.Return(run)
193+ return _c
194+}
195+
196+// CreateRunner provides a mock function for the type CIStore
197+func (_mock *CIStore) CreateRunner(ctx context.Context, r domain.CIRunner, replace bool) ([]domain.CIRunner, error) {
198+ ret := _mock.Called(ctx, r, replace)
199+
200+ if len(ret) == 0 {
201+ panic("no return value specified for CreateRunner")
202+ }
203+
204+ var r0 []domain.CIRunner
205+ var r1 error
206+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.CIRunner, bool) ([]domain.CIRunner, error)); ok {
207+ return returnFunc(ctx, r, replace)
208+ }
209+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.CIRunner, bool) []domain.CIRunner); ok {
210+ r0 = returnFunc(ctx, r, replace)
211+ } else {
212+ if ret.Get(0) != nil {
213+ r0 = ret.Get(0).([]domain.CIRunner)
214+ }
215+ }
216+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.CIRunner, bool) error); ok {
217+ r1 = returnFunc(ctx, r, replace)
218+ } else {
219+ r1 = ret.Error(1)
220+ }
221+ return r0, r1
222+}
223+
224+// CIStore_CreateRunner_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRunner'
225+type CIStore_CreateRunner_Call struct {
226+ *mock.Call
227+}
228+
229+// CreateRunner is a helper method to define mock.On call
230+// - ctx context.Context
231+// - r domain.CIRunner
232+// - replace bool
233+func (_e *CIStore_Expecter) CreateRunner(ctx any, r any, replace any) *CIStore_CreateRunner_Call {
234+ return &CIStore_CreateRunner_Call{Call: _e.mock.On("CreateRunner", ctx, r, replace)}
235+}
236+
237+func (_c *CIStore_CreateRunner_Call) Run(run func(ctx context.Context, r domain.CIRunner, replace bool)) *CIStore_CreateRunner_Call {
238+ _c.Call.Run(func(args mock.Arguments) {
239+ var arg0 context.Context
240+ if args[0] != nil {
241+ arg0 = args[0].(context.Context)
242+ }
243+ var arg1 domain.CIRunner
244+ if args[1] != nil {
245+ arg1 = args[1].(domain.CIRunner)
246+ }
247+ var arg2 bool
248+ if args[2] != nil {
249+ arg2 = args[2].(bool)
250+ }
251+ run(
252+ arg0,
253+ arg1,
254+ arg2,
255+ )
256+ })
257+ return _c
258+}
259+
260+func (_c *CIStore_CreateRunner_Call) Return(removed []domain.CIRunner, err error) *CIStore_CreateRunner_Call {
261+ _c.Call.Return(removed, err)
262+ return _c
263+}
264+
265+func (_c *CIStore_CreateRunner_Call) RunAndReturn(run func(ctx context.Context, r domain.CIRunner, replace bool) ([]domain.CIRunner, error)) *CIStore_CreateRunner_Call {
266+ _c.Call.Return(run)
267+ return _c
268+}
269+
270+// FindJobByToken provides a mock function for the type CIStore
271+func (_mock *CIStore) FindJobByToken(ctx context.Context, hash domain.TokenHash, now time.Time) (domain.CIJob, error) {
272+ ret := _mock.Called(ctx, hash, now)
273+
274+ if len(ret) == 0 {
275+ panic("no return value specified for FindJobByToken")
276+ }
277+
278+ var r0 domain.CIJob
279+ var r1 error
280+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.TokenHash, time.Time) (domain.CIJob, error)); ok {
281+ return returnFunc(ctx, hash, now)
282+ }
283+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.TokenHash, time.Time) domain.CIJob); ok {
284+ r0 = returnFunc(ctx, hash, now)
285+ } else {
286+ r0 = ret.Get(0).(domain.CIJob)
287+ }
288+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.TokenHash, time.Time) error); ok {
289+ r1 = returnFunc(ctx, hash, now)
290+ } else {
291+ r1 = ret.Error(1)
292+ }
293+ return r0, r1
294+}
295+
296+// CIStore_FindJobByToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindJobByToken'
297+type CIStore_FindJobByToken_Call struct {
298+ *mock.Call
299+}
300+
301+// FindJobByToken is a helper method to define mock.On call
302+// - ctx context.Context
303+// - hash domain.TokenHash
304+// - now time.Time
305+func (_e *CIStore_Expecter) FindJobByToken(ctx any, hash any, now any) *CIStore_FindJobByToken_Call {
306+ return &CIStore_FindJobByToken_Call{Call: _e.mock.On("FindJobByToken", ctx, hash, now)}
307+}
308+
309+func (_c *CIStore_FindJobByToken_Call) Run(run func(ctx context.Context, hash domain.TokenHash, now time.Time)) *CIStore_FindJobByToken_Call {
310+ _c.Call.Run(func(args mock.Arguments) {
311+ var arg0 context.Context
312+ if args[0] != nil {
313+ arg0 = args[0].(context.Context)
314+ }
315+ var arg1 domain.TokenHash
316+ if args[1] != nil {
317+ arg1 = args[1].(domain.TokenHash)
318+ }
319+ var arg2 time.Time
320+ if args[2] != nil {
321+ arg2 = args[2].(time.Time)
322+ }
323+ run(
324+ arg0,
325+ arg1,
326+ arg2,
327+ )
328+ })
329+ return _c
330+}
331+
332+func (_c *CIStore_FindJobByToken_Call) Return(cIJob domain.CIJob, err error) *CIStore_FindJobByToken_Call {
333+ _c.Call.Return(cIJob, err)
334+ return _c
335+}
336+
337+func (_c *CIStore_FindJobByToken_Call) RunAndReturn(run func(ctx context.Context, hash domain.TokenHash, now time.Time) (domain.CIJob, error)) *CIStore_FindJobByToken_Call {
338+ _c.Call.Return(run)
339+ return _c
340+}
341+
342+// FindRunnerByToken provides a mock function for the type CIStore
343+func (_mock *CIStore) FindRunnerByToken(ctx context.Context, hash domain.TokenHash) (domain.CIRunner, error) {
344+ ret := _mock.Called(ctx, hash)
345+
346+ if len(ret) == 0 {
347+ panic("no return value specified for FindRunnerByToken")
348+ }
349+
350+ var r0 domain.CIRunner
351+ var r1 error
352+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.TokenHash) (domain.CIRunner, error)); ok {
353+ return returnFunc(ctx, hash)
354+ }
355+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.TokenHash) domain.CIRunner); ok {
356+ r0 = returnFunc(ctx, hash)
357+ } else {
358+ r0 = ret.Get(0).(domain.CIRunner)
359+ }
360+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.TokenHash) error); ok {
361+ r1 = returnFunc(ctx, hash)
362+ } else {
363+ r1 = ret.Error(1)
364+ }
365+ return r0, r1
366+}
367+
368+// CIStore_FindRunnerByToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindRunnerByToken'
369+type CIStore_FindRunnerByToken_Call struct {
370+ *mock.Call
371+}
372+
373+// FindRunnerByToken is a helper method to define mock.On call
374+// - ctx context.Context
375+// - hash domain.TokenHash
376+func (_e *CIStore_Expecter) FindRunnerByToken(ctx any, hash any) *CIStore_FindRunnerByToken_Call {
377+ return &CIStore_FindRunnerByToken_Call{Call: _e.mock.On("FindRunnerByToken", ctx, hash)}
378+}
379+
380+func (_c *CIStore_FindRunnerByToken_Call) Run(run func(ctx context.Context, hash domain.TokenHash)) *CIStore_FindRunnerByToken_Call {
381+ _c.Call.Run(func(args mock.Arguments) {
382+ var arg0 context.Context
383+ if args[0] != nil {
384+ arg0 = args[0].(context.Context)
385+ }
386+ var arg1 domain.TokenHash
387+ if args[1] != nil {
388+ arg1 = args[1].(domain.TokenHash)
389+ }
390+ run(
391+ arg0,
392+ arg1,
393+ )
394+ })
395+ return _c
396+}
397+
398+func (_c *CIStore_FindRunnerByToken_Call) Return(cIRunner domain.CIRunner, err error) *CIStore_FindRunnerByToken_Call {
399+ _c.Call.Return(cIRunner, err)
400+ return _c
401+}
402+
403+func (_c *CIStore_FindRunnerByToken_Call) RunAndReturn(run func(ctx context.Context, hash domain.TokenHash) (domain.CIRunner, error)) *CIStore_FindRunnerByToken_Call {
404+ _c.Call.Return(run)
405+ return _c
406+}
407+
408+// GetJob provides a mock function for the type CIStore
409+func (_mock *CIStore) GetJob(ctx context.Context, runID int64, runAttempt int32, jobID string) (domain.CIJob, error) {
410+ ret := _mock.Called(ctx, runID, runAttempt, jobID)
411+
412+ if len(ret) == 0 {
413+ panic("no return value specified for GetJob")
414+ }
415+
416+ var r0 domain.CIJob
417+ var r1 error
418+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64, int32, string) (domain.CIJob, error)); ok {
419+ return returnFunc(ctx, runID, runAttempt, jobID)
420+ }
421+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64, int32, string) domain.CIJob); ok {
422+ r0 = returnFunc(ctx, runID, runAttempt, jobID)
423+ } else {
424+ r0 = ret.Get(0).(domain.CIJob)
425+ }
426+ if returnFunc, ok := ret.Get(1).(func(context.Context, int64, int32, string) error); ok {
427+ r1 = returnFunc(ctx, runID, runAttempt, jobID)
428+ } else {
429+ r1 = ret.Error(1)
430+ }
431+ return r0, r1
432+}
433+
434+// CIStore_GetJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJob'
435+type CIStore_GetJob_Call struct {
436+ *mock.Call
437+}
438+
439+// GetJob is a helper method to define mock.On call
440+// - ctx context.Context
441+// - runID int64
442+// - runAttempt int32
443+// - jobID string
444+func (_e *CIStore_Expecter) GetJob(ctx any, runID any, runAttempt any, jobID any) *CIStore_GetJob_Call {
445+ return &CIStore_GetJob_Call{Call: _e.mock.On("GetJob", ctx, runID, runAttempt, jobID)}
446+}
447+
448+func (_c *CIStore_GetJob_Call) Run(run func(ctx context.Context, runID int64, runAttempt int32, jobID string)) *CIStore_GetJob_Call {
449+ _c.Call.Run(func(args mock.Arguments) {
450+ var arg0 context.Context
451+ if args[0] != nil {
452+ arg0 = args[0].(context.Context)
453+ }
454+ var arg1 int64
455+ if args[1] != nil {
456+ arg1 = args[1].(int64)
457+ }
458+ var arg2 int32
459+ if args[2] != nil {
460+ arg2 = args[2].(int32)
461+ }
462+ var arg3 string
463+ if args[3] != nil {
464+ arg3 = args[3].(string)
465+ }
466+ run(
467+ arg0,
468+ arg1,
469+ arg2,
470+ arg3,
471+ )
472+ })
473+ return _c
474+}
475+
476+func (_c *CIStore_GetJob_Call) Return(cIJob domain.CIJob, err error) *CIStore_GetJob_Call {
477+ _c.Call.Return(cIJob, err)
478+ return _c
479+}
480+
481+func (_c *CIStore_GetJob_Call) RunAndReturn(run func(ctx context.Context, runID int64, runAttempt int32, jobID string) (domain.CIJob, error)) *CIStore_GetJob_Call {
482+ _c.Call.Return(run)
483+ return _c
484+}
485+
486+// GetRun provides a mock function for the type CIStore
487+func (_mock *CIStore) GetRun(ctx context.Context, repo domain.RepoName, id int64) (domain.CIRun, error) {
488+ ret := _mock.Called(ctx, repo, id)
489+
490+ if len(ret) == 0 {
491+ panic("no return value specified for GetRun")
492+ }
493+
494+ var r0 domain.CIRun
495+ var r1 error
496+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) (domain.CIRun, error)); ok {
497+ return returnFunc(ctx, repo, id)
498+ }
499+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64) domain.CIRun); ok {
500+ r0 = returnFunc(ctx, repo, id)
501+ } else {
502+ r0 = ret.Get(0).(domain.CIRun)
503+ }
504+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64) error); ok {
505+ r1 = returnFunc(ctx, repo, id)
506+ } else {
507+ r1 = ret.Error(1)
508+ }
509+ return r0, r1
510+}
511+
512+// CIStore_GetRun_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRun'
513+type CIStore_GetRun_Call struct {
514+ *mock.Call
515+}
516+
517+// GetRun is a helper method to define mock.On call
518+// - ctx context.Context
519+// - repo domain.RepoName
520+// - id int64
521+func (_e *CIStore_Expecter) GetRun(ctx any, repo any, id any) *CIStore_GetRun_Call {
522+ return &CIStore_GetRun_Call{Call: _e.mock.On("GetRun", ctx, repo, id)}
523+}
524+
525+func (_c *CIStore_GetRun_Call) Run(run func(ctx context.Context, repo domain.RepoName, id int64)) *CIStore_GetRun_Call {
526+ _c.Call.Run(func(args mock.Arguments) {
527+ var arg0 context.Context
528+ if args[0] != nil {
529+ arg0 = args[0].(context.Context)
530+ }
531+ var arg1 domain.RepoName
532+ if args[1] != nil {
533+ arg1 = args[1].(domain.RepoName)
534+ }
535+ var arg2 int64
536+ if args[2] != nil {
537+ arg2 = args[2].(int64)
538+ }
539+ run(
540+ arg0,
541+ arg1,
542+ arg2,
543+ )
544+ })
545+ return _c
546+}
547+
548+func (_c *CIStore_GetRun_Call) Return(cIRun domain.CIRun, err error) *CIStore_GetRun_Call {
549+ _c.Call.Return(cIRun, err)
550+ return _c
551+}
552+
553+func (_c *CIStore_GetRun_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, id int64) (domain.CIRun, error)) *CIStore_GetRun_Call {
554+ _c.Call.Return(run)
555+ return _c
556+}
557+
558+// GetRunSource provides a mock function for the type CIStore
559+func (_mock *CIStore) GetRunSource(ctx context.Context, runID int64) (domain.CIRunSource, error) {
560+ ret := _mock.Called(ctx, runID)
561+
562+ if len(ret) == 0 {
563+ panic("no return value specified for GetRunSource")
564+ }
565+
566+ var r0 domain.CIRunSource
567+ var r1 error
568+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64) (domain.CIRunSource, error)); ok {
569+ return returnFunc(ctx, runID)
570+ }
571+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64) domain.CIRunSource); ok {
572+ r0 = returnFunc(ctx, runID)
573+ } else {
574+ r0 = ret.Get(0).(domain.CIRunSource)
575+ }
576+ if returnFunc, ok := ret.Get(1).(func(context.Context, int64) error); ok {
577+ r1 = returnFunc(ctx, runID)
578+ } else {
579+ r1 = ret.Error(1)
580+ }
581+ return r0, r1
582+}
583+
584+// CIStore_GetRunSource_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRunSource'
585+type CIStore_GetRunSource_Call struct {
586+ *mock.Call
587+}
588+
589+// GetRunSource is a helper method to define mock.On call
590+// - ctx context.Context
591+// - runID int64
592+func (_e *CIStore_Expecter) GetRunSource(ctx any, runID any) *CIStore_GetRunSource_Call {
593+ return &CIStore_GetRunSource_Call{Call: _e.mock.On("GetRunSource", ctx, runID)}
594+}
595+
596+func (_c *CIStore_GetRunSource_Call) Run(run func(ctx context.Context, runID int64)) *CIStore_GetRunSource_Call {
597+ _c.Call.Run(func(args mock.Arguments) {
598+ var arg0 context.Context
599+ if args[0] != nil {
600+ arg0 = args[0].(context.Context)
601+ }
602+ var arg1 int64
603+ if args[1] != nil {
604+ arg1 = args[1].(int64)
605+ }
606+ run(
607+ arg0,
608+ arg1,
609+ )
610+ })
611+ return _c
612+}
613+
614+func (_c *CIStore_GetRunSource_Call) Return(cIRunSource domain.CIRunSource, err error) *CIStore_GetRunSource_Call {
615+ _c.Call.Return(cIRunSource, err)
616+ return _c
617+}
618+
619+func (_c *CIStore_GetRunSource_Call) RunAndReturn(run func(ctx context.Context, runID int64) (domain.CIRunSource, error)) *CIStore_GetRunSource_Call {
620+ _c.Call.Return(run)
621+ return _c
622+}
623+
624+// GetRunner provides a mock function for the type CIStore
625+func (_mock *CIStore) GetRunner(ctx context.Context, id string) (domain.CIRunner, error) {
626+ ret := _mock.Called(ctx, id)
627+
628+ if len(ret) == 0 {
629+ panic("no return value specified for GetRunner")
630+ }
631+
632+ var r0 domain.CIRunner
633+ var r1 error
634+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (domain.CIRunner, error)); ok {
635+ return returnFunc(ctx, id)
636+ }
637+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) domain.CIRunner); ok {
638+ r0 = returnFunc(ctx, id)
639+ } else {
640+ r0 = ret.Get(0).(domain.CIRunner)
641+ }
642+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
643+ r1 = returnFunc(ctx, id)
644+ } else {
645+ r1 = ret.Error(1)
646+ }
647+ return r0, r1
648+}
649+
650+// CIStore_GetRunner_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRunner'
651+type CIStore_GetRunner_Call struct {
652+ *mock.Call
653+}
654+
655+// GetRunner is a helper method to define mock.On call
656+// - ctx context.Context
657+// - id string
658+func (_e *CIStore_Expecter) GetRunner(ctx any, id any) *CIStore_GetRunner_Call {
659+ return &CIStore_GetRunner_Call{Call: _e.mock.On("GetRunner", ctx, id)}
660+}
661+
662+func (_c *CIStore_GetRunner_Call) Run(run func(ctx context.Context, id string)) *CIStore_GetRunner_Call {
663+ _c.Call.Run(func(args mock.Arguments) {
664+ var arg0 context.Context
665+ if args[0] != nil {
666+ arg0 = args[0].(context.Context)
667+ }
668+ var arg1 string
669+ if args[1] != nil {
670+ arg1 = args[1].(string)
671+ }
672+ run(
673+ arg0,
674+ arg1,
675+ )
676+ })
677+ return _c
678+}
679+
680+func (_c *CIStore_GetRunner_Call) Return(cIRunner domain.CIRunner, err error) *CIStore_GetRunner_Call {
681+ _c.Call.Return(cIRunner, err)
682+ return _c
683+}
684+
685+func (_c *CIStore_GetRunner_Call) RunAndReturn(run func(ctx context.Context, id string) (domain.CIRunner, error)) *CIStore_GetRunner_Call {
686+ _c.Call.Return(run)
687+ return _c
688+}
689+
690+// ListActiveJobs provides a mock function for the type CIStore
691+func (_mock *CIStore) ListActiveJobs(ctx context.Context) ([]domain.CIJob, error) {
692+ ret := _mock.Called(ctx)
693+
694+ if len(ret) == 0 {
695+ panic("no return value specified for ListActiveJobs")
696+ }
697+
698+ var r0 []domain.CIJob
699+ var r1 error
700+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]domain.CIJob, error)); ok {
701+ return returnFunc(ctx)
702+ }
703+ if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.CIJob); ok {
704+ r0 = returnFunc(ctx)
705+ } else {
706+ if ret.Get(0) != nil {
707+ r0 = ret.Get(0).([]domain.CIJob)
708+ }
709+ }
710+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
711+ r1 = returnFunc(ctx)
712+ } else {
713+ r1 = ret.Error(1)
714+ }
715+ return r0, r1
716+}
717+
718+// CIStore_ListActiveJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListActiveJobs'
719+type CIStore_ListActiveJobs_Call struct {
720+ *mock.Call
721+}
722+
723+// ListActiveJobs is a helper method to define mock.On call
724+// - ctx context.Context
725+func (_e *CIStore_Expecter) ListActiveJobs(ctx any) *CIStore_ListActiveJobs_Call {
726+ return &CIStore_ListActiveJobs_Call{Call: _e.mock.On("ListActiveJobs", ctx)}
727+}
728+
729+func (_c *CIStore_ListActiveJobs_Call) Run(run func(ctx context.Context)) *CIStore_ListActiveJobs_Call {
730+ _c.Call.Run(func(args mock.Arguments) {
731+ var arg0 context.Context
732+ if args[0] != nil {
733+ arg0 = args[0].(context.Context)
734+ }
735+ run(
736+ arg0,
737+ )
738+ })
739+ return _c
740+}
741+
742+func (_c *CIStore_ListActiveJobs_Call) Return(cIJobs []domain.CIJob, err error) *CIStore_ListActiveJobs_Call {
743+ _c.Call.Return(cIJobs, err)
744+ return _c
745+}
746+
747+func (_c *CIStore_ListActiveJobs_Call) RunAndReturn(run func(ctx context.Context) ([]domain.CIJob, error)) *CIStore_ListActiveJobs_Call {
748+ _c.Call.Return(run)
749+ return _c
750+}
751+
752+// ListChecks provides a mock function for the type CIStore
753+func (_mock *CIStore) ListChecks(ctx context.Context, repo domain.RepoName, sha string, limit int) ([]domain.CICheck, bool, error) {
754+ ret := _mock.Called(ctx, repo, sha, limit)
755+
756+ if len(ret) == 0 {
757+ panic("no return value specified for ListChecks")
758+ }
759+
760+ var r0 []domain.CICheck
761+ var r1 bool
762+ var r2 error
763+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string, int) ([]domain.CICheck, bool, error)); ok {
764+ return returnFunc(ctx, repo, sha, limit)
765+ }
766+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, string, int) []domain.CICheck); ok {
767+ r0 = returnFunc(ctx, repo, sha, limit)
768+ } else {
769+ if ret.Get(0) != nil {
770+ r0 = ret.Get(0).([]domain.CICheck)
771+ }
772+ }
773+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, string, int) bool); ok {
774+ r1 = returnFunc(ctx, repo, sha, limit)
775+ } else {
776+ r1 = ret.Get(1).(bool)
777+ }
778+ if returnFunc, ok := ret.Get(2).(func(context.Context, domain.RepoName, string, int) error); ok {
779+ r2 = returnFunc(ctx, repo, sha, limit)
780+ } else {
781+ r2 = ret.Error(2)
782+ }
783+ return r0, r1, r2
784+}
785+
786+// CIStore_ListChecks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListChecks'
787+type CIStore_ListChecks_Call struct {
788+ *mock.Call
789+}
790+
791+// ListChecks is a helper method to define mock.On call
792+// - ctx context.Context
793+// - repo domain.RepoName
794+// - sha string
795+// - limit int
796+func (_e *CIStore_Expecter) ListChecks(ctx any, repo any, sha any, limit any) *CIStore_ListChecks_Call {
797+ return &CIStore_ListChecks_Call{Call: _e.mock.On("ListChecks", ctx, repo, sha, limit)}
798+}
799+
800+func (_c *CIStore_ListChecks_Call) Run(run func(ctx context.Context, repo domain.RepoName, sha string, limit int)) *CIStore_ListChecks_Call {
801+ _c.Call.Run(func(args mock.Arguments) {
802+ var arg0 context.Context
803+ if args[0] != nil {
804+ arg0 = args[0].(context.Context)
805+ }
806+ var arg1 domain.RepoName
807+ if args[1] != nil {
808+ arg1 = args[1].(domain.RepoName)
809+ }
810+ var arg2 string
811+ if args[2] != nil {
812+ arg2 = args[2].(string)
813+ }
814+ var arg3 int
815+ if args[3] != nil {
816+ arg3 = args[3].(int)
817+ }
818+ run(
819+ arg0,
820+ arg1,
821+ arg2,
822+ arg3,
823+ )
824+ })
825+ return _c
826+}
827+
828+func (_c *CIStore_ListChecks_Call) Return(checks []domain.CICheck, more bool, err error) *CIStore_ListChecks_Call {
829+ _c.Call.Return(checks, more, err)
830+ return _c
831+}
832+
833+func (_c *CIStore_ListChecks_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, sha string, limit int) ([]domain.CICheck, bool, error)) *CIStore_ListChecks_Call {
834+ _c.Call.Return(run)
835+ return _c
836+}
837+
838+// ListJobs provides a mock function for the type CIStore
839+func (_mock *CIStore) ListJobs(ctx context.Context, runID int64, runAttempt int32) ([]domain.CIJob, error) {
840+ ret := _mock.Called(ctx, runID, runAttempt)
841+
842+ if len(ret) == 0 {
843+ panic("no return value specified for ListJobs")
844+ }
845+
846+ var r0 []domain.CIJob
847+ var r1 error
848+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64, int32) ([]domain.CIJob, error)); ok {
849+ return returnFunc(ctx, runID, runAttempt)
850+ }
851+ if returnFunc, ok := ret.Get(0).(func(context.Context, int64, int32) []domain.CIJob); ok {
852+ r0 = returnFunc(ctx, runID, runAttempt)
853+ } else {
854+ if ret.Get(0) != nil {
855+ r0 = ret.Get(0).([]domain.CIJob)
856+ }
857+ }
858+ if returnFunc, ok := ret.Get(1).(func(context.Context, int64, int32) error); ok {
859+ r1 = returnFunc(ctx, runID, runAttempt)
860+ } else {
861+ r1 = ret.Error(1)
862+ }
863+ return r0, r1
864+}
865+
866+// CIStore_ListJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListJobs'
867+type CIStore_ListJobs_Call struct {
868+ *mock.Call
869+}
870+
871+// ListJobs is a helper method to define mock.On call
872+// - ctx context.Context
873+// - runID int64
874+// - runAttempt int32
875+func (_e *CIStore_Expecter) ListJobs(ctx any, runID any, runAttempt any) *CIStore_ListJobs_Call {
876+ return &CIStore_ListJobs_Call{Call: _e.mock.On("ListJobs", ctx, runID, runAttempt)}
877+}
878+
879+func (_c *CIStore_ListJobs_Call) Run(run func(ctx context.Context, runID int64, runAttempt int32)) *CIStore_ListJobs_Call {
880+ _c.Call.Run(func(args mock.Arguments) {
881+ var arg0 context.Context
882+ if args[0] != nil {
883+ arg0 = args[0].(context.Context)
884+ }
885+ var arg1 int64
886+ if args[1] != nil {
887+ arg1 = args[1].(int64)
888+ }
889+ var arg2 int32
890+ if args[2] != nil {
891+ arg2 = args[2].(int32)
892+ }
893+ run(
894+ arg0,
895+ arg1,
896+ arg2,
897+ )
898+ })
899+ return _c
900+}
901+
902+func (_c *CIStore_ListJobs_Call) Return(cIJobs []domain.CIJob, err error) *CIStore_ListJobs_Call {
903+ _c.Call.Return(cIJobs, err)
904+ return _c
905+}
906+
907+func (_c *CIStore_ListJobs_Call) RunAndReturn(run func(ctx context.Context, runID int64, runAttempt int32) ([]domain.CIJob, error)) *CIStore_ListJobs_Call {
908+ _c.Call.Return(run)
909+ return _c
910+}
911+
912+// ListOpenRuns provides a mock function for the type CIStore
913+func (_mock *CIStore) ListOpenRuns(ctx context.Context) ([]domain.CIRun, error) {
914+ ret := _mock.Called(ctx)
915+
916+ if len(ret) == 0 {
917+ panic("no return value specified for ListOpenRuns")
918+ }
919+
920+ var r0 []domain.CIRun
921+ var r1 error
922+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]domain.CIRun, error)); ok {
923+ return returnFunc(ctx)
924+ }
925+ if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.CIRun); ok {
926+ r0 = returnFunc(ctx)
927+ } else {
928+ if ret.Get(0) != nil {
929+ r0 = ret.Get(0).([]domain.CIRun)
930+ }
931+ }
932+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
933+ r1 = returnFunc(ctx)
934+ } else {
935+ r1 = ret.Error(1)
936+ }
937+ return r0, r1
938+}
939+
940+// CIStore_ListOpenRuns_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListOpenRuns'
941+type CIStore_ListOpenRuns_Call struct {
942+ *mock.Call
943+}
944+
945+// ListOpenRuns is a helper method to define mock.On call
946+// - ctx context.Context
947+func (_e *CIStore_Expecter) ListOpenRuns(ctx any) *CIStore_ListOpenRuns_Call {
948+ return &CIStore_ListOpenRuns_Call{Call: _e.mock.On("ListOpenRuns", ctx)}
949+}
950+
951+func (_c *CIStore_ListOpenRuns_Call) Run(run func(ctx context.Context)) *CIStore_ListOpenRuns_Call {
952+ _c.Call.Run(func(args mock.Arguments) {
953+ var arg0 context.Context
954+ if args[0] != nil {
955+ arg0 = args[0].(context.Context)
956+ }
957+ run(
958+ arg0,
959+ )
960+ })
961+ return _c
962+}
963+
964+func (_c *CIStore_ListOpenRuns_Call) Return(cIRuns []domain.CIRun, err error) *CIStore_ListOpenRuns_Call {
965+ _c.Call.Return(cIRuns, err)
966+ return _c
967+}
968+
969+func (_c *CIStore_ListOpenRuns_Call) RunAndReturn(run func(ctx context.Context) ([]domain.CIRun, error)) *CIStore_ListOpenRuns_Call {
970+ _c.Call.Return(run)
971+ return _c
972+}
973+
974+// ListReadyJobs provides a mock function for the type CIStore
975+func (_mock *CIStore) ListReadyJobs(ctx context.Context, limit int) ([]domain.CIJob, error) {
976+ ret := _mock.Called(ctx, limit)
977+
978+ if len(ret) == 0 {
979+ panic("no return value specified for ListReadyJobs")
980+ }
981+
982+ var r0 []domain.CIJob
983+ var r1 error
984+ if returnFunc, ok := ret.Get(0).(func(context.Context, int) ([]domain.CIJob, error)); ok {
985+ return returnFunc(ctx, limit)
986+ }
987+ if returnFunc, ok := ret.Get(0).(func(context.Context, int) []domain.CIJob); ok {
988+ r0 = returnFunc(ctx, limit)
989+ } else {
990+ if ret.Get(0) != nil {
991+ r0 = ret.Get(0).([]domain.CIJob)
992+ }
993+ }
994+ if returnFunc, ok := ret.Get(1).(func(context.Context, int) error); ok {
995+ r1 = returnFunc(ctx, limit)
996+ } else {
997+ r1 = ret.Error(1)
998+ }
999+ return r0, r1
1000+}
1001+
1002+// CIStore_ListReadyJobs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListReadyJobs'
1003+type CIStore_ListReadyJobs_Call struct {
1004+ *mock.Call
1005+}
1006+
1007+// ListReadyJobs is a helper method to define mock.On call
1008+// - ctx context.Context
1009+// - limit int
1010+func (_e *CIStore_Expecter) ListReadyJobs(ctx any, limit any) *CIStore_ListReadyJobs_Call {
1011+ return &CIStore_ListReadyJobs_Call{Call: _e.mock.On("ListReadyJobs", ctx, limit)}
1012+}
1013+
1014+func (_c *CIStore_ListReadyJobs_Call) Run(run func(ctx context.Context, limit int)) *CIStore_ListReadyJobs_Call {
1015+ _c.Call.Run(func(args mock.Arguments) {
1016+ var arg0 context.Context
1017+ if args[0] != nil {
1018+ arg0 = args[0].(context.Context)
1019+ }
1020+ var arg1 int
1021+ if args[1] != nil {
1022+ arg1 = args[1].(int)
1023+ }
1024+ run(
1025+ arg0,
1026+ arg1,
1027+ )
1028+ })
1029+ return _c
1030+}
1031+
1032+func (_c *CIStore_ListReadyJobs_Call) Return(cIJobs []domain.CIJob, err error) *CIStore_ListReadyJobs_Call {
1033+ _c.Call.Return(cIJobs, err)
1034+ return _c
1035+}
1036+
1037+func (_c *CIStore_ListReadyJobs_Call) RunAndReturn(run func(ctx context.Context, limit int) ([]domain.CIJob, error)) *CIStore_ListReadyJobs_Call {
1038+ _c.Call.Return(run)
1039+ return _c
1040+}
1041+
1042+// ListRunners provides a mock function for the type CIStore
1043+func (_mock *CIStore) ListRunners(ctx context.Context) ([]domain.CIRunner, error) {
1044+ ret := _mock.Called(ctx)
1045+
1046+ if len(ret) == 0 {
1047+ panic("no return value specified for ListRunners")
1048+ }
1049+
1050+ var r0 []domain.CIRunner
1051+ var r1 error
1052+ if returnFunc, ok := ret.Get(0).(func(context.Context) ([]domain.CIRunner, error)); ok {
1053+ return returnFunc(ctx)
1054+ }
1055+ if returnFunc, ok := ret.Get(0).(func(context.Context) []domain.CIRunner); ok {
1056+ r0 = returnFunc(ctx)
1057+ } else {
1058+ if ret.Get(0) != nil {
1059+ r0 = ret.Get(0).([]domain.CIRunner)
1060+ }
1061+ }
1062+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
1063+ r1 = returnFunc(ctx)
1064+ } else {
1065+ r1 = ret.Error(1)
1066+ }
1067+ return r0, r1
1068+}
1069+
1070+// CIStore_ListRunners_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRunners'
1071+type CIStore_ListRunners_Call struct {
1072+ *mock.Call
1073+}
1074+
1075+// ListRunners is a helper method to define mock.On call
1076+// - ctx context.Context
1077+func (_e *CIStore_Expecter) ListRunners(ctx any) *CIStore_ListRunners_Call {
1078+ return &CIStore_ListRunners_Call{Call: _e.mock.On("ListRunners", ctx)}
1079+}
1080+
1081+func (_c *CIStore_ListRunners_Call) Run(run func(ctx context.Context)) *CIStore_ListRunners_Call {
1082+ _c.Call.Run(func(args mock.Arguments) {
1083+ var arg0 context.Context
1084+ if args[0] != nil {
1085+ arg0 = args[0].(context.Context)
1086+ }
1087+ run(
1088+ arg0,
1089+ )
1090+ })
1091+ return _c
1092+}
1093+
1094+func (_c *CIStore_ListRunners_Call) Return(cIRunners []domain.CIRunner, err error) *CIStore_ListRunners_Call {
1095+ _c.Call.Return(cIRunners, err)
1096+ return _c
1097+}
1098+
1099+func (_c *CIStore_ListRunners_Call) RunAndReturn(run func(ctx context.Context) ([]domain.CIRunner, error)) *CIStore_ListRunners_Call {
1100+ _c.Call.Return(run)
1101+ return _c
1102+}
1103+
1104+// ListRuns provides a mock function for the type CIStore
1105+func (_mock *CIStore) ListRuns(ctx context.Context, repo domain.RepoName, before int64, limit int) ([]domain.CIRun, error) {
1106+ ret := _mock.Called(ctx, repo, before, limit)
1107+
1108+ if len(ret) == 0 {
1109+ panic("no return value specified for ListRuns")
1110+ }
1111+
1112+ var r0 []domain.CIRun
1113+ var r1 error
1114+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, int) ([]domain.CIRun, error)); ok {
1115+ return returnFunc(ctx, repo, before, limit)
1116+ }
1117+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName, int64, int) []domain.CIRun); ok {
1118+ r0 = returnFunc(ctx, repo, before, limit)
1119+ } else {
1120+ if ret.Get(0) != nil {
1121+ r0 = ret.Get(0).([]domain.CIRun)
1122+ }
1123+ }
1124+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName, int64, int) error); ok {
1125+ r1 = returnFunc(ctx, repo, before, limit)
1126+ } else {
1127+ r1 = ret.Error(1)
1128+ }
1129+ return r0, r1
1130+}
1131+
1132+// CIStore_ListRuns_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRuns'
1133+type CIStore_ListRuns_Call struct {
1134+ *mock.Call
1135+}
1136+
1137+// ListRuns is a helper method to define mock.On call
1138+// - ctx context.Context
1139+// - repo domain.RepoName
1140+// - before int64
1141+// - limit int
1142+func (_e *CIStore_Expecter) ListRuns(ctx any, repo any, before any, limit any) *CIStore_ListRuns_Call {
1143+ return &CIStore_ListRuns_Call{Call: _e.mock.On("ListRuns", ctx, repo, before, limit)}
1144+}
1145+
1146+func (_c *CIStore_ListRuns_Call) Run(run func(ctx context.Context, repo domain.RepoName, before int64, limit int)) *CIStore_ListRuns_Call {
1147+ _c.Call.Run(func(args mock.Arguments) {
1148+ var arg0 context.Context
1149+ if args[0] != nil {
1150+ arg0 = args[0].(context.Context)
1151+ }
1152+ var arg1 domain.RepoName
1153+ if args[1] != nil {
1154+ arg1 = args[1].(domain.RepoName)
1155+ }
1156+ var arg2 int64
1157+ if args[2] != nil {
1158+ arg2 = args[2].(int64)
1159+ }
1160+ var arg3 int
1161+ if args[3] != nil {
1162+ arg3 = args[3].(int)
1163+ }
1164+ run(
1165+ arg0,
1166+ arg1,
1167+ arg2,
1168+ arg3,
1169+ )
1170+ })
1171+ return _c
1172+}
1173+
1174+func (_c *CIStore_ListRuns_Call) Return(cIRuns []domain.CIRun, err error) *CIStore_ListRuns_Call {
1175+ _c.Call.Return(cIRuns, err)
1176+ return _c
1177+}
1178+
1179+func (_c *CIStore_ListRuns_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, before int64, limit int) ([]domain.CIRun, error)) *CIStore_ListRuns_Call {
1180+ _c.Call.Return(run)
1181+ return _c
1182+}
1183+
1184+// RemoveRunner provides a mock function for the type CIStore
1185+func (_mock *CIStore) RemoveRunner(ctx context.Context, id string) (domain.CIRunner, error) {
1186+ ret := _mock.Called(ctx, id)
1187+
1188+ if len(ret) == 0 {
1189+ panic("no return value specified for RemoveRunner")
1190+ }
1191+
1192+ var r0 domain.CIRunner
1193+ var r1 error
1194+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (domain.CIRunner, error)); ok {
1195+ return returnFunc(ctx, id)
1196+ }
1197+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) domain.CIRunner); ok {
1198+ r0 = returnFunc(ctx, id)
1199+ } else {
1200+ r0 = ret.Get(0).(domain.CIRunner)
1201+ }
1202+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
1203+ r1 = returnFunc(ctx, id)
1204+ } else {
1205+ r1 = ret.Error(1)
1206+ }
1207+ return r0, r1
1208+}
1209+
1210+// CIStore_RemoveRunner_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveRunner'
1211+type CIStore_RemoveRunner_Call struct {
1212+ *mock.Call
1213+}
1214+
1215+// RemoveRunner is a helper method to define mock.On call
1216+// - ctx context.Context
1217+// - id string
1218+func (_e *CIStore_Expecter) RemoveRunner(ctx any, id any) *CIStore_RemoveRunner_Call {
1219+ return &CIStore_RemoveRunner_Call{Call: _e.mock.On("RemoveRunner", ctx, id)}
1220+}
1221+
1222+func (_c *CIStore_RemoveRunner_Call) Run(run func(ctx context.Context, id string)) *CIStore_RemoveRunner_Call {
1223+ _c.Call.Run(func(args mock.Arguments) {
1224+ var arg0 context.Context
1225+ if args[0] != nil {
1226+ arg0 = args[0].(context.Context)
1227+ }
1228+ var arg1 string
1229+ if args[1] != nil {
1230+ arg1 = args[1].(string)
1231+ }
1232+ run(
1233+ arg0,
1234+ arg1,
1235+ )
1236+ })
1237+ return _c
1238+}
1239+
1240+func (_c *CIStore_RemoveRunner_Call) Return(cIRunner domain.CIRunner, err error) *CIStore_RemoveRunner_Call {
1241+ _c.Call.Return(cIRunner, err)
1242+ return _c
1243+}
1244+
1245+func (_c *CIStore_RemoveRunner_Call) RunAndReturn(run func(ctx context.Context, id string) (domain.CIRunner, error)) *CIStore_RemoveRunner_Call {
1246+ _c.Call.Return(run)
1247+ return _c
1248+}
1249+
1250+// RunIDs provides a mock function for the type CIStore
1251+func (_mock *CIStore) RunIDs(ctx context.Context, repo domain.RepoName) ([]int64, error) {
1252+ ret := _mock.Called(ctx, repo)
1253+
1254+ if len(ret) == 0 {
1255+ panic("no return value specified for RunIDs")
1256+ }
1257+
1258+ var r0 []int64
1259+ var r1 error
1260+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) ([]int64, error)); ok {
1261+ return returnFunc(ctx, repo)
1262+ }
1263+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RepoName) []int64); ok {
1264+ r0 = returnFunc(ctx, repo)
1265+ } else {
1266+ if ret.Get(0) != nil {
1267+ r0 = ret.Get(0).([]int64)
1268+ }
1269+ }
1270+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RepoName) error); ok {
1271+ r1 = returnFunc(ctx, repo)
1272+ } else {
1273+ r1 = ret.Error(1)
1274+ }
1275+ return r0, r1
1276+}
1277+
1278+// CIStore_RunIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RunIDs'
1279+type CIStore_RunIDs_Call struct {
1280+ *mock.Call
1281+}
1282+
1283+// RunIDs is a helper method to define mock.On call
1284+// - ctx context.Context
1285+// - repo domain.RepoName
1286+func (_e *CIStore_Expecter) RunIDs(ctx any, repo any) *CIStore_RunIDs_Call {
1287+ return &CIStore_RunIDs_Call{Call: _e.mock.On("RunIDs", ctx, repo)}
1288+}
1289+
1290+func (_c *CIStore_RunIDs_Call) Run(run func(ctx context.Context, repo domain.RepoName)) *CIStore_RunIDs_Call {
1291+ _c.Call.Run(func(args mock.Arguments) {
1292+ var arg0 context.Context
1293+ if args[0] != nil {
1294+ arg0 = args[0].(context.Context)
1295+ }
1296+ var arg1 domain.RepoName
1297+ if args[1] != nil {
1298+ arg1 = args[1].(domain.RepoName)
1299+ }
1300+ run(
1301+ arg0,
1302+ arg1,
1303+ )
1304+ })
1305+ return _c
1306+}
1307+
1308+func (_c *CIStore_RunIDs_Call) Return(int64s []int64, err error) *CIStore_RunIDs_Call {
1309+ _c.Call.Return(int64s, err)
1310+ return _c
1311+}
1312+
1313+func (_c *CIStore_RunIDs_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName) ([]int64, error)) *CIStore_RunIDs_Call {
1314+ _c.Call.Return(run)
1315+ return _c
1316+}
1317+
1318+// Sweep provides a mock function for the type CIStore
1319+func (_mock *CIStore) Sweep(ctx context.Context, before time.Time) ([]int64, error) {
1320+ ret := _mock.Called(ctx, before)
1321+
1322+ if len(ret) == 0 {
1323+ panic("no return value specified for Sweep")
1324+ }
1325+
1326+ var r0 []int64
1327+ var r1 error
1328+ if returnFunc, ok := ret.Get(0).(func(context.Context, time.Time) ([]int64, error)); ok {
1329+ return returnFunc(ctx, before)
1330+ }
1331+ if returnFunc, ok := ret.Get(0).(func(context.Context, time.Time) []int64); ok {
1332+ r0 = returnFunc(ctx, before)
1333+ } else {
1334+ if ret.Get(0) != nil {
1335+ r0 = ret.Get(0).([]int64)
1336+ }
1337+ }
1338+ if returnFunc, ok := ret.Get(1).(func(context.Context, time.Time) error); ok {
1339+ r1 = returnFunc(ctx, before)
1340+ } else {
1341+ r1 = ret.Error(1)
1342+ }
1343+ return r0, r1
1344+}
1345+
1346+// CIStore_Sweep_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sweep'
1347+type CIStore_Sweep_Call struct {
1348+ *mock.Call
1349+}
1350+
1351+// Sweep is a helper method to define mock.On call
1352+// - ctx context.Context
1353+// - before time.Time
1354+func (_e *CIStore_Expecter) Sweep(ctx any, before any) *CIStore_Sweep_Call {
1355+ return &CIStore_Sweep_Call{Call: _e.mock.On("Sweep", ctx, before)}
1356+}
1357+
1358+func (_c *CIStore_Sweep_Call) Run(run func(ctx context.Context, before time.Time)) *CIStore_Sweep_Call {
1359+ _c.Call.Run(func(args mock.Arguments) {
1360+ var arg0 context.Context
1361+ if args[0] != nil {
1362+ arg0 = args[0].(context.Context)
1363+ }
1364+ var arg1 time.Time
1365+ if args[1] != nil {
1366+ arg1 = args[1].(time.Time)
1367+ }
1368+ run(
1369+ arg0,
1370+ arg1,
1371+ )
1372+ })
1373+ return _c
1374+}
1375+
1376+func (_c *CIStore_Sweep_Call) Return(int64s []int64, err error) *CIStore_Sweep_Call {
1377+ _c.Call.Return(int64s, err)
1378+ return _c
1379+}
1380+
1381+func (_c *CIStore_Sweep_Call) RunAndReturn(run func(ctx context.Context, before time.Time) ([]int64, error)) *CIStore_Sweep_Call {
1382+ _c.Call.Return(run)
1383+ return _c
1384+}
A internal/ports/mocks/RunService.go +769 -0
@@ -0,0 +1,769 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+ mock "github.com/stretchr/testify/mock"
13+)
14+
15+// NewRunService creates a new instance of RunService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
16+// The first argument is typically a *testing.T value.
17+func NewRunService(t interface {
18+ mock.TestingT
19+ Cleanup(func())
20+}) *RunService {
21+ if helper, ok := t.(interface{ Helper() }); ok {
22+ helper.Helper()
23+ }
24+
25+ mock := &RunService{}
26+ mock.Mock.Test(t)
27+
28+ t.Cleanup(func() {
29+ if helper, ok := t.(interface{ Helper() }); ok {
30+ helper.Helper()
31+ }
32+ mock.AssertExpectations(t)
33+ })
34+
35+ return mock
36+}
37+
38+// RunService is an autogenerated mock type for the RunService type
39+type RunService struct {
40+ mock.Mock
41+}
42+
43+type RunService_Expecter struct {
44+ mock *mock.Mock
45+}
46+
47+func (_m *RunService) EXPECT() *RunService_Expecter {
48+ return &RunService_Expecter{mock: &_m.Mock}
49+}
50+
51+// CancelRun provides a mock function for the type RunService
52+func (_mock *RunService) CancelRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64) (domain.CIRun, error) {
53+ ret := _mock.Called(ctx, p, repo, runID)
54+
55+ if len(ret) == 0 {
56+ panic("no return value specified for CancelRun")
57+ }
58+
59+ var r0 domain.CIRun
60+ var r1 error
61+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) (domain.CIRun, error)); ok {
62+ return returnFunc(ctx, p, repo, runID)
63+ }
64+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64) domain.CIRun); ok {
65+ r0 = returnFunc(ctx, p, repo, runID)
66+ } else {
67+ r0 = ret.Get(0).(domain.CIRun)
68+ }
69+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64) error); ok {
70+ r1 = returnFunc(ctx, p, repo, runID)
71+ } else {
72+ r1 = ret.Error(1)
73+ }
74+ return r0, r1
75+}
76+
77+// RunService_CancelRun_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CancelRun'
78+type RunService_CancelRun_Call struct {
79+ *mock.Call
80+}
81+
82+// CancelRun is a helper method to define mock.On call
83+// - ctx context.Context
84+// - p domain.Principal
85+// - repo domain.RepoName
86+// - runID int64
87+func (_e *RunService_Expecter) CancelRun(ctx any, p any, repo any, runID any) *RunService_CancelRun_Call {
88+ return &RunService_CancelRun_Call{Call: _e.mock.On("CancelRun", ctx, p, repo, runID)}
89+}
90+
91+func (_c *RunService_CancelRun_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64)) *RunService_CancelRun_Call {
92+ _c.Call.Run(func(args mock.Arguments) {
93+ var arg0 context.Context
94+ if args[0] != nil {
95+ arg0 = args[0].(context.Context)
96+ }
97+ var arg1 domain.Principal
98+ if args[1] != nil {
99+ arg1 = args[1].(domain.Principal)
100+ }
101+ var arg2 domain.RepoName
102+ if args[2] != nil {
103+ arg2 = args[2].(domain.RepoName)
104+ }
105+ var arg3 int64
106+ if args[3] != nil {
107+ arg3 = args[3].(int64)
108+ }
109+ run(
110+ arg0,
111+ arg1,
112+ arg2,
113+ arg3,
114+ )
115+ })
116+ return _c
117+}
118+
119+func (_c *RunService_CancelRun_Call) Return(cIRun domain.CIRun, err error) *RunService_CancelRun_Call {
120+ _c.Call.Return(cIRun, err)
121+ return _c
122+}
123+
124+func (_c *RunService_CancelRun_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64) (domain.CIRun, error)) *RunService_CancelRun_Call {
125+ _c.Call.Return(run)
126+ return _c
127+}
128+
129+// GetJob provides a mock function for the type RunService
130+func (_mock *RunService) GetJob(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32) (ports.JobView, error) {
131+ ret := _mock.Called(ctx, p, repo, jobID, attempt)
132+
133+ if len(ret) == 0 {
134+ panic("no return value specified for GetJob")
135+ }
136+
137+ var r0 ports.JobView
138+ var r1 error
139+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, string, int32) (ports.JobView, error)); ok {
140+ return returnFunc(ctx, p, repo, jobID, attempt)
141+ }
142+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, string, int32) ports.JobView); ok {
143+ r0 = returnFunc(ctx, p, repo, jobID, attempt)
144+ } else {
145+ r0 = ret.Get(0).(ports.JobView)
146+ }
147+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, string, int32) error); ok {
148+ r1 = returnFunc(ctx, p, repo, jobID, attempt)
149+ } else {
150+ r1 = ret.Error(1)
151+ }
152+ return r0, r1
153+}
154+
155+// RunService_GetJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJob'
156+type RunService_GetJob_Call struct {
157+ *mock.Call
158+}
159+
160+// GetJob is a helper method to define mock.On call
161+// - ctx context.Context
162+// - p domain.Principal
163+// - repo domain.RepoName
164+// - jobID string
165+// - attempt int32
166+func (_e *RunService_Expecter) GetJob(ctx any, p any, repo any, jobID any, attempt any) *RunService_GetJob_Call {
167+ return &RunService_GetJob_Call{Call: _e.mock.On("GetJob", ctx, p, repo, jobID, attempt)}
168+}
169+
170+func (_c *RunService_GetJob_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32)) *RunService_GetJob_Call {
171+ _c.Call.Run(func(args mock.Arguments) {
172+ var arg0 context.Context
173+ if args[0] != nil {
174+ arg0 = args[0].(context.Context)
175+ }
176+ var arg1 domain.Principal
177+ if args[1] != nil {
178+ arg1 = args[1].(domain.Principal)
179+ }
180+ var arg2 domain.RepoName
181+ if args[2] != nil {
182+ arg2 = args[2].(domain.RepoName)
183+ }
184+ var arg3 string
185+ if args[3] != nil {
186+ arg3 = args[3].(string)
187+ }
188+ var arg4 int32
189+ if args[4] != nil {
190+ arg4 = args[4].(int32)
191+ }
192+ run(
193+ arg0,
194+ arg1,
195+ arg2,
196+ arg3,
197+ arg4,
198+ )
199+ })
200+ return _c
201+}
202+
203+func (_c *RunService_GetJob_Call) Return(jobView ports.JobView, err error) *RunService_GetJob_Call {
204+ _c.Call.Return(jobView, err)
205+ return _c
206+}
207+
208+func (_c *RunService_GetJob_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32) (ports.JobView, error)) *RunService_GetJob_Call {
209+ _c.Call.Return(run)
210+ return _c
211+}
212+
213+// GetJobLog provides a mock function for the type RunService
214+func (_mock *RunService) GetJobLog(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32, offset int64, follow bool, send func(ports.LogBatch) error) error {
215+ ret := _mock.Called(ctx, p, repo, jobID, attempt, offset, follow, send)
216+
217+ if len(ret) == 0 {
218+ panic("no return value specified for GetJobLog")
219+ }
220+
221+ var r0 error
222+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, string, int32, int64, bool, func(ports.LogBatch) error) error); ok {
223+ r0 = returnFunc(ctx, p, repo, jobID, attempt, offset, follow, send)
224+ } else {
225+ r0 = ret.Error(0)
226+ }
227+ return r0
228+}
229+
230+// RunService_GetJobLog_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetJobLog'
231+type RunService_GetJobLog_Call struct {
232+ *mock.Call
233+}
234+
235+// GetJobLog is a helper method to define mock.On call
236+// - ctx context.Context
237+// - p domain.Principal
238+// - repo domain.RepoName
239+// - jobID string
240+// - attempt int32
241+// - offset int64
242+// - follow bool
243+// - send func(ports.LogBatch) error
244+func (_e *RunService_Expecter) GetJobLog(ctx any, p any, repo any, jobID any, attempt any, offset any, follow any, send any) *RunService_GetJobLog_Call {
245+ return &RunService_GetJobLog_Call{Call: _e.mock.On("GetJobLog", ctx, p, repo, jobID, attempt, offset, follow, send)}
246+}
247+
248+func (_c *RunService_GetJobLog_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32, offset int64, follow bool, send func(ports.LogBatch) error)) *RunService_GetJobLog_Call {
249+ _c.Call.Run(func(args mock.Arguments) {
250+ var arg0 context.Context
251+ if args[0] != nil {
252+ arg0 = args[0].(context.Context)
253+ }
254+ var arg1 domain.Principal
255+ if args[1] != nil {
256+ arg1 = args[1].(domain.Principal)
257+ }
258+ var arg2 domain.RepoName
259+ if args[2] != nil {
260+ arg2 = args[2].(domain.RepoName)
261+ }
262+ var arg3 string
263+ if args[3] != nil {
264+ arg3 = args[3].(string)
265+ }
266+ var arg4 int32
267+ if args[4] != nil {
268+ arg4 = args[4].(int32)
269+ }
270+ var arg5 int64
271+ if args[5] != nil {
272+ arg5 = args[5].(int64)
273+ }
274+ var arg6 bool
275+ if args[6] != nil {
276+ arg6 = args[6].(bool)
277+ }
278+ var arg7 func(ports.LogBatch) error
279+ if args[7] != nil {
280+ arg7 = args[7].(func(ports.LogBatch) error)
281+ }
282+ run(
283+ arg0,
284+ arg1,
285+ arg2,
286+ arg3,
287+ arg4,
288+ arg5,
289+ arg6,
290+ arg7,
291+ )
292+ })
293+ return _c
294+}
295+
296+func (_c *RunService_GetJobLog_Call) Return(err error) *RunService_GetJobLog_Call {
297+ _c.Call.Return(err)
298+ return _c
299+}
300+
301+func (_c *RunService_GetJobLog_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32, offset int64, follow bool, send func(ports.LogBatch) error) error) *RunService_GetJobLog_Call {
302+ _c.Call.Return(run)
303+ return _c
304+}
305+
306+// GetRun provides a mock function for the type RunService
307+func (_mock *RunService) GetRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32) (ports.RunDetail, error) {
308+ ret := _mock.Called(ctx, p, repo, runID, attempt)
309+
310+ if len(ret) == 0 {
311+ panic("no return value specified for GetRun")
312+ }
313+
314+ var r0 ports.RunDetail
315+ var r1 error
316+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, int32) (ports.RunDetail, error)); ok {
317+ return returnFunc(ctx, p, repo, runID, attempt)
318+ }
319+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, int32) ports.RunDetail); ok {
320+ r0 = returnFunc(ctx, p, repo, runID, attempt)
321+ } else {
322+ r0 = ret.Get(0).(ports.RunDetail)
323+ }
324+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64, int32) error); ok {
325+ r1 = returnFunc(ctx, p, repo, runID, attempt)
326+ } else {
327+ r1 = ret.Error(1)
328+ }
329+ return r0, r1
330+}
331+
332+// RunService_GetRun_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRun'
333+type RunService_GetRun_Call struct {
334+ *mock.Call
335+}
336+
337+// GetRun is a helper method to define mock.On call
338+// - ctx context.Context
339+// - p domain.Principal
340+// - repo domain.RepoName
341+// - runID int64
342+// - attempt int32
343+func (_e *RunService_Expecter) GetRun(ctx any, p any, repo any, runID any, attempt any) *RunService_GetRun_Call {
344+ return &RunService_GetRun_Call{Call: _e.mock.On("GetRun", ctx, p, repo, runID, attempt)}
345+}
346+
347+func (_c *RunService_GetRun_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32)) *RunService_GetRun_Call {
348+ _c.Call.Run(func(args mock.Arguments) {
349+ var arg0 context.Context
350+ if args[0] != nil {
351+ arg0 = args[0].(context.Context)
352+ }
353+ var arg1 domain.Principal
354+ if args[1] != nil {
355+ arg1 = args[1].(domain.Principal)
356+ }
357+ var arg2 domain.RepoName
358+ if args[2] != nil {
359+ arg2 = args[2].(domain.RepoName)
360+ }
361+ var arg3 int64
362+ if args[3] != nil {
363+ arg3 = args[3].(int64)
364+ }
365+ var arg4 int32
366+ if args[4] != nil {
367+ arg4 = args[4].(int32)
368+ }
369+ run(
370+ arg0,
371+ arg1,
372+ arg2,
373+ arg3,
374+ arg4,
375+ )
376+ })
377+ return _c
378+}
379+
380+func (_c *RunService_GetRun_Call) Return(runDetail ports.RunDetail, err error) *RunService_GetRun_Call {
381+ _c.Call.Return(runDetail, err)
382+ return _c
383+}
384+
385+func (_c *RunService_GetRun_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32) (ports.RunDetail, error)) *RunService_GetRun_Call {
386+ _c.Call.Return(run)
387+ return _c
388+}
389+
390+// ListChecks provides a mock function for the type RunService
391+func (_mock *RunService) ListChecks(ctx context.Context, p domain.Principal, repo domain.RepoName, sha string) (ports.ChecksResult, error) {
392+ ret := _mock.Called(ctx, p, repo, sha)
393+
394+ if len(ret) == 0 {
395+ panic("no return value specified for ListChecks")
396+ }
397+
398+ var r0 ports.ChecksResult
399+ var r1 error
400+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, string) (ports.ChecksResult, error)); ok {
401+ return returnFunc(ctx, p, repo, sha)
402+ }
403+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, string) ports.ChecksResult); ok {
404+ r0 = returnFunc(ctx, p, repo, sha)
405+ } else {
406+ r0 = ret.Get(0).(ports.ChecksResult)
407+ }
408+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, string) error); ok {
409+ r1 = returnFunc(ctx, p, repo, sha)
410+ } else {
411+ r1 = ret.Error(1)
412+ }
413+ return r0, r1
414+}
415+
416+// RunService_ListChecks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListChecks'
417+type RunService_ListChecks_Call struct {
418+ *mock.Call
419+}
420+
421+// ListChecks is a helper method to define mock.On call
422+// - ctx context.Context
423+// - p domain.Principal
424+// - repo domain.RepoName
425+// - sha string
426+func (_e *RunService_Expecter) ListChecks(ctx any, p any, repo any, sha any) *RunService_ListChecks_Call {
427+ return &RunService_ListChecks_Call{Call: _e.mock.On("ListChecks", ctx, p, repo, sha)}
428+}
429+
430+func (_c *RunService_ListChecks_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, sha string)) *RunService_ListChecks_Call {
431+ _c.Call.Run(func(args mock.Arguments) {
432+ var arg0 context.Context
433+ if args[0] != nil {
434+ arg0 = args[0].(context.Context)
435+ }
436+ var arg1 domain.Principal
437+ if args[1] != nil {
438+ arg1 = args[1].(domain.Principal)
439+ }
440+ var arg2 domain.RepoName
441+ if args[2] != nil {
442+ arg2 = args[2].(domain.RepoName)
443+ }
444+ var arg3 string
445+ if args[3] != nil {
446+ arg3 = args[3].(string)
447+ }
448+ run(
449+ arg0,
450+ arg1,
451+ arg2,
452+ arg3,
453+ )
454+ })
455+ return _c
456+}
457+
458+func (_c *RunService_ListChecks_Call) Return(checksResult ports.ChecksResult, err error) *RunService_ListChecks_Call {
459+ _c.Call.Return(checksResult, err)
460+ return _c
461+}
462+
463+func (_c *RunService_ListChecks_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, sha string) (ports.ChecksResult, error)) *RunService_ListChecks_Call {
464+ _c.Call.Return(run)
465+ return _c
466+}
467+
468+// ListRunners provides a mock function for the type RunService
469+func (_mock *RunService) ListRunners(ctx context.Context, p domain.Principal, size int, token string) (ports.RunnerPage, error) {
470+ ret := _mock.Called(ctx, p, size, token)
471+
472+ if len(ret) == 0 {
473+ panic("no return value specified for ListRunners")
474+ }
475+
476+ var r0 ports.RunnerPage
477+ var r1 error
478+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, int, string) (ports.RunnerPage, error)); ok {
479+ return returnFunc(ctx, p, size, token)
480+ }
481+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, int, string) ports.RunnerPage); ok {
482+ r0 = returnFunc(ctx, p, size, token)
483+ } else {
484+ r0 = ret.Get(0).(ports.RunnerPage)
485+ }
486+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, int, string) error); ok {
487+ r1 = returnFunc(ctx, p, size, token)
488+ } else {
489+ r1 = ret.Error(1)
490+ }
491+ return r0, r1
492+}
493+
494+// RunService_ListRunners_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRunners'
495+type RunService_ListRunners_Call struct {
496+ *mock.Call
497+}
498+
499+// ListRunners is a helper method to define mock.On call
500+// - ctx context.Context
501+// - p domain.Principal
502+// - size int
503+// - token string
504+func (_e *RunService_Expecter) ListRunners(ctx any, p any, size any, token any) *RunService_ListRunners_Call {
505+ return &RunService_ListRunners_Call{Call: _e.mock.On("ListRunners", ctx, p, size, token)}
506+}
507+
508+func (_c *RunService_ListRunners_Call) Run(run func(ctx context.Context, p domain.Principal, size int, token string)) *RunService_ListRunners_Call {
509+ _c.Call.Run(func(args mock.Arguments) {
510+ var arg0 context.Context
511+ if args[0] != nil {
512+ arg0 = args[0].(context.Context)
513+ }
514+ var arg1 domain.Principal
515+ if args[1] != nil {
516+ arg1 = args[1].(domain.Principal)
517+ }
518+ var arg2 int
519+ if args[2] != nil {
520+ arg2 = args[2].(int)
521+ }
522+ var arg3 string
523+ if args[3] != nil {
524+ arg3 = args[3].(string)
525+ }
526+ run(
527+ arg0,
528+ arg1,
529+ arg2,
530+ arg3,
531+ )
532+ })
533+ return _c
534+}
535+
536+func (_c *RunService_ListRunners_Call) Return(runnerPage ports.RunnerPage, err error) *RunService_ListRunners_Call {
537+ _c.Call.Return(runnerPage, err)
538+ return _c
539+}
540+
541+func (_c *RunService_ListRunners_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, size int, token string) (ports.RunnerPage, error)) *RunService_ListRunners_Call {
542+ _c.Call.Return(run)
543+ return _c
544+}
545+
546+// ListRuns provides a mock function for the type RunService
547+func (_mock *RunService) ListRuns(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.ListRunsParams) (ports.RunPage, error) {
548+ ret := _mock.Called(ctx, p, repo, params)
549+
550+ if len(ret) == 0 {
551+ panic("no return value specified for ListRuns")
552+ }
553+
554+ var r0 ports.RunPage
555+ var r1 error
556+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.ListRunsParams) (ports.RunPage, error)); ok {
557+ return returnFunc(ctx, p, repo, params)
558+ }
559+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, ports.ListRunsParams) ports.RunPage); ok {
560+ r0 = returnFunc(ctx, p, repo, params)
561+ } else {
562+ r0 = ret.Get(0).(ports.RunPage)
563+ }
564+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, ports.ListRunsParams) error); ok {
565+ r1 = returnFunc(ctx, p, repo, params)
566+ } else {
567+ r1 = ret.Error(1)
568+ }
569+ return r0, r1
570+}
571+
572+// RunService_ListRuns_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListRuns'
573+type RunService_ListRuns_Call struct {
574+ *mock.Call
575+}
576+
577+// ListRuns is a helper method to define mock.On call
578+// - ctx context.Context
579+// - p domain.Principal
580+// - repo domain.RepoName
581+// - params ports.ListRunsParams
582+func (_e *RunService_Expecter) ListRuns(ctx any, p any, repo any, params any) *RunService_ListRuns_Call {
583+ return &RunService_ListRuns_Call{Call: _e.mock.On("ListRuns", ctx, p, repo, params)}
584+}
585+
586+func (_c *RunService_ListRuns_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.ListRunsParams)) *RunService_ListRuns_Call {
587+ _c.Call.Run(func(args mock.Arguments) {
588+ var arg0 context.Context
589+ if args[0] != nil {
590+ arg0 = args[0].(context.Context)
591+ }
592+ var arg1 domain.Principal
593+ if args[1] != nil {
594+ arg1 = args[1].(domain.Principal)
595+ }
596+ var arg2 domain.RepoName
597+ if args[2] != nil {
598+ arg2 = args[2].(domain.RepoName)
599+ }
600+ var arg3 ports.ListRunsParams
601+ if args[3] != nil {
602+ arg3 = args[3].(ports.ListRunsParams)
603+ }
604+ run(
605+ arg0,
606+ arg1,
607+ arg2,
608+ arg3,
609+ )
610+ })
611+ return _c
612+}
613+
614+func (_c *RunService_ListRuns_Call) Return(runPage ports.RunPage, err error) *RunService_ListRuns_Call {
615+ _c.Call.Return(runPage, err)
616+ return _c
617+}
618+
619+func (_c *RunService_ListRuns_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, params ports.ListRunsParams) (ports.RunPage, error)) *RunService_ListRuns_Call {
620+ _c.Call.Return(run)
621+ return _c
622+}
623+
624+// RemoveRunner provides a mock function for the type RunService
625+func (_mock *RunService) RemoveRunner(ctx context.Context, p domain.Principal, id string) error {
626+ ret := _mock.Called(ctx, p, id)
627+
628+ if len(ret) == 0 {
629+ panic("no return value specified for RemoveRunner")
630+ }
631+
632+ var r0 error
633+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, string) error); ok {
634+ r0 = returnFunc(ctx, p, id)
635+ } else {
636+ r0 = ret.Error(0)
637+ }
638+ return r0
639+}
640+
641+// RunService_RemoveRunner_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveRunner'
642+type RunService_RemoveRunner_Call struct {
643+ *mock.Call
644+}
645+
646+// RemoveRunner is a helper method to define mock.On call
647+// - ctx context.Context
648+// - p domain.Principal
649+// - id string
650+func (_e *RunService_Expecter) RemoveRunner(ctx any, p any, id any) *RunService_RemoveRunner_Call {
651+ return &RunService_RemoveRunner_Call{Call: _e.mock.On("RemoveRunner", ctx, p, id)}
652+}
653+
654+func (_c *RunService_RemoveRunner_Call) Run(run func(ctx context.Context, p domain.Principal, id string)) *RunService_RemoveRunner_Call {
655+ _c.Call.Run(func(args mock.Arguments) {
656+ var arg0 context.Context
657+ if args[0] != nil {
658+ arg0 = args[0].(context.Context)
659+ }
660+ var arg1 domain.Principal
661+ if args[1] != nil {
662+ arg1 = args[1].(domain.Principal)
663+ }
664+ var arg2 string
665+ if args[2] != nil {
666+ arg2 = args[2].(string)
667+ }
668+ run(
669+ arg0,
670+ arg1,
671+ arg2,
672+ )
673+ })
674+ return _c
675+}
676+
677+func (_c *RunService_RemoveRunner_Call) Return(err error) *RunService_RemoveRunner_Call {
678+ _c.Call.Return(err)
679+ return _c
680+}
681+
682+func (_c *RunService_RemoveRunner_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, id string) error) *RunService_RemoveRunner_Call {
683+ _c.Call.Return(run)
684+ return _c
685+}
686+
687+// RerunRun provides a mock function for the type RunService
688+func (_mock *RunService) RerunRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, failedOnly bool) (domain.CIRun, error) {
689+ ret := _mock.Called(ctx, p, repo, runID, failedOnly)
690+
691+ if len(ret) == 0 {
692+ panic("no return value specified for RerunRun")
693+ }
694+
695+ var r0 domain.CIRun
696+ var r1 error
697+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, bool) (domain.CIRun, error)); ok {
698+ return returnFunc(ctx, p, repo, runID, failedOnly)
699+ }
700+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, domain.RepoName, int64, bool) domain.CIRun); ok {
701+ r0 = returnFunc(ctx, p, repo, runID, failedOnly)
702+ } else {
703+ r0 = ret.Get(0).(domain.CIRun)
704+ }
705+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, domain.RepoName, int64, bool) error); ok {
706+ r1 = returnFunc(ctx, p, repo, runID, failedOnly)
707+ } else {
708+ r1 = ret.Error(1)
709+ }
710+ return r0, r1
711+}
712+
713+// RunService_RerunRun_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RerunRun'
714+type RunService_RerunRun_Call struct {
715+ *mock.Call
716+}
717+
718+// RerunRun is a helper method to define mock.On call
719+// - ctx context.Context
720+// - p domain.Principal
721+// - repo domain.RepoName
722+// - runID int64
723+// - failedOnly bool
724+func (_e *RunService_Expecter) RerunRun(ctx any, p any, repo any, runID any, failedOnly any) *RunService_RerunRun_Call {
725+ return &RunService_RerunRun_Call{Call: _e.mock.On("RerunRun", ctx, p, repo, runID, failedOnly)}
726+}
727+
728+func (_c *RunService_RerunRun_Call) Run(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, failedOnly bool)) *RunService_RerunRun_Call {
729+ _c.Call.Run(func(args mock.Arguments) {
730+ var arg0 context.Context
731+ if args[0] != nil {
732+ arg0 = args[0].(context.Context)
733+ }
734+ var arg1 domain.Principal
735+ if args[1] != nil {
736+ arg1 = args[1].(domain.Principal)
737+ }
738+ var arg2 domain.RepoName
739+ if args[2] != nil {
740+ arg2 = args[2].(domain.RepoName)
741+ }
742+ var arg3 int64
743+ if args[3] != nil {
744+ arg3 = args[3].(int64)
745+ }
746+ var arg4 bool
747+ if args[4] != nil {
748+ arg4 = args[4].(bool)
749+ }
750+ run(
751+ arg0,
752+ arg1,
753+ arg2,
754+ arg3,
755+ arg4,
756+ )
757+ })
758+ return _c
759+}
760+
761+func (_c *RunService_RerunRun_Call) Return(cIRun domain.CIRun, err error) *RunService_RerunRun_Call {
762+ _c.Call.Return(cIRun, err)
763+ return _c
764+}
765+
766+func (_c *RunService_RerunRun_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, failedOnly bool) (domain.CIRun, error)) *RunService_RerunRun_Call {
767+ _c.Call.Return(run)
768+ return _c
769+}
A internal/ports/mocks/RunnerService.go +556 -0
@@ -0,0 +1,556 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ "bnema.dev/ermite/internal/ports"
12+ mock "github.com/stretchr/testify/mock"
13+)
14+
15+// NewRunnerService creates a new instance of RunnerService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
16+// The first argument is typically a *testing.T value.
17+func NewRunnerService(t interface {
18+ mock.TestingT
19+ Cleanup(func())
20+}) *RunnerService {
21+ if helper, ok := t.(interface{ Helper() }); ok {
22+ helper.Helper()
23+ }
24+
25+ mock := &RunnerService{}
26+ mock.Mock.Test(t)
27+
28+ t.Cleanup(func() {
29+ if helper, ok := t.(interface{ Helper() }); ok {
30+ helper.Helper()
31+ }
32+ mock.AssertExpectations(t)
33+ })
34+
35+ return mock
36+}
37+
38+// RunnerService is an autogenerated mock type for the RunnerService type
39+type RunnerService struct {
40+ mock.Mock
41+}
42+
43+type RunnerService_Expecter struct {
44+ mock *mock.Mock
45+}
46+
47+func (_m *RunnerService) EXPECT() *RunnerService_Expecter {
48+ return &RunnerService_Expecter{mock: &_m.Mock}
49+}
50+
51+// AppendLog provides a mock function for the type RunnerService
52+func (_mock *RunnerService) AppendLog(ctx context.Context, secret string, jobID string, step int32, offset int64, lines []domain.CILogLine) (int64, error) {
53+ ret := _mock.Called(ctx, secret, jobID, step, offset, lines)
54+
55+ if len(ret) == 0 {
56+ panic("no return value specified for AppendLog")
57+ }
58+
59+ var r0 int64
60+ var r1 error
61+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, int32, int64, []domain.CILogLine) (int64, error)); ok {
62+ return returnFunc(ctx, secret, jobID, step, offset, lines)
63+ }
64+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, int32, int64, []domain.CILogLine) int64); ok {
65+ r0 = returnFunc(ctx, secret, jobID, step, offset, lines)
66+ } else {
67+ r0 = ret.Get(0).(int64)
68+ }
69+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, int32, int64, []domain.CILogLine) error); ok {
70+ r1 = returnFunc(ctx, secret, jobID, step, offset, lines)
71+ } else {
72+ r1 = ret.Error(1)
73+ }
74+ return r0, r1
75+}
76+
77+// RunnerService_AppendLog_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppendLog'
78+type RunnerService_AppendLog_Call struct {
79+ *mock.Call
80+}
81+
82+// AppendLog is a helper method to define mock.On call
83+// - ctx context.Context
84+// - secret string
85+// - jobID string
86+// - step int32
87+// - offset int64
88+// - lines []domain.CILogLine
89+func (_e *RunnerService_Expecter) AppendLog(ctx any, secret any, jobID any, step any, offset any, lines any) *RunnerService_AppendLog_Call {
90+ return &RunnerService_AppendLog_Call{Call: _e.mock.On("AppendLog", ctx, secret, jobID, step, offset, lines)}
91+}
92+
93+func (_c *RunnerService_AppendLog_Call) Run(run func(ctx context.Context, secret string, jobID string, step int32, offset int64, lines []domain.CILogLine)) *RunnerService_AppendLog_Call {
94+ _c.Call.Run(func(args mock.Arguments) {
95+ var arg0 context.Context
96+ if args[0] != nil {
97+ arg0 = args[0].(context.Context)
98+ }
99+ var arg1 string
100+ if args[1] != nil {
101+ arg1 = args[1].(string)
102+ }
103+ var arg2 string
104+ if args[2] != nil {
105+ arg2 = args[2].(string)
106+ }
107+ var arg3 int32
108+ if args[3] != nil {
109+ arg3 = args[3].(int32)
110+ }
111+ var arg4 int64
112+ if args[4] != nil {
113+ arg4 = args[4].(int64)
114+ }
115+ var arg5 []domain.CILogLine
116+ if args[5] != nil {
117+ arg5 = args[5].([]domain.CILogLine)
118+ }
119+ run(
120+ arg0,
121+ arg1,
122+ arg2,
123+ arg3,
124+ arg4,
125+ arg5,
126+ )
127+ })
128+ return _c
129+}
130+
131+func (_c *RunnerService_AppendLog_Call) Return(next int64, err error) *RunnerService_AppendLog_Call {
132+ _c.Call.Return(next, err)
133+ return _c
134+}
135+
136+func (_c *RunnerService_AppendLog_Call) RunAndReturn(run func(ctx context.Context, secret string, jobID string, step int32, offset int64, lines []domain.CILogLine) (int64, error)) *RunnerService_AppendLog_Call {
137+ _c.Call.Return(run)
138+ return _c
139+}
140+
141+// CompleteJob provides a mock function for the type RunnerService
142+func (_mock *RunnerService) CompleteJob(ctx context.Context, secret string, params ports.CompleteJobParams) (ports.CompleteJobResult, error) {
143+ ret := _mock.Called(ctx, secret, params)
144+
145+ if len(ret) == 0 {
146+ panic("no return value specified for CompleteJob")
147+ }
148+
149+ var r0 ports.CompleteJobResult
150+ var r1 error
151+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.CompleteJobParams) (ports.CompleteJobResult, error)); ok {
152+ return returnFunc(ctx, secret, params)
153+ }
154+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.CompleteJobParams) ports.CompleteJobResult); ok {
155+ r0 = returnFunc(ctx, secret, params)
156+ } else {
157+ r0 = ret.Get(0).(ports.CompleteJobResult)
158+ }
159+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, ports.CompleteJobParams) error); ok {
160+ r1 = returnFunc(ctx, secret, params)
161+ } else {
162+ r1 = ret.Error(1)
163+ }
164+ return r0, r1
165+}
166+
167+// RunnerService_CompleteJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompleteJob'
168+type RunnerService_CompleteJob_Call struct {
169+ *mock.Call
170+}
171+
172+// CompleteJob is a helper method to define mock.On call
173+// - ctx context.Context
174+// - secret string
175+// - params ports.CompleteJobParams
176+func (_e *RunnerService_Expecter) CompleteJob(ctx any, secret any, params any) *RunnerService_CompleteJob_Call {
177+ return &RunnerService_CompleteJob_Call{Call: _e.mock.On("CompleteJob", ctx, secret, params)}
178+}
179+
180+func (_c *RunnerService_CompleteJob_Call) Run(run func(ctx context.Context, secret string, params ports.CompleteJobParams)) *RunnerService_CompleteJob_Call {
181+ _c.Call.Run(func(args mock.Arguments) {
182+ var arg0 context.Context
183+ if args[0] != nil {
184+ arg0 = args[0].(context.Context)
185+ }
186+ var arg1 string
187+ if args[1] != nil {
188+ arg1 = args[1].(string)
189+ }
190+ var arg2 ports.CompleteJobParams
191+ if args[2] != nil {
192+ arg2 = args[2].(ports.CompleteJobParams)
193+ }
194+ run(
195+ arg0,
196+ arg1,
197+ arg2,
198+ )
199+ })
200+ return _c
201+}
202+
203+func (_c *RunnerService_CompleteJob_Call) Return(completeJobResult ports.CompleteJobResult, err error) *RunnerService_CompleteJob_Call {
204+ _c.Call.Return(completeJobResult, err)
205+ return _c
206+}
207+
208+func (_c *RunnerService_CompleteJob_Call) RunAndReturn(run func(ctx context.Context, secret string, params ports.CompleteJobParams) (ports.CompleteJobResult, error)) *RunnerService_CompleteJob_Call {
209+ _c.Call.Return(run)
210+ return _c
211+}
212+
213+// FetchJob provides a mock function for the type RunnerService
214+func (_mock *RunnerService) FetchJob(ctx context.Context, secret string, params ports.FetchJobParams) (ports.FetchJobResult, error) {
215+ ret := _mock.Called(ctx, secret, params)
216+
217+ if len(ret) == 0 {
218+ panic("no return value specified for FetchJob")
219+ }
220+
221+ var r0 ports.FetchJobResult
222+ var r1 error
223+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.FetchJobParams) (ports.FetchJobResult, error)); ok {
224+ return returnFunc(ctx, secret, params)
225+ }
226+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, ports.FetchJobParams) ports.FetchJobResult); ok {
227+ r0 = returnFunc(ctx, secret, params)
228+ } else {
229+ r0 = ret.Get(0).(ports.FetchJobResult)
230+ }
231+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, ports.FetchJobParams) error); ok {
232+ r1 = returnFunc(ctx, secret, params)
233+ } else {
234+ r1 = ret.Error(1)
235+ }
236+ return r0, r1
237+}
238+
239+// RunnerService_FetchJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FetchJob'
240+type RunnerService_FetchJob_Call struct {
241+ *mock.Call
242+}
243+
244+// FetchJob is a helper method to define mock.On call
245+// - ctx context.Context
246+// - secret string
247+// - params ports.FetchJobParams
248+func (_e *RunnerService_Expecter) FetchJob(ctx any, secret any, params any) *RunnerService_FetchJob_Call {
249+ return &RunnerService_FetchJob_Call{Call: _e.mock.On("FetchJob", ctx, secret, params)}
250+}
251+
252+func (_c *RunnerService_FetchJob_Call) Run(run func(ctx context.Context, secret string, params ports.FetchJobParams)) *RunnerService_FetchJob_Call {
253+ _c.Call.Run(func(args mock.Arguments) {
254+ var arg0 context.Context
255+ if args[0] != nil {
256+ arg0 = args[0].(context.Context)
257+ }
258+ var arg1 string
259+ if args[1] != nil {
260+ arg1 = args[1].(string)
261+ }
262+ var arg2 ports.FetchJobParams
263+ if args[2] != nil {
264+ arg2 = args[2].(ports.FetchJobParams)
265+ }
266+ run(
267+ arg0,
268+ arg1,
269+ arg2,
270+ )
271+ })
272+ return _c
273+}
274+
275+func (_c *RunnerService_FetchJob_Call) Return(fetchJobResult ports.FetchJobResult, err error) *RunnerService_FetchJob_Call {
276+ _c.Call.Return(fetchJobResult, err)
277+ return _c
278+}
279+
280+func (_c *RunnerService_FetchJob_Call) RunAndReturn(run func(ctx context.Context, secret string, params ports.FetchJobParams) (ports.FetchJobResult, error)) *RunnerService_FetchJob_Call {
281+ _c.Call.Return(run)
282+ return _c
283+}
284+
285+// Heartbeat provides a mock function for the type RunnerService
286+func (_mock *RunnerService) Heartbeat(ctx context.Context, secret string, jobID string) error {
287+ ret := _mock.Called(ctx, secret, jobID)
288+
289+ if len(ret) == 0 {
290+ panic("no return value specified for Heartbeat")
291+ }
292+
293+ var r0 error
294+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
295+ r0 = returnFunc(ctx, secret, jobID)
296+ } else {
297+ r0 = ret.Error(0)
298+ }
299+ return r0
300+}
301+
302+// RunnerService_Heartbeat_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Heartbeat'
303+type RunnerService_Heartbeat_Call struct {
304+ *mock.Call
305+}
306+
307+// Heartbeat is a helper method to define mock.On call
308+// - ctx context.Context
309+// - secret string
310+// - jobID string
311+func (_e *RunnerService_Expecter) Heartbeat(ctx any, secret any, jobID any) *RunnerService_Heartbeat_Call {
312+ return &RunnerService_Heartbeat_Call{Call: _e.mock.On("Heartbeat", ctx, secret, jobID)}
313+}
314+
315+func (_c *RunnerService_Heartbeat_Call) Run(run func(ctx context.Context, secret string, jobID string)) *RunnerService_Heartbeat_Call {
316+ _c.Call.Run(func(args mock.Arguments) {
317+ var arg0 context.Context
318+ if args[0] != nil {
319+ arg0 = args[0].(context.Context)
320+ }
321+ var arg1 string
322+ if args[1] != nil {
323+ arg1 = args[1].(string)
324+ }
325+ var arg2 string
326+ if args[2] != nil {
327+ arg2 = args[2].(string)
328+ }
329+ run(
330+ arg0,
331+ arg1,
332+ arg2,
333+ )
334+ })
335+ return _c
336+}
337+
338+func (_c *RunnerService_Heartbeat_Call) Return(err error) *RunnerService_Heartbeat_Call {
339+ _c.Call.Return(err)
340+ return _c
341+}
342+
343+func (_c *RunnerService_Heartbeat_Call) RunAndReturn(run func(ctx context.Context, secret string, jobID string) error) *RunnerService_Heartbeat_Call {
344+ _c.Call.Return(run)
345+ return _c
346+}
347+
348+// Register provides a mock function for the type RunnerService
349+func (_mock *RunnerService) Register(ctx context.Context, p domain.Principal, params ports.RegisterRunnerParams) (ports.RegisteredRunner, error) {
350+ ret := _mock.Called(ctx, p, params)
351+
352+ if len(ret) == 0 {
353+ panic("no return value specified for Register")
354+ }
355+
356+ var r0 ports.RegisteredRunner
357+ var r1 error
358+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, ports.RegisterRunnerParams) (ports.RegisteredRunner, error)); ok {
359+ return returnFunc(ctx, p, params)
360+ }
361+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.Principal, ports.RegisterRunnerParams) ports.RegisteredRunner); ok {
362+ r0 = returnFunc(ctx, p, params)
363+ } else {
364+ r0 = ret.Get(0).(ports.RegisteredRunner)
365+ }
366+ if returnFunc, ok := ret.Get(1).(func(context.Context, domain.Principal, ports.RegisterRunnerParams) error); ok {
367+ r1 = returnFunc(ctx, p, params)
368+ } else {
369+ r1 = ret.Error(1)
370+ }
371+ return r0, r1
372+}
373+
374+// RunnerService_Register_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Register'
375+type RunnerService_Register_Call struct {
376+ *mock.Call
377+}
378+
379+// Register is a helper method to define mock.On call
380+// - ctx context.Context
381+// - p domain.Principal
382+// - params ports.RegisterRunnerParams
383+func (_e *RunnerService_Expecter) Register(ctx any, p any, params any) *RunnerService_Register_Call {
384+ return &RunnerService_Register_Call{Call: _e.mock.On("Register", ctx, p, params)}
385+}
386+
387+func (_c *RunnerService_Register_Call) Run(run func(ctx context.Context, p domain.Principal, params ports.RegisterRunnerParams)) *RunnerService_Register_Call {
388+ _c.Call.Run(func(args mock.Arguments) {
389+ var arg0 context.Context
390+ if args[0] != nil {
391+ arg0 = args[0].(context.Context)
392+ }
393+ var arg1 domain.Principal
394+ if args[1] != nil {
395+ arg1 = args[1].(domain.Principal)
396+ }
397+ var arg2 ports.RegisterRunnerParams
398+ if args[2] != nil {
399+ arg2 = args[2].(ports.RegisterRunnerParams)
400+ }
401+ run(
402+ arg0,
403+ arg1,
404+ arg2,
405+ )
406+ })
407+ return _c
408+}
409+
410+func (_c *RunnerService_Register_Call) Return(registeredRunner ports.RegisteredRunner, err error) *RunnerService_Register_Call {
411+ _c.Call.Return(registeredRunner, err)
412+ return _c
413+}
414+
415+func (_c *RunnerService_Register_Call) RunAndReturn(run func(ctx context.Context, p domain.Principal, params ports.RegisterRunnerParams) (ports.RegisteredRunner, error)) *RunnerService_Register_Call {
416+ _c.Call.Return(run)
417+ return _c
418+}
419+
420+// UpdateJob provides a mock function for the type RunnerService
421+func (_mock *RunnerService) UpdateJob(ctx context.Context, secret string, jobID string, steps []ports.StepUpdate) error {
422+ ret := _mock.Called(ctx, secret, jobID, steps)
423+
424+ if len(ret) == 0 {
425+ panic("no return value specified for UpdateJob")
426+ }
427+
428+ var r0 error
429+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []ports.StepUpdate) error); ok {
430+ r0 = returnFunc(ctx, secret, jobID, steps)
431+ } else {
432+ r0 = ret.Error(0)
433+ }
434+ return r0
435+}
436+
437+// RunnerService_UpdateJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateJob'
438+type RunnerService_UpdateJob_Call struct {
439+ *mock.Call
440+}
441+
442+// UpdateJob is a helper method to define mock.On call
443+// - ctx context.Context
444+// - secret string
445+// - jobID string
446+// - steps []ports.StepUpdate
447+func (_e *RunnerService_Expecter) UpdateJob(ctx any, secret any, jobID any, steps any) *RunnerService_UpdateJob_Call {
448+ return &RunnerService_UpdateJob_Call{Call: _e.mock.On("UpdateJob", ctx, secret, jobID, steps)}
449+}
450+
451+func (_c *RunnerService_UpdateJob_Call) Run(run func(ctx context.Context, secret string, jobID string, steps []ports.StepUpdate)) *RunnerService_UpdateJob_Call {
452+ _c.Call.Run(func(args mock.Arguments) {
453+ var arg0 context.Context
454+ if args[0] != nil {
455+ arg0 = args[0].(context.Context)
456+ }
457+ var arg1 string
458+ if args[1] != nil {
459+ arg1 = args[1].(string)
460+ }
461+ var arg2 string
462+ if args[2] != nil {
463+ arg2 = args[2].(string)
464+ }
465+ var arg3 []ports.StepUpdate
466+ if args[3] != nil {
467+ arg3 = args[3].([]ports.StepUpdate)
468+ }
469+ run(
470+ arg0,
471+ arg1,
472+ arg2,
473+ arg3,
474+ )
475+ })
476+ return _c
477+}
478+
479+func (_c *RunnerService_UpdateJob_Call) Return(err error) *RunnerService_UpdateJob_Call {
480+ _c.Call.Return(err)
481+ return _c
482+}
483+
484+func (_c *RunnerService_UpdateJob_Call) RunAndReturn(run func(ctx context.Context, secret string, jobID string, steps []ports.StepUpdate) error) *RunnerService_UpdateJob_Call {
485+ _c.Call.Return(run)
486+ return _c
487+}
488+
489+// WatchJob provides a mock function for the type RunnerService
490+func (_mock *RunnerService) WatchJob(ctx context.Context, secret string, jobID string, send func(ports.JobSignal) error) error {
491+ ret := _mock.Called(ctx, secret, jobID, send)
492+
493+ if len(ret) == 0 {
494+ panic("no return value specified for WatchJob")
495+ }
496+
497+ var r0 error
498+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, func(ports.JobSignal) error) error); ok {
499+ r0 = returnFunc(ctx, secret, jobID, send)
500+ } else {
501+ r0 = ret.Error(0)
502+ }
503+ return r0
504+}
505+
506+// RunnerService_WatchJob_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WatchJob'
507+type RunnerService_WatchJob_Call struct {
508+ *mock.Call
509+}
510+
511+// WatchJob is a helper method to define mock.On call
512+// - ctx context.Context
513+// - secret string
514+// - jobID string
515+// - send func(ports.JobSignal) error
516+func (_e *RunnerService_Expecter) WatchJob(ctx any, secret any, jobID any, send any) *RunnerService_WatchJob_Call {
517+ return &RunnerService_WatchJob_Call{Call: _e.mock.On("WatchJob", ctx, secret, jobID, send)}
518+}
519+
520+func (_c *RunnerService_WatchJob_Call) Run(run func(ctx context.Context, secret string, jobID string, send func(ports.JobSignal) error)) *RunnerService_WatchJob_Call {
521+ _c.Call.Run(func(args mock.Arguments) {
522+ var arg0 context.Context
523+ if args[0] != nil {
524+ arg0 = args[0].(context.Context)
525+ }
526+ var arg1 string
527+ if args[1] != nil {
528+ arg1 = args[1].(string)
529+ }
530+ var arg2 string
531+ if args[2] != nil {
532+ arg2 = args[2].(string)
533+ }
534+ var arg3 func(ports.JobSignal) error
535+ if args[3] != nil {
536+ arg3 = args[3].(func(ports.JobSignal) error)
537+ }
538+ run(
539+ arg0,
540+ arg1,
541+ arg2,
542+ arg3,
543+ )
544+ })
545+ return _c
546+}
547+
548+func (_c *RunnerService_WatchJob_Call) Return(err error) *RunnerService_WatchJob_Call {
549+ _c.Call.Return(err)
550+ return _c
551+}
552+
553+func (_c *RunnerService_WatchJob_Call) RunAndReturn(run func(ctx context.Context, secret string, jobID string, send func(ports.JobSignal) error) error) *RunnerService_WatchJob_Call {
554+ _c.Call.Return(run)
555+ return _c
556+}
A internal/ports/mocks/WorkflowTrigger.go +106 -0
@@ -0,0 +1,106 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package mocks
6+
7+import (
8+ "context"
9+
10+ "bnema.dev/ermite/internal/domain"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// NewWorkflowTrigger creates a new instance of WorkflowTrigger. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func NewWorkflowTrigger(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *WorkflowTrigger {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &WorkflowTrigger{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// WorkflowTrigger is an autogenerated mock type for the WorkflowTrigger type
38+type WorkflowTrigger struct {
39+ mock.Mock
40+}
41+
42+type WorkflowTrigger_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *WorkflowTrigger) EXPECT() *WorkflowTrigger_Expecter {
47+ return &WorkflowTrigger_Expecter{mock: &_m.Mock}
48+}
49+
50+// OnPush provides a mock function for the type WorkflowTrigger
51+func (_mock *WorkflowTrigger) OnPush(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, pusher domain.Principal) {
52+ _mock.Called(ctx, repo, updates, pusher)
53+ return
54+}
55+
56+// WorkflowTrigger_OnPush_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OnPush'
57+type WorkflowTrigger_OnPush_Call struct {
58+ *mock.Call
59+}
60+
61+// OnPush is a helper method to define mock.On call
62+// - ctx context.Context
63+// - repo domain.RepoName
64+// - updates []domain.RefUpdate
65+// - pusher domain.Principal
66+func (_e *WorkflowTrigger_Expecter) OnPush(ctx any, repo any, updates any, pusher any) *WorkflowTrigger_OnPush_Call {
67+ return &WorkflowTrigger_OnPush_Call{Call: _e.mock.On("OnPush", ctx, repo, updates, pusher)}
68+}
69+
70+func (_c *WorkflowTrigger_OnPush_Call) Run(run func(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, pusher domain.Principal)) *WorkflowTrigger_OnPush_Call {
71+ _c.Call.Run(func(args mock.Arguments) {
72+ var arg0 context.Context
73+ if args[0] != nil {
74+ arg0 = args[0].(context.Context)
75+ }
76+ var arg1 domain.RepoName
77+ if args[1] != nil {
78+ arg1 = args[1].(domain.RepoName)
79+ }
80+ var arg2 []domain.RefUpdate
81+ if args[2] != nil {
82+ arg2 = args[2].([]domain.RefUpdate)
83+ }
84+ var arg3 domain.Principal
85+ if args[3] != nil {
86+ arg3 = args[3].(domain.Principal)
87+ }
88+ run(
89+ arg0,
90+ arg1,
91+ arg2,
92+ arg3,
93+ )
94+ })
95+ return _c
96+}
97+
98+func (_c *WorkflowTrigger_OnPush_Call) Return() *WorkflowTrigger_OnPush_Call {
99+ _c.Call.Return()
100+ return _c
101+}
102+
103+func (_c *WorkflowTrigger_OnPush_Call) RunAndReturn(run func(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, pusher domain.Principal)) *WorkflowTrigger_OnPush_Call {
104+ _c.Run(run)
105+ return _c
106+}
A proto/ermite/storage/ci.proto +194 -0
@@ -0,0 +1,194 @@
1+// SPDX-License-Identifier: AGPL-3.0-or-later
2+
3+// Internal, unversioned storage schema of ermite: CI runs, jobs, checks and
4+// runners (ADR 0025). Logs are not stored here: they are files under
5+// $ERMITE_DATA_DIR/ci/logs.
6+
7+syntax = "proto3";
8+
9+package ermite.storage;
10+
11+import "google/protobuf/timestamp.proto";
12+
13+option go_package = "bnema.dev/ermite/gen/ermite/storage;storagepb";
14+
15+// CIStatus is where a run, a job or a check is in its life.
16+enum CIStatus {
17+ CI_STATUS_UNSPECIFIED = 0;
18+ CI_STATUS_QUEUED = 1;
19+ CI_STATUS_IN_PROGRESS = 2;
20+ CI_STATUS_COMPLETED = 3;
21+}
22+
23+// CIConclusion is how a run, a job, a step or a check ended.
24+enum CIConclusion {
25+ CI_CONCLUSION_UNSPECIFIED = 0;
26+ CI_CONCLUSION_SUCCESS = 1;
27+ CI_CONCLUSION_FAILURE = 2;
28+ CI_CONCLUSION_CANCELLED = 3;
29+ CI_CONCLUSION_SKIPPED = 4;
30+ CI_CONCLUSION_TIMED_OUT = 5;
31+ CI_CONCLUSION_NEUTRAL = 6;
32+ CI_CONCLUSION_ACTION_REQUIRED = 7;
33+}
34+
35+// CIStepState is where a step is in its life.
36+enum CIStepState {
37+ CI_STEP_STATE_UNSPECIFIED = 0;
38+ CI_STEP_STATE_PENDING = 1;
39+ CI_STEP_STATE_IN_PROGRESS = 2;
40+ CI_STEP_STATE_COMPLETED = 3;
41+}
42+
43+// CIJobPhase is where a job is for the server.
44+enum CIJobPhase {
45+ CI_JOB_PHASE_UNSPECIFIED = 0;
46+ // Waits for the jobs it needs, or for a free max-parallel slot.
47+ CI_JOB_PHASE_WAITING = 1;
48+ // Planned and waiting for a runner.
49+ CI_JOB_PHASE_READY = 2;
50+ // Given to a runner that has not acknowledged it yet.
51+ CI_JOB_PHASE_ASSIGNED = 3;
52+ // Acknowledged by its runner.
53+ CI_JOB_PHASE_RUNNING = 4;
54+ // Over.
55+ CI_JOB_PHASE_ENDED = 5;
56+}
57+
58+// CIIsolation is how a runner isolates jobs.
59+enum CIIsolation {
60+ CI_ISOLATION_UNSPECIFIED = 0;
61+ CI_ISOLATION_VM = 1;
62+ CI_ISOLATION_CONTAINER_SYSBOX = 2;
63+ CI_ISOLATION_CONTAINER_PRIVILEGED = 3;
64+}
65+
66+// CIRun is stored under "run/<repo>/<id:020d>".
67+message CIRun {
68+ string repo = 1;
69+ int64 id = 2;
70+ int64 number = 3;
71+ int32 attempt = 4;
72+ string workflow_path = 5;
73+ string workflow_name = 6;
74+ string title = 7;
75+ string event = 8;
76+ string ref = 9;
77+ string sha = 10;
78+ string actor = 11;
79+ CIStatus status = 12;
80+ CIConclusion conclusion = 13;
81+ string error = 14;
82+ google.protobuf.Timestamp created_at = 15;
83+ google.protobuf.Timestamp started_at = 16;
84+ google.protobuf.Timestamp completed_at = 17;
85+ bool cancel_requested = 20;
86+ google.protobuf.Timestamp cancel_requested_at = 21;
87+}
88+
89+// CIRunSource is stored under "runsrc/<run id:020d>": what a run was planned
90+// from, kept apart so that listing runs does not decode it.
91+message CIRunSource {
92+ int64 run_id = 4;
93+ // The workflow file at sha, as is.
94+ string workflow_yaml = 1;
95+ // The "github" context of the run as JSON, without "job" and "run_attempt".
96+ string github_json = 2;
97+ // Repository of the run: the key of the run record is run/<repo>/<id>, and
98+ // the runner side knows a run by its id only.
99+ string repo = 3;
100+}
101+
102+// CIStep is a step of a job.
103+message CIStep {
104+ int32 number = 1;
105+ string name = 2;
106+ CIStepState state = 3;
107+ CIConclusion conclusion = 4;
108+ google.protobuf.Timestamp started_at = 5;
109+ google.protobuf.Timestamp completed_at = 6;
110+ // Number of the first log line of the step; meaningful with has_lines. The
111+ // end of a step is the start of the next step that has lines, or the line
112+ // count of the job: it is never stored.
113+ int64 log_start = 7;
114+ bool has_lines = 9;
115+}
116+
117+// CIJob is stored under "job/<run_id:020d>/<run_attempt:010d>/<job id>".
118+message CIJob {
119+ string id = 1;
120+ int64 run_id = 2;
121+ // Attempt in which the job executed (the one of its log).
122+ int32 attempt = 3;
123+ string repo = 4;
124+ string key = 5;
125+ string name = 6;
126+ CIJobPhase phase = 7;
127+ CIConclusion conclusion = 8;
128+ string error = 9;
129+ repeated string labels = 10;
130+ repeated string needs = 11;
131+ string runner_id = 12;
132+ string runner_name = 13;
133+ CIIsolation isolation = 14;
134+ repeated CIStep steps = 15;
135+ google.protobuf.Timestamp created_at = 16;
136+ google.protobuf.Timestamp started_at = 17;
137+ google.protobuf.Timestamp completed_at = 18;
138+ // Position of the job in the workflow file, and of the instance in its
139+ // matrix. A combo of -1 is a job whose matrix is not expanded yet.
140+ int32 index = 19;
141+ int32 combo = 20;
142+ string matrix_json = 21;
143+ // Number of instances of the job's matrix, for the strategy context.
144+ int32 combo_total = 22;
145+ int32 timeout_minutes = 23;
146+ bool fail_fast = 24;
147+ int32 max_parallel = 25;
148+ map<string, string> outputs = 26;
149+ bytes token_hash = 27;
150+ google.protobuf.Timestamp ready_at = 28;
151+ google.protobuf.Timestamp assigned_at = 29;
152+ google.protobuf.Timestamp acked_at = 30;
153+ int32 assign_cancels = 31;
154+ google.protobuf.Timestamp token_valid_until = 32;
155+ int32 cancel_reason = 33;
156+ string cancel_message = 34;
157+ google.protobuf.Timestamp cancel_requested_at = 35;
158+ // Set when the job is planned: runs-on, name and contexts are evaluated.
159+ bool planned = 37;
160+ // Attempt of the run this record belongs to; it is the key. A job kept by a
161+ // re-run of the failed jobs is copied with the run attempt of the re-run and
162+ // keeps its own attempt.
163+ int32 run_attempt = 38;
164+}
165+
166+// CICheck is stored under "check/<repo>/<sha>/<job id>".
167+message CICheck {
168+ string id = 1;
169+ string repo = 2;
170+ string sha = 3;
171+ string name = 4;
172+ CIStatus status = 5;
173+ CIConclusion conclusion = 6;
174+ int64 run_id = 7;
175+ string job_id = 8;
176+ int32 attempt = 9;
177+ string workflow_name = 10;
178+ google.protobuf.Timestamp created_at = 11;
179+ google.protobuf.Timestamp started_at = 12;
180+ google.protobuf.Timestamp completed_at = 13;
181+}
182+
183+// CIRunner is stored under "runner/<id>".
184+message CIRunner {
185+ string id = 1;
186+ string name = 2;
187+ repeated string labels = 3;
188+ CIIsolation isolation = 4;
189+ string version = 5;
190+ string arch = 6;
191+ string os = 7;
192+ google.protobuf.Timestamp registered_at = 8;
193+ bytes token_hash = 9;
194+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 44

↑↓ moveenter openesc close