1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package ports
4
5import (
6 "context"
7 "errors"
8 "strconv"
9 "time"
10
11 "bnema.dev/ermite/internal/domain"
12)
13
14// CIRunDraft is what CreateRun stores: a run with its source, its jobs and
15// their checks, built once the store has chosen the ids.
16type CIRunDraft struct {
17 Run domain.CIRun
18 Source domain.CIRunSource
19 Jobs []domain.CIJob
20 Checks []domain.CICheck
21}
22
23// CIWrite is a set of records written in one atomic batch. A run or a job with
24// Version 0 must not exist yet; any other must be unchanged since it was read.
25// Checks are derived from the jobs and upserted without a precondition.
26// Apply returns domain.ErrConflict otherwise and domain.ErrNotFound when the
27// repository no longer exists.
28//
29// RepoTokens are job repository tokens created with the batch (they must not
30// exist yet), RevokeJobs the jobs whose repository tokens are deleted by it,
31// and Resolutions the action resolutions of a run attempt to keep. They are
32// written under the lock of the run that owns them.
33//
34// Annotations replace, for each (execution of a job, step) they name, all the
35// annotations stored for it (none deletes them); ClearAnnotations deletes all
36// the annotations of the executions of jobs they name, steps included. Both
37// are written with the batch, so a call is applied whole or not at all.
38type CIWrite struct {
39 Runs []domain.CIRun
40 Jobs []domain.CIJob
41 Checks []domain.CICheck
42 RepoTokens []domain.CIRepoToken
43 RevokeJobs []CIJobRef
44 Resolutions []domain.CIActionResolution
45 Annotations []CIStepAnnotations
46 ClearAnnotations []CILogRef
47}
48
49// CIStepAnnotations are all the annotations of one step of one execution of a
50// job, in the order the step produced them.
51type CIStepAnnotations struct {
52 Repo domain.RepoName
53 Exec CILogRef
54 Step int32
55 // Items are the annotations; none removes those of the step.
56 Items []domain.CIAnnotation
57}
58
59// CIJobRef names one execution of a job.
60type CIJobRef struct {
61 RunID int64
62 RunAttempt int32
63 JobID string
64}
65
66// CIRepoTokenStore reads the job repository tokens, for the authentication of
67// a request. They are written by CIStore.Apply. *kvstore.CI implements it.
68type CIRepoTokenStore interface {
69 // FindRepoTokenByHash returns the token whose secret has that hash, or
70 // domain.ErrNotFound.
71 FindRepoTokenByHash(ctx context.Context, hash domain.TokenHash) (domain.CIRepoToken, error)
72 // GetRepoToken returns the token with that id, or domain.ErrNotFound.
73 GetRepoToken(ctx context.Context, id string) (domain.CIRepoToken, error)
74}
75
76// CIStore keeps CI runs, jobs and checks. The store derives its own indexes
77// (the queue of ready jobs, the active jobs, the open runs, the job tokens)
78// from the records it writes.
79type CIStore interface {
80 // CreateRun takes the next run id and the next number of the workflow in
81 // the repository (both never reused), calls build with them and stores
82 // the draft it returns, in one batch that fails with domain.ErrNotFound
83 // once the repository is deleted. The records come back with their
84 // versions.
85 CreateRun(ctx context.Context, repo domain.RepoName, workflowPath string, build func(id, number int64) CIRunDraft) (CIRunDraft, error)
86 // Apply writes the records in one batch and returns the version they all
87 // have afterwards.
88 Apply(ctx context.Context, w CIWrite) (version uint64, err error)
89 // GetRun returns the run, or domain.ErrNotFound.
90 GetRun(ctx context.Context, repo domain.RepoName, id int64) (domain.CIRun, error)
91 // GetActionResolution returns the resolution kept for the key in the run
92 // attempt, or domain.ErrNotFound.
93 GetActionResolution(ctx context.Context, runID int64, runAttempt int32, key string) (domain.CIActionResolution, error)
94 // GetRunSource returns what the run was planned from, or
95 // domain.ErrNotFound. It is how a run is found from its id alone.
96 GetRunSource(ctx context.Context, runID int64) (domain.CIRunSource, error)
97 // ListRuns returns the runs of the repository with an id below before (0
98 // for no bound), newest first, at most limit.
99 ListRuns(ctx context.Context, repo domain.RepoName, before int64, limit int) ([]domain.CIRun, error)
100 // RunIDs returns the ids of the runs of the repository.
101 RunIDs(ctx context.Context, repo domain.RepoName) ([]int64, error)
102 // ListOpenRuns returns the runs that have not completed.
103 ListOpenRuns(ctx context.Context) ([]domain.CIRun, error)
104 // JobAnnotations returns the annotations of one execution of a job
105 // (CIJob.Attempt), ordered by step then by the order the step produced
106 // them. A job without annotation gives an empty list.
107 JobAnnotations(ctx context.Context, runID int64, attempt int32, jobID string) ([]domain.CIAnnotation, error)
108 // GetJob returns a job of a run at an attempt of the run, or
109 // domain.ErrNotFound.
110 GetJob(ctx context.Context, runID int64, runAttempt int32, jobID string) (domain.CIJob, error)
111 // ListJobs returns the jobs of a run at an attempt, ordered by index then
112 // combination.
113 ListJobs(ctx context.Context, runID int64, runAttempt int32) ([]domain.CIJob, error)
114 // FindJobByToken returns the job whose current token has that hash and
115 // whose token is still valid at now, or domain.ErrNotFound.
116 FindJobByToken(ctx context.Context, hash domain.TokenHash, now time.Time) (domain.CIJob, error)
117 // ListReadyJobs returns the jobs that wait for a runner, oldest first, at
118 // most limit.
119 ListReadyJobs(ctx context.Context, limit int) ([]domain.CIJob, error)
120 // ListActiveJobs returns the jobs given to a runner and not over.
121 ListActiveJobs(ctx context.Context) ([]domain.CIJob, error)
122 // ListChecks returns the checks of a commit, at most limit; more is true
123 // when there are more.
124 ListChecks(ctx context.Context, repo domain.RepoName, sha string, limit int) (checks []domain.CICheck, more bool, err error)
125 // Sweep removes the runs completed before the time, with their jobs,
126 // checks and sources (not their numbers), and returns their ids so that
127 // the logs can go too.
128 Sweep(ctx context.Context, before time.Time) ([]int64, error)
129 // CreateRunner stores the runner and the index of its token. A name in use
130 // is domain.ErrAlreadyExists, or with replace the old runner is removed in
131 // the same batch and returned.
132 CreateRunner(ctx context.Context, r domain.CIRunner, replace bool) (removed []domain.CIRunner, err error)
133 // GetRunner returns the runner, or domain.ErrNotFound.
134 GetRunner(ctx context.Context, id string) (domain.CIRunner, error)
135 // FindRunnerByToken returns the runner whose token has that hash, or
136 // domain.ErrNotFound.
137 FindRunnerByToken(ctx context.Context, hash domain.TokenHash) (domain.CIRunner, error)
138 // ListRunners returns every runner ordered by name.
139 ListRunners(ctx context.Context) ([]domain.CIRunner, error)
140 // RemoveRunner removes the runner and its token, or returns
141 // domain.ErrNotFound.
142 RemoveRunner(ctx context.Context, id string) (domain.CIRunner, error)
143}
144
145// CIRunCleaner is what repository deletion needs from CI: the runs of the
146// repository before its records go, and the removal of their logs and
147// summaries after. *usecase.CI implements it.
148type CIRunCleaner interface {
149 RunIDs(ctx context.Context, repo domain.RepoName) ([]int64, error)
150 RemoveLogs(ctx context.Context, runIDs []int64) error
151}
152
153// CIChecksReader is the read side of the checks, for the merge rule and the
154// pages that show them. *usecase.CI implements it.
155type CIChecksReader interface {
156 // CommitChecks returns the checks of a commit, without access control (the
157 // caller has checked it). More than 1,000 is domain.ErrResourceExhausted,
158 // not a part of them: a rule must not decide on some of the checks.
159 CommitChecks(ctx context.Context, repo domain.RepoName, sha string) ([]domain.CICheck, error)
160}
161
162// CILogRef names one execution of a job: its log, its summaries and its
163// annotations are kept under it.
164type CILogRef struct {
165 RunID int64
166 JobID string
167 Attempt int32
168}
169
170// ErrCILogLimit is returned by CILogStore.Append when the lines do not fit in
171// the log limit; nothing was appended.
172var ErrCILogLimit = errors.New("log limit reached")
173
174// ErrCILogGap wraps domain.ErrOutOfRange when an offset is above the count.
175type ErrCILogGap struct{ Count int64 }
176
177// Error implements error.
178func (e *ErrCILogGap) Error() string {
179 return "out of range: the log holds lines up to the offset of " + strconv.FormatInt(e.Count, 10)
180}
181
182// Is makes errors.Is(err, domain.ErrOutOfRange) true.
183func (e *ErrCILogGap) Is(target error) bool { return target == domain.ErrOutOfRange }
184
185// CILogStore keeps the logs of the jobs: append-only files, one line per log
186// line, never in the metadata store. Lines are numbered from 0.
187type CILogStore interface {
188 // Append appends the lines at offset (the number of the first line), with
189 // the idempotence of AppendLog: lines already stored are skipped, a gap is
190 // an *ErrCILogGap. It returns the number of lines after the call and how
191 // many of the request were new. A log that would exceed limit bytes gets
192 // nothing and ErrCILogLimit.
193 Append(ctx context.Context, ref CILogRef, offset int64, lines []domain.CILogLine, limit int64) (next int64, added int, err error)
194 // AppendForced appends lines whatever the limit (the last line of a log
195 // that reached it).
196 AppendForced(ctx context.Context, ref CILogRef, lines []domain.CILogLine) (next int64, err error)
197 // Count returns the number of lines held, 0 for a log that does not exist.
198 Count(ctx context.Context, ref CILogRef) (int64, error)
199 // Read returns lines from the one numbered from, at most maxLines lines and
200 // about maxBytes of text. It returns domain.ErrNotFound when the log does
201 // not exist and domain.ErrOutOfRange when from is above the count.
202 Read(ctx context.Context, ref CILogRef, from int64, maxLines, maxBytes int) ([]domain.CILogLine, error)
203 // Changed returns a channel that is closed at the next append to the log.
204 Changed(ref CILogRef) <-chan struct{}
205 // Reset empties the log.
206 Reset(ctx context.Context, ref CILogRef) error
207 // RemoveRuns deletes the logs of the runs.
208 RemoveRuns(ctx context.Context, runIDs []int64) error
209 // RunIDs lists the runs that have a log directory, for the orphan sweep.
210 RunIDs(ctx context.Context) ([]int64, error)
211}
212
213// CISummaryStore keeps the job summaries (the Markdown a step wrote to
214// GITHUB_STEP_SUMMARY): one file per step of an execution of a job, never in
215// the metadata store.
216type CISummaryStore interface {
217 // Put stores the summary of a step, replacing the one it has; the file
218 // appears whole or not at all. An empty markdown removes the summary.
219 Put(ctx context.Context, ref CILogRef, step int32, markdown string) error
220 // List returns the summaries of the steps that have one, by step number,
221 // at most limit; more is true when there are others. With maxBytes above
222 // 0 the files are sized (stat) before they are read, and a summary that
223 // would take the total of the Markdown read past maxBytes is returned
224 // Omitted, unread, as are all the ones after it: a call never reads more
225 // than maxBytes.
226 List(ctx context.Context, ref CILogRef, limit int, maxBytes int64) (summaries []domain.CIStepSummary, more bool, err error)
227 // RemoveJob deletes the summaries of one execution of a job.
228 RemoveJob(ctx context.Context, ref CILogRef) error
229 // RemoveRuns deletes the summaries of the runs.
230 RemoveRuns(ctx context.Context, runIDs []int64) error
231 // RunIDs lists the runs that have a summary directory, for the orphan
232 // sweep.
233 RunIDs(ctx context.Context) ([]int64, error)
234}
235
236// WorkflowTrigger starts the workflows that match pushed refs. OnPush only
237// queues the work and returns: planning happens in the background and its
238// errors never reach the push.
239type WorkflowTrigger interface {
240 OnPush(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, pusher domain.Principal)
241 // OnMerge is OnPush for the move of a target branch by a merge. GitHub shows
242 // the owner as the actor of the push of a merge, whoever merged; but the
243 // rights the run's actions are resolved with are those of merger, the
244 // principal that merged, never the owner's.
245 OnMerge(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, merger domain.Principal)
246}
247
248// CIGit is what planning needs from Git beyond RepoReader and MergeGit.
249type CIGit interface {
250 // ChangedPaths returns the paths changed from base to head (a zero base
251 // means the empty tree), in the order of the diff and without renames
252 // detection, at most max; truncated is true when there were more.
253 ChangedPaths(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, max int) (paths []string, truncated bool, err error)
254 // CommitsOffBranches returns the commits reachable from head and from no
255 // branch (refs/heads/*; tags do not count), newest first, at most limit
256 // (1 to MaxCommitLimit); more is true when there are others. It is empty
257 // for a commit that a branch already has.
258 CommitsOffBranches(ctx context.Context, repo domain.RepoName, head domain.ObjectID, limit int) (commits []domain.Commit, more bool, err error)
259}
260
261// --- inbound ---
262
263// RegisterRunnerParams describe a runner to register.
264type RegisterRunnerParams struct {
265 Name string
266 Labels []string
267 Version string
268 Isolation domain.CIIsolation
269 Arch, OS string
270 Replace bool
271}
272
273// RegisteredRunner is a new runner with its secret, shown once.
274type RegisteredRunner struct {
275 Runner domain.CIRunner
276 Token string
277}
278
279// RunnerJob is a job given to a runner.
280type RunnerJob struct {
281 Job domain.CIJob
282 Token string
283 // GithubToken is the job repository token, the "github.token" of the job.
284 GithubToken string
285 Repo domain.RepoName
286 Sha, Ref string
287 WorkflowPath string
288 WorkflowYAML string
289}
290
291// FetchJobParams selects the job a runner asks for.
292type FetchJobParams struct {
293 Labels []string
294 PollID string
295}
296
297// FetchJobResult is the answer of a poll.
298type FetchJobResult struct {
299 Job *RunnerJob
300 // RetryAfter is how long the runner waits before polling again.
301 RetryAfter time.Duration
302}
303
304// StepUpdate is the state of one step as a runner reports it.
305type StepUpdate struct {
306 Number int32
307 Name string
308 State domain.CIStepState
309 Conclusion domain.CIConclusion
310 // Outcome is how the step ended before continue-on-error; none when the
311 // runner does not report it.
312 Outcome domain.CIConclusion
313 StartedAt time.Time
314 CompletedAt time.Time
315}
316
317// CompleteJobParams end a job.
318type CompleteJobParams struct {
319 JobID string
320 Conclusion domain.CIConclusion
321 Outputs map[string]string
322 RunnerShutdown bool
323 Steps []StepUpdate
324 // Annotations replace those of each step they name, as in UpdateJob.
325 Annotations []domain.CIAnnotation
326}
327
328// CompleteJobResult is the stored result of a job.
329type CompleteJobResult struct {
330 Conclusion domain.CIConclusion
331 Error string
332 Applied bool
333}
334
335// ResolvedAction is the resolution of one "uses" of ResolveActions: where to
336// download the action, or Error (then only Ref is set).
337type ResolvedAction struct {
338 Ref string
339 NameWithOwner string
340 Path string
341 ResolvedSHA string
342 TarballURL string
343 Token string
344 Source domain.CIActionSource
345 Error string
346}
347
348// RemoteRef is a ref of a remote repository, as `git ls-remote` lists it.
349type RemoteRef struct {
350 // Name is the full ref name ("refs/heads/main", "refs/tags/v1").
351 Name string
352 // ID is the object the ref points to (the tag object of an annotated tag).
353 ID string
354 // Peeled is the commit an annotated tag points to; empty for other refs.
355 Peeled string
356}
357
358// GitRemote lists the refs of a repository on github.com, with no
359// credentials. It returns domain.ErrNotFound for a repository that does not
360// exist or needs credentials (GitHub answers alike), and an error matching
361// domain.ErrUnavailable when it cannot tell (network, timeout, too large an
362// answer).
363type GitRemote interface {
364 ListRefs(ctx context.Context, owner, repo string) ([]RemoteRef, error)
365}
366
367// JobSignal is one message of WatchJob.
368type JobSignal struct {
369 Keepalive bool
370 // Cancel is set for a cancel request.
371 Cancel *JobCancel
372 // Ended is set when the job has ended.
373 Ended *domain.JobEndedError
374}
375
376// JobCancel is a cancel request for a running job.
377type JobCancel struct {
378 Reason domain.CICancelReason
379 Message string
380 RequestedAt time.Time
381}
382
383// RunnerService is what runners call. The secret is the bearer secret of the
384// call: the runner token for FetchJob, the job token for the other calls.
385// A bad secret is domain.ErrUnauthenticated; a job token of another job than
386// the one named is domain.ErrForbidden. *usecase.CI implements it.
387type RunnerService interface {
388 Register(ctx context.Context, p domain.Principal, params RegisterRunnerParams) (RegisteredRunner, error)
389 FetchJob(ctx context.Context, secret string, params FetchJobParams) (FetchJobResult, error)
390 // UpdateJob applies the steps and the annotations whole or not at all: for
391 // each step number present in annotations, the annotations of that step are
392 // replaced (also for a step already completed, or not in steps).
393 UpdateJob(ctx context.Context, secret, jobID string, steps []StepUpdate, annotations []domain.CIAnnotation) error
394 AppendLog(ctx context.Context, secret, jobID string, step int32, offset int64, lines []domain.CILogLine) (next int64, err error)
395 Heartbeat(ctx context.Context, secret, jobID string) error
396 // WatchJob calls ready once the job token is valid and the stream is
397 // registered, before any message: the adapter flushes the response headers
398 // then, because a client's call returns only when they arrive.
399 WatchJob(ctx context.Context, secret, jobID string, ready func(), send func(JobSignal) error) error
400 CompleteJob(ctx context.Context, secret string, params CompleteJobParams) (CompleteJobResult, error)
401 // ResolveActions resolves the "uses" of the job's steps, one answer per
402 // reference in the same order. A reference that cannot be resolved is not
403 // an error: its answer carries Error. A local ("./") or Docker
404 // ("docker://") reference, or more than 100, is domain.ErrInvalid.
405 ResolveActions(ctx context.Context, secret, jobID string, uses []string) ([]ResolvedAction, error)
406 // UploadSummary stores the summary of a step, replacing the one it has; an
407 // empty markdown removes it. More than 1 MiB, invalid UTF-8 or a step
408 // number outside 1 to 1,000 is domain.ErrInvalid.
409 UploadSummary(ctx context.Context, secret, jobID string, step int32, markdown string) error
410}
411
412// JobView is a job as owners see it.
413type JobView struct {
414 domain.CIJob
415 // Lines is the number of lines of its log now.
416 Lines int64
417 // RunnerGone is set when the runner of the job was removed since.
418 RunnerGone bool
419}
420
421// StepRanges returns where the lines of every step are in the log.
422func (v JobView) StepRanges() []domain.StepRange { return domain.StepRanges(v.Steps, v.Lines) }
423
424// ListRunsParams select a page of runs.
425type ListRunsParams struct {
426 PageSize int
427 PageToken string
428 // Workflow restricts the result to a workflow file: its path or its file
429 // name. Branch restricts it to the runs of a branch, by short name. Status
430 // restricts it to the runs of that status. Empty means no restriction.
431 Workflow string
432 Branch string
433 Status domain.CIStatus
434}
435
436// RunPage is a page of runs, newest first.
437type RunPage struct {
438 Runs []domain.CIRun
439 NextPageToken string
440}
441
442// RunDetail is a run with its jobs.
443type RunDetail struct {
444 Run domain.CIRun
445 Jobs []JobView
446 LatestAttempt int32
447}
448
449// LogBatch is a message of a log stream: consecutive lines from Offset.
450type LogBatch struct {
451 Offset int64
452 Lines []JobLogLine
453}
454
455// JobLogLine is a log line with its step.
456type JobLogLine struct {
457 Step int32
458 domain.CILogLine
459}
460
461// GetJobLogParams select the log to stream.
462type GetJobLogParams struct {
463 JobID string
464 // Attempt is the attempt of the job, 0 for the latest.
465 Attempt int32
466 Offset int64
467 Follow bool
468 // Ready, when not nil, is called once the access is checked and the
469 // stream is counted, before any batch: the adapter flushes the response
470 // headers then (a follow on a job with no line yet would keep the client's
471 // call waiting for the first one).
472 Ready func()
473 // Client identifies where the call comes from, for the limit of streams
474 // per client; empty counts against the total only.
475 Client string
476}
477
478// CheckView is a check with what owners need to read it.
479type CheckView struct {
480 domain.CICheck
481 // WaitingForRunner is set for a queued check whose job no registered
482 // runner can take (no runner has all its labels).
483 WaitingForRunner bool
484}
485
486// ChecksResult are the checks of a commit.
487type ChecksResult struct {
488 Checks []CheckView
489 Truncated bool
490}
491
492// RunnerPage is a page of runners.
493type RunnerPage struct {
494 Runners []RunnerView
495 NextPageToken string
496}
497
498// RunnerView is a runner with what is known of it now.
499type RunnerView struct {
500 domain.CIRunner
501 LastSeen time.Time
502 Busy bool
503}
504
505// JobAnnotation is an annotation with the job it belongs to.
506type JobAnnotation struct {
507 JobID string
508 Annotation domain.CIAnnotation
509}
510
511// AnnotationList are the annotations of a run, at most
512// domain.CIMaxListedAnnotations.
513type AnnotationList struct {
514 Annotations []JobAnnotation
515 Truncated bool
516}
517
518// SummaryList are the summaries of a job, at most domain.CIMaxListedSummaries.
519type SummaryList struct {
520 Summaries []domain.CIStepSummary
521 Truncated bool
522}
523
524// RunService is the owner side of CI. *usecase.CI implements it.
525type RunService interface {
526 ListRuns(ctx context.Context, p domain.Principal, repo domain.RepoName, params ListRunsParams) (RunPage, error)
527 GetRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32) (RunDetail, error)
528 GetJob(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32) (JobView, error)
529 GetJobLog(ctx context.Context, p domain.Principal, repo domain.RepoName, params GetJobLogParams, send func(LogBatch) error) error
530 CancelRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64) (domain.CIRun, error)
531 RerunRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, failedOnly bool) (domain.CIRun, error)
532 ListChecks(ctx context.Context, p domain.Principal, repo domain.RepoName, sha string) (ChecksResult, error)
533 // ListAnnotations returns the annotations of a run attempt (0 for the
534 // latest), of one job when jobID is not empty. Each job gives those of the
535 // attempt in which it executed.
536 ListAnnotations(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32, jobID string) (AnnotationList, error)
537 // GetStepSummaries returns the summaries of a job at an attempt (0 for the
538 // latest), by step number. maxBytes above 0 bounds the Markdown read: the
539 // summaries past it come back Omitted, without their Markdown (the API
540 // passes 0, no bound).
541 GetStepSummaries(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32, maxBytes int64) (SummaryList, error)
542 ListRunners(ctx context.Context, p domain.Principal, size int, token string) (RunnerPage, error)
543 RemoveRunner(ctx context.Context, p domain.Principal, id string) error
544}