1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package ports
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "strconv"
10 "time"
11
12 "bnema.dev/ermite/internal/domain"
13)
14
15// CIRunDraft is what CreateRun stores: a run with its source, its jobs and
16// their checks, built once the store has chosen the ids.
17type CIRunDraft struct {
18 Run domain.CIRun
19 Source domain.CIRunSource
20 Jobs []domain.CIJob
21 Checks []domain.CICheck
22}
23
24// CIWrite is a set of records written in one atomic batch. A run or a job with
25// Version 0 must not exist yet; any other must be unchanged since it was read.
26// Checks are derived from the jobs and upserted without a precondition.
27// Apply returns domain.ErrConflict otherwise and domain.ErrNotFound when the
28// repository no longer exists.
29//
30// RepoTokens are job repository tokens created with the batch (they must not
31// exist yet), RevokeJobs the jobs whose repository tokens are deleted by it,
32// and Resolutions the action resolutions of a run attempt to keep. They are
33// written under the lock of the run that owns them.
34//
35// Annotations replace, for each (execution of a job, step) they name, all the
36// annotations stored for it (none deletes them); ClearAnnotations deletes all
37// the annotations of the executions of jobs they name, steps included. Both
38// are written with the batch, so a call is applied whole or not at all.
39type CIWrite struct {
40 Runs []domain.CIRun
41 Jobs []domain.CIJob
42 Checks []domain.CICheck
43 RepoTokens []domain.CIRepoToken
44 RevokeJobs []CIJobRef
45 Resolutions []domain.CIActionResolution
46 Annotations []CIStepAnnotations
47 ClearAnnotations []CILogRef
48}
49
50// CIConcurrencyGroup is a stored concurrency group: its repository, its key (the
51// group, canonical: ConcurrencyKey), its record and its marks.
52type CIConcurrencyGroup struct {
53 Repo domain.RepoName
54 Group string
55 State domain.CIConcurrencyGroupState
56}
57
58// CIStepAnnotations are all the annotations of one step of one execution of a
59// job, in the order the step produced them.
60type CIStepAnnotations struct {
61 Repo domain.RepoName
62 Exec CILogRef
63 Step int32
64 // Items are the annotations; none removes those of the step.
65 Items []domain.CIAnnotation
66}
67
68// CIJobRef names one execution of a job.
69type CIJobRef struct {
70 RunID int64
71 RunAttempt int32
72 JobID string
73}
74
75// CIRepoTokenStore reads the job repository tokens, for the authentication of
76// a request. They are written by CIStore.Apply. *kvstore.CI implements it.
77type CIRepoTokenStore interface {
78 // FindRepoTokenByHash returns the token whose secret has that hash, or
79 // domain.ErrNotFound.
80 FindRepoTokenByHash(ctx context.Context, hash domain.TokenHash) (domain.CIRepoToken, error)
81 // GetRepoToken returns the token with that id, or domain.ErrNotFound.
82 GetRepoToken(ctx context.Context, id string) (domain.CIRepoToken, error)
83}
84
85// CIRefTipStore keeps, per branch or tag (full ref name), who moves it to its
86// object (domain.CIRefTip). The record is written before the ref moves.
87// *kvstore.CI implements it; the records go with the repository.
88type CIRefTipStore interface {
89 // PutRefTip stores the record of a ref, replacing the earlier one. A
90 // repository that does not exist is domain.ErrNotFound.
91 PutRefTip(ctx context.Context, t domain.CIRefTip) error
92 // GetRefTip returns the record of a ref, or domain.ErrNotFound.
93 GetRefTip(ctx context.Context, repo domain.RepoName, ref string) (domain.CIRefTip, error)
94 // DeleteRefTip removes the record of a ref; a missing one is not an error.
95 DeleteRefTip(ctx context.Context, repo domain.RepoName, ref string) error
96 // DeleteRefTipIf removes the record of a ref only when it still names
97 // sha; a missing or different one is left alone and is not an error.
98 DeleteRefTipIf(ctx context.Context, repo domain.RepoName, ref, sha string) error
99}
100
101// CIWorkflowStore keeps, per repository, the workflow files that are disabled
102// (`ermite workflow disable`). Paths are normalized (domain.ParseWorkflowPath);
103// the file does not have to exist. The records go with the repository.
104// *kvstore.CI implements it.
105type CIWorkflowStore interface {
106 // DisableWorkflow records the path as disabled; one already disabled is
107 // left as it is. A repository that does not exist is domain.ErrNotFound.
108 DisableWorkflow(ctx context.Context, repo domain.RepoName, path string, at time.Time) error
109 // EnableWorkflow removes the record; a path that is not disabled is not an
110 // error.
111 EnableWorkflow(ctx context.Context, repo domain.RepoName, path string) error
112 // DisabledWorkflows returns the disabled paths of the repository, sorted.
113 DisabledWorkflows(ctx context.Context, repo domain.RepoName) ([]string, error)
114}
115
116// CIStore keeps CI runs, jobs and checks. The store derives its own indexes
117// (the queue of ready jobs, the active jobs, the open runs, the job tokens)
118// from the records it writes.
119type CIStore interface {
120 // CreateRun takes the next run id and the next number of the workflow in
121 // the repository (both never reused), calls build with them and stores
122 // the draft it returns, in one batch that fails with domain.ErrNotFound
123 // once the repository is deleted, and with domain.ErrWorkflowDisabled when
124 // the repository disabled workflowPath (CIWorkflowStore) when the batch
125 // applies: the state is checked in that transaction, not before it, so a
126 // disable can never be overtaken, whatever event started the run and
127 // whether the run is a failed one for an invalid file or not. The records
128 // come back with their versions.
129 CreateRun(ctx context.Context, repo domain.RepoName, workflowPath string, build func(id, number int64) CIRunDraft) (CIRunDraft, error)
130 // Apply writes the records in one batch and returns the version they all
131 // have afterwards.
132 Apply(ctx context.Context, w CIWrite) (version uint64, err error)
133 // GetRun returns the run, or domain.ErrNotFound.
134 GetRun(ctx context.Context, repo domain.RepoName, id int64) (domain.CIRun, error)
135 // GetActionResolution returns the resolution kept for the key in the run
136 // attempt, or domain.ErrNotFound.
137 GetActionResolution(ctx context.Context, runID int64, runAttempt int32, key string) (domain.CIActionResolution, error)
138 // GetRunSource returns what the run was planned from, or
139 // domain.ErrNotFound. It is how a run is found from its id alone.
140 GetRunSource(ctx context.Context, runID int64) (domain.CIRunSource, error)
141 // ListRuns returns the runs of the repository with an id below before (0
142 // for no bound), newest first, at most limit.
143 ListRuns(ctx context.Context, repo domain.RepoName, before int64, limit int) ([]domain.CIRun, error)
144 // RunIDs returns the ids of the runs of the repository.
145 RunIDs(ctx context.Context, repo domain.RepoName) ([]int64, error)
146 // ListOpenRuns returns the runs that have not completed.
147 ListOpenRuns(ctx context.Context) ([]domain.CIRun, error)
148 // JobAnnotations returns the annotations of one execution of a job
149 // (CIJob.Attempt), ordered by step then by the order the step produced
150 // them. A job without annotation gives an empty list.
151 JobAnnotations(ctx context.Context, runID int64, attempt int32, jobID string) ([]domain.CIAnnotation, error)
152 // GetJob returns a job of a run at an attempt of the run, or
153 // domain.ErrNotFound.
154 GetJob(ctx context.Context, runID int64, runAttempt int32, jobID string) (domain.CIJob, error)
155 // ListJobs returns the jobs of a run at an attempt, ordered by index then
156 // combination.
157 ListJobs(ctx context.Context, runID int64, runAttempt int32) ([]domain.CIJob, error)
158 // FindJobByToken returns the job whose current token has that hash and
159 // whose token is still valid at now, or domain.ErrNotFound.
160 FindJobByToken(ctx context.Context, hash domain.TokenHash, now time.Time) (domain.CIJob, error)
161 // ListReadyJobs returns the jobs that wait for a runner, oldest first, at
162 // most limit.
163 ListReadyJobs(ctx context.Context, limit int) ([]domain.CIJob, error)
164 // ListActiveJobs returns the jobs given to a runner and not over.
165 ListActiveJobs(ctx context.Context) ([]domain.CIJob, error)
166 // ListChecks returns the checks of a commit, at most limit; more is true
167 // when there are more.
168 ListChecks(ctx context.Context, repo domain.RepoName, sha string, limit int) (checks []domain.CICheck, more bool, err error)
169 // UpdateConcurrency reads a concurrency group of a repository (compared
170 // with domain.ConcurrencyKey) with the marks of its entries, calls fn on a
171 // copy and stores what fn leaves in it, in one batch: the record, the marks
172 // that changed and the marks that fn removed. A group left with neither
173 // record nor marks is removed. The batch carries the version precondition of
174 // the group whatever fn does, and every change of the marks rewrites the
175 // record, so no two transactions of a group both succeed on the same state:
176 // on a conflict fn is called again on the new state and must be repeatable.
177 // A deleted repository is domain.ErrNotFound.
178 UpdateConcurrency(ctx context.Context, repo domain.RepoName, group string, fn func(*domain.CIConcurrencyGroupState) error) error
179 // ListConcurrency returns every concurrency group that has an entry, for
180 // the reconciliation at startup.
181 ListConcurrency(ctx context.Context) ([]CIConcurrencyGroup, error)
182 // Sweep removes the runs completed before the time, with their jobs,
183 // checks and sources (not their numbers), and returns their ids so that
184 // the logs can go too.
185 Sweep(ctx context.Context, before time.Time) ([]int64, error)
186 // CreateRunner stores the runner and the index of its token. A name in use
187 // is domain.ErrAlreadyExists, or with replace the old runner is removed in
188 // the same batch and returned.
189 CreateRunner(ctx context.Context, r domain.CIRunner, replace bool) (removed []domain.CIRunner, err error)
190 // GetRunner returns the runner, or domain.ErrNotFound.
191 GetRunner(ctx context.Context, id string) (domain.CIRunner, error)
192 // FindRunnerByToken returns the runner whose token has that hash, or
193 // domain.ErrNotFound.
194 FindRunnerByToken(ctx context.Context, hash domain.TokenHash) (domain.CIRunner, error)
195 // ListRunners returns every runner ordered by name.
196 ListRunners(ctx context.Context) ([]domain.CIRunner, error)
197 // SetRunnerVersion records the version the runner runs now. It writes only
198 // when the stored version differs, and returns domain.ErrNotFound for a
199 // runner that is gone.
200 SetRunnerVersion(ctx context.Context, id, version string) error
201 // RemoveRunner removes the runner and its token, or returns
202 // domain.ErrNotFound.
203 RemoveRunner(ctx context.Context, id string) (domain.CIRunner, error)
204}
205
206// CIRunCleaner is what repository deletion needs from CI: the runs of the
207// repository before its records go, and the removal of their logs and
208// summaries after. *usecase.CI implements it.
209type CIRunCleaner interface {
210 RunIDs(ctx context.Context, repo domain.RepoName) ([]int64, error)
211 RemoveLogs(ctx context.Context, runIDs []int64) error
212 // ForgetRepo drops what CI keeps in memory for the deleted repository (its
213 // cached schedules).
214 ForgetRepo(repo domain.RepoName)
215}
216
217// CIChecksReader is the read side of the checks, for the merge rule and the
218// pages that show them. *usecase.CI implements it.
219type CIChecksReader interface {
220 // CommitChecks returns the checks of a commit, without access control (the
221 // caller has checked it). More than 1,000 is domain.ErrResourceExhausted,
222 // not a part of them: a rule must not decide on some of the checks.
223 CommitChecks(ctx context.Context, repo domain.RepoName, sha string) ([]domain.CICheck, error)
224}
225
226// CILogRef names one execution of a job: its log, its summaries and its
227// annotations are kept under it.
228type CILogRef struct {
229 RunID int64
230 JobID string
231 Attempt int32
232}
233
234// ErrCILogLimit is returned by CILogStore.Append when the lines do not fit in
235// the log limit; nothing was appended.
236var ErrCILogLimit = errors.New("log limit reached")
237
238// ErrCILogGap wraps domain.ErrOutOfRange when an offset is above the count.
239type ErrCILogGap struct{ Count int64 }
240
241// Error implements error.
242func (e *ErrCILogGap) Error() string {
243 return "out of range: the log holds lines up to the offset of " + strconv.FormatInt(e.Count, 10)
244}
245
246// Is makes errors.Is(err, domain.ErrOutOfRange) true.
247func (e *ErrCILogGap) Is(target error) bool { return target == domain.ErrOutOfRange }
248
249// CILogStore keeps the logs of the jobs: append-only files, one line per log
250// line, never in the metadata store. Lines are numbered from 0.
251type CILogStore interface {
252 // Append appends the lines at offset (the number of the first line), with
253 // the idempotence of AppendLog: lines already stored are skipped, a gap is
254 // an *ErrCILogGap. It returns the number of lines after the call and how
255 // many of the request were new. A log that would exceed limit bytes gets
256 // nothing and ErrCILogLimit.
257 Append(ctx context.Context, ref CILogRef, offset int64, lines []domain.CILogLine, limit int64) (next int64, added int, err error)
258 // AppendForced appends lines whatever the limit (the last line of a log
259 // that reached it).
260 AppendForced(ctx context.Context, ref CILogRef, lines []domain.CILogLine) (next int64, err error)
261 // Count returns the number of lines held, 0 for a log that does not exist.
262 Count(ctx context.Context, ref CILogRef) (int64, error)
263 // Read returns lines from the one numbered from, at most maxLines lines and
264 // about maxBytes of text. It returns domain.ErrNotFound when the log does
265 // not exist and domain.ErrOutOfRange when from is above the count.
266 Read(ctx context.Context, ref CILogRef, from int64, maxLines, maxBytes int) ([]domain.CILogLine, error)
267 // Changed returns a channel that is closed at the next append to the log.
268 Changed(ref CILogRef) <-chan struct{}
269 // Reset empties the log.
270 Reset(ctx context.Context, ref CILogRef) error
271 // RemoveRuns deletes the logs of the runs.
272 RemoveRuns(ctx context.Context, runIDs []int64) error
273 // RunIDs lists the runs that have a log directory, for the orphan sweep.
274 RunIDs(ctx context.Context) ([]int64, error)
275}
276
277// CISummaryStore keeps the job summaries (the Markdown a step wrote to
278// GITHUB_STEP_SUMMARY): one file per step of an execution of a job, never in
279// the metadata store.
280type CISummaryStore interface {
281 // Put stores the summary of a step, replacing the one it has; the file
282 // appears whole or not at all. An empty markdown removes the summary.
283 Put(ctx context.Context, ref CILogRef, step int32, markdown string) error
284 // List returns the summaries of the steps that have one, by step number,
285 // at most limit; more is true when there are others. With maxBytes above
286 // 0 the files are sized (stat) before they are read, and a summary that
287 // would take the total of the Markdown read past maxBytes is returned
288 // Omitted, unread, as are all the ones after it: a call never reads more
289 // than maxBytes.
290 List(ctx context.Context, ref CILogRef, limit int, maxBytes int64) (summaries []domain.CIStepSummary, more bool, err error)
291 // RemoveJob deletes the summaries of one execution of a job.
292 RemoveJob(ctx context.Context, ref CILogRef) error
293 // RemoveRuns deletes the summaries of the runs.
294 RemoveRuns(ctx context.Context, runIDs []int64) error
295 // RunIDs lists the runs that have a summary directory, for the orphan
296 // sweep.
297 RunIDs(ctx context.Context) ([]int64, error)
298}
299
300// DispatchParams are the arguments of RunService.DispatchWorkflow.
301type DispatchParams struct {
302 // WorkflowPath is the path of the workflow file, or its file name.
303 WorkflowPath string
304 // Ref is a branch or a tag, short or full name; empty is the default branch.
305 Ref string
306 // Inputs are the values of the inputs by name, as text.
307 Inputs map[string]string
308}
309
310// WorkflowInfo is a workflow file of a repository and whether it starts runs.
311type WorkflowInfo struct {
312 // Path is ".github/workflows/<file>".
313 Path string
314 // Name is the "name:" of the workflow at the tip of the default branch (the
315 // path when it has none); empty when that file is absent or not a workflow.
316 Name string
317 Disabled bool
318}
319
320// WorkflowTrigger starts the workflows that match pushed refs. OnPush only
321// queues the work and returns: planning happens in the background and its
322// errors never reach the push.
323type WorkflowTrigger interface {
324 OnPush(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, pusher domain.Principal)
325 // OnMerge is OnPush for the move of a target branch by a merge. GitHub shows
326 // the owner as the actor of the push of a merge, whoever merged; but the
327 // rights the run's actions are resolved with are those of merger, the
328 // principal that merged, never the owner's.
329 OnMerge(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, merger domain.Principal)
330}
331
332// PullRequestTrigger starts the pull_request workflows of the merge requests
333// (CI-3, D5), like WorkflowTrigger does for pushes. It only queues the work.
334// The synchronize activity needs no call here: the push of the source branch
335// reaches WorkflowTrigger.OnPush.
336type PullRequestTrigger interface {
337 // OnPullRequest queues the activity ev of a merge request. p is who caused
338 // it: its trust is the run's, whatever happens to the branches before the
339 // event is handled. A job token starts no run.
340 OnPullRequest(ctx context.Context, repo domain.RepoName, ev PullRequestEvent, p domain.Principal)
341}
342
343// PullRequestEvent is one activity of a merge request, with the commits it is
344// for fixed when it happened: it runs for exactly these, with the trust of
345// whoever caused it, even when the branches have moved on by the time it is
346// handled.
347type PullRequestEvent struct {
348 Number int64
349 // Action is domain.PullOpened or domain.PullClosed.
350 Action string
351 // Head is the head of the source branch and Base the tip of the target at
352 // that moment; either may be zero when unknown (the current one is read).
353 Head, Base domain.ObjectID
354 // Merged is set on the closing a merge makes: its runs have the merger's
355 // trust (usecase.CI checks the merge against the request).
356 Merged bool
357}
358
359// PullGate lets the CI change what depends on a merge request staying open
360// (its refs/pull refs, the runs of its opening and updates) without racing a
361// close or a merge. *usecase.MergeRequests implements it.
362type PullGate interface {
363 // WhileOpen runs fn holding the merge lock of the repository (the one
364 // Close and Merge take) when merge request number is open, and returns
365 // domain.ErrPreconditionFailed without running fn when it is not. fn must
366 // be short and must not call the merge request use cases.
367 WhileOpen(ctx context.Context, repo domain.RepoName, number int64, fn func() error) error
368}
369
370// PullGit is what the refs/pull/<n>/{head,merge} of the merge requests need
371// from Git. Every method returns domain.ErrNotFound when the repository has no
372// directory.
373type PullGit interface {
374 // TestMerge merges head into base without touching a ref (`git merge-tree
375 // --write-tree`) and returns the tree of the result. clean is false when
376 // the merge conflicts, or when the commits share no history.
377 TestMerge(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID) (tree domain.ObjectID, clean bool, err error)
378 // SetPullRef points a ref of domain.IsPullRef to the commit. Any other ref
379 // name is domain.ErrInvalid.
380 SetPullRef(ctx context.Context, repo domain.RepoName, ref string, id domain.ObjectID) error
381 // PullRef returns the commit a ref of domain.IsPullRef points to, or
382 // domain.ErrNotFound.
383 PullRef(ctx context.Context, repo domain.RepoName, ref string) (domain.ObjectID, error)
384}
385
386// CIGit is what planning needs from Git beyond RepoReader and MergeGit.
387type CIGit interface {
388 // ChangedPaths returns the paths changed from base to head (a zero base
389 // means the empty tree), in the order of the diff and without renames
390 // detection, at most max; truncated is true when there were more.
391 ChangedPaths(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, max int) (paths []string, truncated bool, err error)
392 // CommitsOffBranches returns the commits reachable from head and from no
393 // branch (refs/heads/*; tags do not count), newest first, at most limit
394 // (1 to MaxCommitLimit); more is true when there are others. It is empty
395 // for a commit that a branch already has.
396 CommitsOffBranches(ctx context.Context, repo domain.RepoName, head domain.ObjectID, limit int) (commits []domain.Commit, more bool, err error)
397}
398
399// PushObjects tells where the objects of a push in progress are. Git receives a
400// push into a quarantine directory and moves the objects into the repository
401// only when pre-receive accepts it, so the objects a pre-receive check reads are
402// not in the repository yet. Both fields are what git's own environment holds
403// in the hook (GIT_OBJECT_DIRECTORY and GIT_ALTERNATE_OBJECT_DIRECTORIES). The
404// zero value is a push with nothing in quarantine.
405type PushObjects struct {
406 Dir string
407 Alternates string
408}
409
410// PushTreeReader reads, for a push still in quarantine, the objects the
411// workflow policy (domain.WorkflowChangeOf) judges. It knows nothing of the
412// policy: it peels, classifies, walks and compares trees.
413type PushTreeReader interface {
414 // PushSubtrees reads what a push brings, and the entry at path (a
415 // directory path without leading or trailing slash, e.g.
416 // domain.WorkflowDir) in each object concerned.
417 //
418 // Updates that delete (New == ZeroOID) have no objects: they are skipped.
419 // For every other update, Old (unless ZeroOID) and New are peeled
420 // (`<oid>^{}`): the result is in Ends keyed by the oid as given. An end
421 // that is missing, malformed, or peels to neither commit, tree nor blob is
422 // an error matching domain.ErrInvalid.
423 //
424 // Commits is `rev-list --parents <tips> --not --all` where tips are the
425 // distinct New ends that peel to a commit: the commits no ref has yet,
426 // each with its parents, in rev-list order. More than bounds.Commits
427 // commits, more than bounds.IDs ids (commits and parents, repeats
428 // included) or a commit whose parent line exceeds the adapter's line
429 // buffer is an error matching domain.ErrInvalid, raised as soon as the
430 // line crossing the bound is read.
431 //
432 // Subtrees holds the entry at path for every end, every commit, every
433 // parent, and, when a create has a non-blob New, the default branch head
434 // (key domain.HeadKey, absent when HEAD is unborn or has no such entry). An object that peels to a blob, or an
435 // object with no entry at path (including a path that crosses a
436 // gitlink, symlink or file), has no key. Kind is EntryDirectory only for
437 // mode 040000 and type tree.
438 //
439 // objects are the quarantine directories of the push; one outside the
440 // repository's objects directory (symlinks resolved) is an error matching
441 // domain.ErrInvalid, checked before anything is read.
442 PushSubtrees(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, objects PushObjects, path string, bounds domain.PushWalkBounds) (domain.PushSubtrees, error)
443
444 // FirstDifferentEntry returns the name of the first top-level entry that
445 // differs between the trees old and new (`diff-tree --name-only -z
446 // --no-renames`, read as a stream up to the first name, then git is
447 // stopped). A zero old is the empty tree. It returns "" when nothing
448 // differs. A name longer than the adapter's path bound is an error
449 // matching domain.ErrInvalid. objects as above.
450 FirstDifferentEntry(ctx context.Context, repo domain.RepoName, objects PushObjects, old, new domain.ObjectID) (name string, err error)
451}
452
453// --- inbound ---
454
455// RegisterRunnerParams describe a runner to register.
456type RegisterRunnerParams struct {
457 Name string
458 Labels []string
459 Version string
460 Isolation domain.CIIsolation
461 Arch, OS string
462 Replace bool
463}
464
465// RegisteredRunner is a new runner with its secret, shown once.
466type RegisteredRunner struct {
467 Runner domain.CIRunner
468 Token string
469}
470
471// RunnerJob is a job given to a runner.
472type RunnerJob struct {
473 Job domain.CIJob
474 Token string
475 // GithubToken is the job repository token, the "github.token" of the job.
476 GithubToken string
477 Repo domain.RepoName
478 Sha, Ref string
479 WorkflowPath string
480 WorkflowYAML string
481 // Environment is the environment the job deploys to, "" for none.
482 Environment string
483 // Vars are the merged variables of the job (environment over repository
484 // over instance), already in its "vars" context.
485 Vars map[string]string
486 // Secrets are the secrets the job refers to, in plain text: empty when the
487 // run is not trusted, when the job refers to none and when the server has
488 // no key. They go to the runner and nowhere else: never into the run
489 // record, the github context or a log.
490 Secrets map[string]string
491 // Trusted is the trusted flag of the run: secrets were allowed.
492 Trusted bool
493 // Permissions are the permissions of GithubToken (key to level), after the
494 // trust rule: no write when the run is not trusted.
495 Permissions map[string]string
496 // Cache is the scope of the job in the runner's cache (Job.cache).
497 Cache JobCache
498 // IDTokenURL is the token endpoint of the job's OIDC ID tokens and
499 // IDTokenRequestToken its bearer (Job.id_token_url and
500 // id_token_request_token); both "" when the job may not have ID tokens.
501 IDTokenURL string
502 IDTokenRequestToken string
503}
504
505// JobCache is what a job may do in the cache the runner serves: save entries
506// under WriteRef only ("" for none) and restore those of ReadRefs, most
507// specific first. Repository is "owner/name".
508type JobCache struct {
509 Repository string
510 WriteRef string
511 ReadRefs []string
512}
513
514// String describes the job without its secrets and tokens, so that a job
515// printed by mistake leaks nothing.
516func (j RunnerJob) String() string {
517 return fmt.Sprintf("RunnerJob{job:%s run:%d repo:%s secrets:%d trusted:%t}", j.Job.ID, j.Job.RunID, j.Repo, len(j.Secrets), j.Trusted)
518}
519
520// GoString is String for %#v.
521func (j RunnerJob) GoString() string { return j.String() }
522
523// FetchJobParams selects the job a runner asks for.
524type FetchJobParams struct {
525 Labels []string
526 PollID string
527 // Version is the version of the running runner, "" from an older runner.
528 Version string
529}
530
531// FetchJobResult is the answer of a poll.
532type FetchJobResult struct {
533 Job *RunnerJob
534 // RetryAfter is how long the runner waits before polling again.
535 RetryAfter time.Duration
536}
537
538// StepUpdate is the state of one step as a runner reports it.
539type StepUpdate struct {
540 Number int32
541 Name string
542 State domain.CIStepState
543 Conclusion domain.CIConclusion
544 // Outcome is how the step ended before continue-on-error; none when the
545 // runner does not report it.
546 Outcome domain.CIConclusion
547 StartedAt time.Time
548 CompletedAt time.Time
549}
550
551// CompleteJobParams end a job.
552type CompleteJobParams struct {
553 JobID string
554 Conclusion domain.CIConclusion
555 Outputs map[string]string
556 RunnerShutdown bool
557 Steps []StepUpdate
558 // Annotations replace those of each step they name, as in UpdateJob.
559 Annotations []domain.CIAnnotation
560}
561
562// CompleteJobResult is the stored result of a job.
563type CompleteJobResult struct {
564 Conclusion domain.CIConclusion
565 Error string
566 Applied bool
567}
568
569// ResolvedAction is the resolution of one "uses" of ResolveActions: where to
570// download the action, or Error (then only Ref is set).
571type ResolvedAction struct {
572 Ref string
573 NameWithOwner string
574 Path string
575 ResolvedSHA string
576 TarballURL string
577 Token string
578 Source domain.CIActionSource
579 Error string
580}
581
582// RemoteRef is a ref of a remote repository, as `git ls-remote` lists it.
583type RemoteRef struct {
584 // Name is the full ref name ("refs/heads/main", "refs/tags/v1").
585 Name string
586 // ID is the object the ref points to (the tag object of an annotated tag).
587 ID string
588 // Peeled is the commit an annotated tag points to; empty for other refs.
589 Peeled string
590}
591
592// GitRemote lists the refs of a repository on github.com, with no
593// credentials. It returns domain.ErrNotFound for a repository that does not
594// exist or needs credentials (GitHub answers alike), and an error matching
595// domain.ErrUnavailable when it cannot tell (network, timeout, too large an
596// answer).
597type GitRemote interface {
598 ListRefs(ctx context.Context, owner, repo string) ([]RemoteRef, error)
599}
600
601// WorkflowFetcher downloads one reusable workflow file from github.com at a
602// commit that was already resolved (CI-3, D7), with no credentials. It returns
603// domain.ErrNotFound for a file or repository that does not exist or needs
604// credentials, domain.ErrResourceExhausted for a file larger than maxBytes, and
605// an error matching domain.ErrUnavailable when it cannot tell.
606type WorkflowFetcher interface {
607 FetchWorkflow(ctx context.Context, owner, repo, sha, path string, maxBytes int64) ([]byte, error)
608}
609
610// JobSignal is one message of WatchJob.
611type JobSignal struct {
612 Keepalive bool
613 // Cancel is set for a cancel request.
614 Cancel *JobCancel
615 // Ended is set when the job has ended.
616 Ended *domain.JobEndedError
617}
618
619// JobCancel is a cancel request for a running job.
620type JobCancel struct {
621 Reason domain.CICancelReason
622 Message string
623 RequestedAt time.Time
624}
625
626// RunnerService is what runners call. The secret is the bearer secret of the
627// call: the runner token for FetchJob, the job token for the other calls.
628// A bad secret is domain.ErrUnauthenticated; a job token of another job than
629// the one named is domain.ErrForbidden. *usecase.CI implements it.
630type RunnerService interface {
631 Register(ctx context.Context, p domain.Principal, params RegisterRunnerParams) (RegisteredRunner, error)
632 FetchJob(ctx context.Context, secret string, params FetchJobParams) (FetchJobResult, error)
633 // UpdateJob applies the steps and the annotations whole or not at all: for
634 // each step number present in annotations, the annotations of that step are
635 // replaced (also for a step already completed, or not in steps).
636 UpdateJob(ctx context.Context, secret, jobID string, steps []StepUpdate, annotations []domain.CIAnnotation) error
637 AppendLog(ctx context.Context, secret, jobID string, step int32, offset int64, lines []domain.CILogLine) (next int64, err error)
638 Heartbeat(ctx context.Context, secret, jobID string) error
639 // WatchJob calls ready once the job token is valid and the stream is
640 // registered, before any message: the adapter flushes the response headers
641 // then, because a client's call returns only when they arrive.
642 WatchJob(ctx context.Context, secret, jobID string, ready func(), send func(JobSignal) error) error
643 CompleteJob(ctx context.Context, secret string, params CompleteJobParams) (CompleteJobResult, error)
644 // ResolveActions resolves the "uses" of the job's steps, one answer per
645 // reference in the same order. A reference that cannot be resolved is not
646 // an error: its answer carries Error. A local ("./") or Docker
647 // ("docker://") reference, or more than 100, is domain.ErrInvalid.
648 ResolveActions(ctx context.Context, secret, jobID string, uses []string) ([]ResolvedAction, error)
649 // UploadSummary stores the summary of a step, replacing the one it has; an
650 // empty markdown removes it. More than 1 MiB, invalid UTF-8 or a step
651 // number outside 1 to 1,000 is domain.ErrInvalid.
652 UploadSummary(ctx context.Context, secret, jobID string, step int32, markdown string) error
653}
654
655// JobView is a job as owners see it.
656type JobView struct {
657 domain.CIJob
658 // Lines is the number of lines of its log now.
659 Lines int64
660 // RunnerGone is set when the runner of the job was removed since.
661 RunnerGone bool
662}
663
664// StepRanges returns where the lines of every step are in the log.
665func (v JobView) StepRanges() []domain.StepRange { return domain.StepRanges(v.Steps, v.Lines) }
666
667// ListRunsParams select a page of runs.
668type ListRunsParams struct {
669 PageSize int
670 PageToken string
671 // Workflow restricts the result to a workflow file: its path or its file
672 // name. Branch restricts it to the runs of a branch, by short name. Status
673 // restricts it to the runs of that status. Empty means no restriction.
674 Workflow string
675 Branch string
676 Status domain.CIStatus
677}
678
679// RunPage is a page of runs, newest first.
680type RunPage struct {
681 Runs []domain.CIRun
682 NextPageToken string
683}
684
685// RunDetail is a run with its jobs.
686type RunDetail struct {
687 Run domain.CIRun
688 Jobs []JobView
689 LatestAttempt int32
690}
691
692// LogBatch is a message of a log stream: consecutive lines from Offset.
693type LogBatch struct {
694 Offset int64
695 Lines []JobLogLine
696}
697
698// JobLogLine is a log line with its step.
699type JobLogLine struct {
700 Step int32
701 domain.CILogLine
702}
703
704// GetJobLogParams select the log to stream.
705type GetJobLogParams struct {
706 JobID string
707 // Attempt is the attempt of the job, 0 for the latest.
708 Attempt int32
709 Offset int64
710 Follow bool
711 // Ready, when not nil, is called once the access is checked and the
712 // stream is counted, before any batch: the adapter flushes the response
713 // headers then (a follow on a job with no line yet would keep the client's
714 // call waiting for the first one).
715 Ready func()
716 // Client identifies where the call comes from, for the limit of streams
717 // per client; empty counts against the total only.
718 Client string
719}
720
721// CheckView is a check with what owners need to read it.
722type CheckView struct {
723 domain.CICheck
724 // Event is the event of the run of the check (push, pull_request, ...), empty if unknown.
725 Event string
726 // WaitingForRunner is set for a queued check whose job no registered
727 // runner can take (no runner has all its labels).
728 WaitingForRunner bool
729}
730
731// ChecksResult are the checks of a commit.
732type ChecksResult struct {
733 Checks []CheckView
734 Truncated bool
735}
736
737// RunnerPage is a page of runners.
738type RunnerPage struct {
739 Runners []RunnerView
740 NextPageToken string
741}
742
743// RunnerView is a runner with what is known of it now.
744type RunnerView struct {
745 domain.CIRunner
746 LastSeen time.Time
747 Busy bool
748 // Online is decided by the server's clock (see usecase.RunnerOnlineWindow).
749 Online bool
750}
751
752// JobAnnotation is an annotation with the job it belongs to.
753type JobAnnotation struct {
754 JobID string
755 Annotation domain.CIAnnotation
756}
757
758// AnnotationList are the annotations of a run, at most
759// domain.CIMaxListedAnnotations.
760type AnnotationList struct {
761 Annotations []JobAnnotation
762 Truncated bool
763}
764
765// SummaryList are the summaries of a job, at most domain.CIMaxListedSummaries.
766type SummaryList struct {
767 Summaries []domain.CIStepSummary
768 Truncated bool
769}
770
771// RunService is the owner side of CI. *usecase.CI implements it.
772type RunService interface {
773 ListRuns(ctx context.Context, p domain.Principal, repo domain.RepoName, params ListRunsParams) (RunPage, error)
774 GetRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32) (RunDetail, error)
775 GetJob(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32) (JobView, error)
776 GetJobLog(ctx context.Context, p domain.Principal, repo domain.RepoName, params GetJobLogParams, send func(LogBatch) error) error
777 CancelRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64) (domain.CIRun, error)
778 RerunRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, failedOnly bool) (domain.CIRun, error)
779 // DispatchWorkflow starts a run of a workflow file by hand (workflow_dispatch).
780 // The caller needs ci:write on the repository, or is a job token with the
781 // permission actions: write. The run is returned queued.
782 DispatchWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, params DispatchParams) (domain.CIRun, error)
783 ListChecks(ctx context.Context, p domain.Principal, repo domain.RepoName, sha string) (ChecksResult, error)
784 // ListWorkflows returns the workflow files at the tip of the default branch
785 // and the disabled ones whatever the branch holds, ordered by path. Reading
786 // the repository is enough.
787 ListWorkflows(ctx context.Context, p domain.Principal, repo domain.RepoName) ([]WorkflowInfo, error)
788 // DisabledWorkflows returns the paths of the disabled workflows, sorted,
789 // without reading any file: the cheap form of ListWorkflows for the pages.
790 DisabledWorkflows(ctx context.Context, p domain.Principal, repo domain.RepoName) ([]string, error)
791 // DisableWorkflow stops the workflow (its file name or path; it does not
792 // have to exist) from starting runs for any event. EnableWorkflow undoes it.
793 // Both need repo:admin on the repository and are idempotent.
794 DisableWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, file string) (WorkflowInfo, error)
795 EnableWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, file string) (WorkflowInfo, error)
796 // ListAnnotations returns the annotations of a run attempt (0 for the
797 // latest), of one job when jobID is not empty. Each job gives those of the
798 // attempt in which it executed.
799 ListAnnotations(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32, jobID string) (AnnotationList, error)
800 // GetStepSummaries returns the summaries of a job at an attempt (0 for the
801 // latest), by step number. maxBytes above 0 bounds the Markdown read: the
802 // summaries past it come back Omitted, without their Markdown (the API
803 // passes 0, no bound).
804 GetStepSummaries(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32, maxBytes int64) (SummaryList, error)
805 ListRunners(ctx context.Context, p domain.Principal, size int, token string) (RunnerPage, error)
806 RemoveRunner(ctx context.Context, p domain.Principal, id string) error
807}
808
809// ErrSecretNotOpened means a sealed secret does not open: another key, other
810// additional data (a value moved to another scope or name) or a damaged value.
811var ErrSecretNotOpened = errors.New("secret does not open")
812
813// SecretSealer encrypts the values of secrets at rest. The adapter holds the
814// master key; without one the use cases get no sealer and refuse secrets.
815type SecretSealer interface {
816 // Seal returns plaintext sealed and bound to aad. It never fails.
817 Seal(plaintext, aad []byte) []byte
818 // Open reverses Seal with the same aad, or returns ErrSecretNotOpened.
819 Open(sealed, aad []byte) ([]byte, error)
820 // KeyCheck is a value derived from the key that does not reveal it.
821 KeyCheck() []byte
822}
823
824// SealedSecret is a stored secret: its value is sealed.
825type SealedSecret struct {
826 Name string
827 Sealed []byte
828 UpdatedAt time.Time
829}
830
831// CISettingsStore keeps the secrets, variables and environments of CI (ADR
832// 0019). A scope that names a missing repository or environment is
833// domain.ErrNotFound for every write and every read of its content. Names are
834// already upper-cased (domain.ParseSettingName).
835type CISettingsStore interface {
836 // BindSecretsKey records the key check beside the secrets when none is
837 // stored, and returns domain.ErrSecretsKeyMismatch when the stored one
838 // differs.
839 BindSecretsKey(ctx context.Context, check []byte) error
840 // PutSecret creates or replaces a secret.
841 PutSecret(ctx context.Context, scope domain.SettingsScope, name string, sealed []byte, at time.Time) (domain.Secret, error)
842 // GetSealedSecret reads one secret of a scope, or domain.ErrNotFound when
843 // it, the repository or the environment does not exist.
844 GetSealedSecret(ctx context.Context, scope domain.SettingsScope, name string) (SealedSecret, error)
845 // ListSealedSecrets lists the secrets of one scope ordered by name.
846 ListSealedSecrets(ctx context.Context, scope domain.SettingsScope) ([]SealedSecret, error)
847 // DeleteSecret deletes a secret, or returns domain.ErrNotFound.
848 DeleteSecret(ctx context.Context, scope domain.SettingsScope, name string) error
849 // PutVariable creates or replaces a variable.
850 PutVariable(ctx context.Context, scope domain.SettingsScope, name, value string, at time.Time) (domain.Variable, error)
851 // GetVariable returns a variable, or domain.ErrNotFound.
852 GetVariable(ctx context.Context, scope domain.SettingsScope, name string) (domain.Variable, error)
853 // ListVariables lists the variables of one scope ordered by name.
854 ListVariables(ctx context.Context, scope domain.SettingsScope) ([]domain.Variable, error)
855 // DeleteVariable deletes a variable, or returns domain.ErrNotFound.
856 DeleteVariable(ctx context.Context, scope domain.SettingsScope, name string) error
857 // CreateEnvironment creates the environment, or returns the existing one
858 // (compared without regard to case) unchanged.
859 CreateEnvironment(ctx context.Context, repo domain.RepoName, name string, at time.Time) (domain.Environment, error)
860 // ListEnvironments lists the environments of a repository ordered by name.
861 ListEnvironments(ctx context.Context, repo domain.RepoName) ([]domain.Environment, error)
862 // DeleteEnvironment deletes the environment with its secrets and
863 // variables, or returns domain.ErrNotFound.
864 DeleteEnvironment(ctx context.Context, repo domain.RepoName, name string) error
865}
866
867// JobSettingsRequest asks for the variables, and when wanted the secrets, a job
868// sees.
869type JobSettingsRequest struct {
870 Repo domain.RepoName
871 // Environment is the environment of the job, "" for none.
872 Environment string
873 // CreateEnvironment creates the environment first when it does not exist
874 // (a job that names one creates it on first use).
875 CreateEnvironment bool
876 // WantSecrets asks for secrets at all: false for a run that is not
877 // trusted. SecretNames are the (upper-cased) names to return, or AllSecrets
878 // for every secret of the scopes.
879 WantSecrets bool
880 SecretNames []string
881 AllSecrets bool
882}
883
884// JobSettings are what a job sees: the merged variables and secrets, with the
885// environment over the repository over the instance.
886type JobSettings struct {
887 Vars map[string]string
888 Secrets map[string]string
889 // SecretsOff is set when secrets were wanted but the server has no
890 // ERMITE_SECRETS_KEY, so none can be given.
891 SecretsOff bool
892}
893
894// CIJobSettings resolves the variables and secrets of a job. *usecase.CISettings
895// implements it.
896type CIJobSettings interface {
897 Resolve(ctx context.Context, req JobSettingsRequest) (JobSettings, error)
898}