detached aa7340c

ci.go RawHistoryFolder

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// WorkflowGuard finds the changes of a push to the workflow files of a
411// repository.
412type WorkflowGuard interface {
413	// ChangedWorkflow returns the first path under .github/workflows/ that the
414	// updates change, "" for none. Two checks, as GitHub judges the token of a
415	// job: (1) every commit the push brings (those no ref has yet) is compared
416	// with each of its parents (a root commit with nothing) and is a change when
417	// it has workflows that differ from every parent's; (2) the workflows a ref
418	// ends with must be its old ones or none for an update, and the default
419	// branch head's or none for a new ref (none when there is no head). A
420	// workflows directory that disappears is no change, and a deleted ref changes
421	// nothing. objects are those of the push in progress; a directory that is not
422	// inside the repository is refused. A push beyond the bounds of the check
423	// (commits, parents) is an error that matches domain.ErrInvalid.
424	ChangedWorkflow(ctx context.Context, repo domain.RepoName, updates []domain.RefUpdate, objects PushObjects) (path string, err error)
425}
426
427// --- inbound ---
428
429// RegisterRunnerParams describe a runner to register.
430type RegisterRunnerParams struct {
431	Name      string
432	Labels    []string
433	Version   string
434	Isolation domain.CIIsolation
435	Arch, OS  string
436	Replace   bool
437}
438
439// RegisteredRunner is a new runner with its secret, shown once.
440type RegisteredRunner struct {
441	Runner domain.CIRunner
442	Token  string
443}
444
445// RunnerJob is a job given to a runner.
446type RunnerJob struct {
447	Job   domain.CIJob
448	Token string
449	// GithubToken is the job repository token, the "github.token" of the job.
450	GithubToken  string
451	Repo         domain.RepoName
452	Sha, Ref     string
453	WorkflowPath string
454	WorkflowYAML string
455	// Environment is the environment the job deploys to, "" for none.
456	Environment string
457	// Vars are the merged variables of the job (environment over repository
458	// over instance), already in its "vars" context.
459	Vars map[string]string
460	// Secrets are the secrets the job refers to, in plain text: empty when the
461	// run is not trusted, when the job refers to none and when the server has
462	// no key. They go to the runner and nowhere else: never into the run
463	// record, the github context or a log.
464	Secrets map[string]string
465	// Trusted is the trusted flag of the run: secrets were allowed.
466	Trusted bool
467	// Permissions are the permissions of GithubToken (key to level), after the
468	// trust rule: no write when the run is not trusted.
469	Permissions map[string]string
470	// Cache is the scope of the job in the runner's cache (Job.cache).
471	Cache JobCache
472	// IDTokenURL is the token endpoint of the job's OIDC ID tokens and
473	// IDTokenRequestToken its bearer (Job.id_token_url and
474	// id_token_request_token); both "" when the job may not have ID tokens.
475	IDTokenURL          string
476	IDTokenRequestToken string
477}
478
479// JobCache is what a job may do in the cache the runner serves: save entries
480// under WriteRef only ("" for none) and restore those of ReadRefs, most
481// specific first. Repository is "owner/name".
482type JobCache struct {
483	Repository string
484	WriteRef   string
485	ReadRefs   []string
486}
487
488// String describes the job without its secrets and tokens, so that a job
489// printed by mistake leaks nothing.
490func (j RunnerJob) String() string {
491	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)
492}
493
494// GoString is String for %#v.
495func (j RunnerJob) GoString() string { return j.String() }
496
497// FetchJobParams selects the job a runner asks for.
498type FetchJobParams struct {
499	Labels []string
500	PollID string
501	// Version is the version of the running runner, "" from an older runner.
502	Version string
503}
504
505// FetchJobResult is the answer of a poll.
506type FetchJobResult struct {
507	Job *RunnerJob
508	// RetryAfter is how long the runner waits before polling again.
509	RetryAfter time.Duration
510}
511
512// StepUpdate is the state of one step as a runner reports it.
513type StepUpdate struct {
514	Number     int32
515	Name       string
516	State      domain.CIStepState
517	Conclusion domain.CIConclusion
518	// Outcome is how the step ended before continue-on-error; none when the
519	// runner does not report it.
520	Outcome     domain.CIConclusion
521	StartedAt   time.Time
522	CompletedAt time.Time
523}
524
525// CompleteJobParams end a job.
526type CompleteJobParams struct {
527	JobID          string
528	Conclusion     domain.CIConclusion
529	Outputs        map[string]string
530	RunnerShutdown bool
531	Steps          []StepUpdate
532	// Annotations replace those of each step they name, as in UpdateJob.
533	Annotations []domain.CIAnnotation
534}
535
536// CompleteJobResult is the stored result of a job.
537type CompleteJobResult struct {
538	Conclusion domain.CIConclusion
539	Error      string
540	Applied    bool
541}
542
543// ResolvedAction is the resolution of one "uses" of ResolveActions: where to
544// download the action, or Error (then only Ref is set).
545type ResolvedAction struct {
546	Ref           string
547	NameWithOwner string
548	Path          string
549	ResolvedSHA   string
550	TarballURL    string
551	Token         string
552	Source        domain.CIActionSource
553	Error         string
554}
555
556// RemoteRef is a ref of a remote repository, as `git ls-remote` lists it.
557type RemoteRef struct {
558	// Name is the full ref name ("refs/heads/main", "refs/tags/v1").
559	Name string
560	// ID is the object the ref points to (the tag object of an annotated tag).
561	ID string
562	// Peeled is the commit an annotated tag points to; empty for other refs.
563	Peeled string
564}
565
566// GitRemote lists the refs of a repository on github.com, with no
567// credentials. It returns domain.ErrNotFound for a repository that does not
568// exist or needs credentials (GitHub answers alike), and an error matching
569// domain.ErrUnavailable when it cannot tell (network, timeout, too large an
570// answer).
571type GitRemote interface {
572	ListRefs(ctx context.Context, owner, repo string) ([]RemoteRef, error)
573}
574
575// WorkflowFetcher downloads one reusable workflow file from github.com at a
576// commit that was already resolved (CI-3, D7), with no credentials. It returns
577// domain.ErrNotFound for a file or repository that does not exist or needs
578// credentials, domain.ErrResourceExhausted for a file larger than maxBytes, and
579// an error matching domain.ErrUnavailable when it cannot tell.
580type WorkflowFetcher interface {
581	FetchWorkflow(ctx context.Context, owner, repo, sha, path string, maxBytes int64) ([]byte, error)
582}
583
584// JobSignal is one message of WatchJob.
585type JobSignal struct {
586	Keepalive bool
587	// Cancel is set for a cancel request.
588	Cancel *JobCancel
589	// Ended is set when the job has ended.
590	Ended *domain.JobEndedError
591}
592
593// JobCancel is a cancel request for a running job.
594type JobCancel struct {
595	Reason      domain.CICancelReason
596	Message     string
597	RequestedAt time.Time
598}
599
600// RunnerService is what runners call. The secret is the bearer secret of the
601// call: the runner token for FetchJob, the job token for the other calls.
602// A bad secret is domain.ErrUnauthenticated; a job token of another job than
603// the one named is domain.ErrForbidden. *usecase.CI implements it.
604type RunnerService interface {
605	Register(ctx context.Context, p domain.Principal, params RegisterRunnerParams) (RegisteredRunner, error)
606	FetchJob(ctx context.Context, secret string, params FetchJobParams) (FetchJobResult, error)
607	// UpdateJob applies the steps and the annotations whole or not at all: for
608	// each step number present in annotations, the annotations of that step are
609	// replaced (also for a step already completed, or not in steps).
610	UpdateJob(ctx context.Context, secret, jobID string, steps []StepUpdate, annotations []domain.CIAnnotation) error
611	AppendLog(ctx context.Context, secret, jobID string, step int32, offset int64, lines []domain.CILogLine) (next int64, err error)
612	Heartbeat(ctx context.Context, secret, jobID string) error
613	// WatchJob calls ready once the job token is valid and the stream is
614	// registered, before any message: the adapter flushes the response headers
615	// then, because a client's call returns only when they arrive.
616	WatchJob(ctx context.Context, secret, jobID string, ready func(), send func(JobSignal) error) error
617	CompleteJob(ctx context.Context, secret string, params CompleteJobParams) (CompleteJobResult, error)
618	// ResolveActions resolves the "uses" of the job's steps, one answer per
619	// reference in the same order. A reference that cannot be resolved is not
620	// an error: its answer carries Error. A local ("./") or Docker
621	// ("docker://") reference, or more than 100, is domain.ErrInvalid.
622	ResolveActions(ctx context.Context, secret, jobID string, uses []string) ([]ResolvedAction, error)
623	// UploadSummary stores the summary of a step, replacing the one it has; an
624	// empty markdown removes it. More than 1 MiB, invalid UTF-8 or a step
625	// number outside 1 to 1,000 is domain.ErrInvalid.
626	UploadSummary(ctx context.Context, secret, jobID string, step int32, markdown string) error
627}
628
629// JobView is a job as owners see it.
630type JobView struct {
631	domain.CIJob
632	// Lines is the number of lines of its log now.
633	Lines int64
634	// RunnerGone is set when the runner of the job was removed since.
635	RunnerGone bool
636}
637
638// StepRanges returns where the lines of every step are in the log.
639func (v JobView) StepRanges() []domain.StepRange { return domain.StepRanges(v.Steps, v.Lines) }
640
641// ListRunsParams select a page of runs.
642type ListRunsParams struct {
643	PageSize  int
644	PageToken string
645	// Workflow restricts the result to a workflow file: its path or its file
646	// name. Branch restricts it to the runs of a branch, by short name. Status
647	// restricts it to the runs of that status. Empty means no restriction.
648	Workflow string
649	Branch   string
650	Status   domain.CIStatus
651}
652
653// RunPage is a page of runs, newest first.
654type RunPage struct {
655	Runs          []domain.CIRun
656	NextPageToken string
657}
658
659// RunDetail is a run with its jobs.
660type RunDetail struct {
661	Run           domain.CIRun
662	Jobs          []JobView
663	LatestAttempt int32
664}
665
666// LogBatch is a message of a log stream: consecutive lines from Offset.
667type LogBatch struct {
668	Offset int64
669	Lines  []JobLogLine
670}
671
672// JobLogLine is a log line with its step.
673type JobLogLine struct {
674	Step int32
675	domain.CILogLine
676}
677
678// GetJobLogParams select the log to stream.
679type GetJobLogParams struct {
680	JobID string
681	// Attempt is the attempt of the job, 0 for the latest.
682	Attempt int32
683	Offset  int64
684	Follow  bool
685	// Ready, when not nil, is called once the access is checked and the
686	// stream is counted, before any batch: the adapter flushes the response
687	// headers then (a follow on a job with no line yet would keep the client's
688	// call waiting for the first one).
689	Ready func()
690	// Client identifies where the call comes from, for the limit of streams
691	// per client; empty counts against the total only.
692	Client string
693}
694
695// CheckView is a check with what owners need to read it.
696type CheckView struct {
697	domain.CICheck
698	// WaitingForRunner is set for a queued check whose job no registered
699	// runner can take (no runner has all its labels).
700	WaitingForRunner bool
701}
702
703// ChecksResult are the checks of a commit.
704type ChecksResult struct {
705	Checks    []CheckView
706	Truncated bool
707}
708
709// RunnerPage is a page of runners.
710type RunnerPage struct {
711	Runners       []RunnerView
712	NextPageToken string
713}
714
715// RunnerView is a runner with what is known of it now.
716type RunnerView struct {
717	domain.CIRunner
718	LastSeen time.Time
719	Busy     bool
720	// Online is decided by the server's clock (see usecase.RunnerOnlineWindow).
721	Online bool
722}
723
724// JobAnnotation is an annotation with the job it belongs to.
725type JobAnnotation struct {
726	JobID      string
727	Annotation domain.CIAnnotation
728}
729
730// AnnotationList are the annotations of a run, at most
731// domain.CIMaxListedAnnotations.
732type AnnotationList struct {
733	Annotations []JobAnnotation
734	Truncated   bool
735}
736
737// SummaryList are the summaries of a job, at most domain.CIMaxListedSummaries.
738type SummaryList struct {
739	Summaries []domain.CIStepSummary
740	Truncated bool
741}
742
743// RunService is the owner side of CI. *usecase.CI implements it.
744type RunService interface {
745	ListRuns(ctx context.Context, p domain.Principal, repo domain.RepoName, params ListRunsParams) (RunPage, error)
746	GetRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32) (RunDetail, error)
747	GetJob(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32) (JobView, error)
748	GetJobLog(ctx context.Context, p domain.Principal, repo domain.RepoName, params GetJobLogParams, send func(LogBatch) error) error
749	CancelRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64) (domain.CIRun, error)
750	RerunRun(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, failedOnly bool) (domain.CIRun, error)
751	// DispatchWorkflow starts a run of a workflow file by hand (workflow_dispatch).
752	// The caller needs ci:write on the repository, or is a job token with the
753	// permission actions: write. The run is returned queued.
754	DispatchWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, params DispatchParams) (domain.CIRun, error)
755	ListChecks(ctx context.Context, p domain.Principal, repo domain.RepoName, sha string) (ChecksResult, error)
756	// ListWorkflows returns the workflow files at the tip of the default branch
757	// and the disabled ones whatever the branch holds, ordered by path. Reading
758	// the repository is enough.
759	ListWorkflows(ctx context.Context, p domain.Principal, repo domain.RepoName) ([]WorkflowInfo, error)
760	// DisabledWorkflows returns the paths of the disabled workflows, sorted,
761	// without reading any file: the cheap form of ListWorkflows for the pages.
762	DisabledWorkflows(ctx context.Context, p domain.Principal, repo domain.RepoName) ([]string, error)
763	// DisableWorkflow stops the workflow (its file name or path; it does not
764	// have to exist) from starting runs for any event. EnableWorkflow undoes it.
765	// Both need repo:admin on the repository and are idempotent.
766	DisableWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, file string) (WorkflowInfo, error)
767	EnableWorkflow(ctx context.Context, p domain.Principal, repo domain.RepoName, file string) (WorkflowInfo, error)
768	// ListAnnotations returns the annotations of a run attempt (0 for the
769	// latest), of one job when jobID is not empty. Each job gives those of the
770	// attempt in which it executed.
771	ListAnnotations(ctx context.Context, p domain.Principal, repo domain.RepoName, runID int64, attempt int32, jobID string) (AnnotationList, error)
772	// GetStepSummaries returns the summaries of a job at an attempt (0 for the
773	// latest), by step number. maxBytes above 0 bounds the Markdown read: the
774	// summaries past it come back Omitted, without their Markdown (the API
775	// passes 0, no bound).
776	GetStepSummaries(ctx context.Context, p domain.Principal, repo domain.RepoName, jobID string, attempt int32, maxBytes int64) (SummaryList, error)
777	ListRunners(ctx context.Context, p domain.Principal, size int, token string) (RunnerPage, error)
778	RemoveRunner(ctx context.Context, p domain.Principal, id string) error
779}
780
781// ErrSecretNotOpened means a sealed secret does not open: another key, other
782// additional data (a value moved to another scope or name) or a damaged value.
783var ErrSecretNotOpened = errors.New("secret does not open")
784
785// SecretSealer encrypts the values of secrets at rest. The adapter holds the
786// master key; without one the use cases get no sealer and refuse secrets.
787type SecretSealer interface {
788	// Seal returns plaintext sealed and bound to aad. It never fails.
789	Seal(plaintext, aad []byte) []byte
790	// Open reverses Seal with the same aad, or returns ErrSecretNotOpened.
791	Open(sealed, aad []byte) ([]byte, error)
792	// KeyCheck is a value derived from the key that does not reveal it.
793	KeyCheck() []byte
794}
795
796// SealedSecret is a stored secret: its value is sealed.
797type SealedSecret struct {
798	Name      string
799	Sealed    []byte
800	UpdatedAt time.Time
801}
802
803// CISettingsStore keeps the secrets, variables and environments of CI (ADR
804// 0019). A scope that names a missing repository or environment is
805// domain.ErrNotFound for every write and every read of its content. Names are
806// already upper-cased (domain.ParseSettingName).
807type CISettingsStore interface {
808	// BindSecretsKey records the key check beside the secrets when none is
809	// stored, and returns domain.ErrSecretsKeyMismatch when the stored one
810	// differs.
811	BindSecretsKey(ctx context.Context, check []byte) error
812	// PutSecret creates or replaces a secret.
813	PutSecret(ctx context.Context, scope domain.SettingsScope, name string, sealed []byte, at time.Time) (domain.Secret, error)
814	// GetSealedSecret reads one secret of a scope, or domain.ErrNotFound when
815	// it, the repository or the environment does not exist.
816	GetSealedSecret(ctx context.Context, scope domain.SettingsScope, name string) (SealedSecret, error)
817	// ListSealedSecrets lists the secrets of one scope ordered by name.
818	ListSealedSecrets(ctx context.Context, scope domain.SettingsScope) ([]SealedSecret, error)
819	// DeleteSecret deletes a secret, or returns domain.ErrNotFound.
820	DeleteSecret(ctx context.Context, scope domain.SettingsScope, name string) error
821	// PutVariable creates or replaces a variable.
822	PutVariable(ctx context.Context, scope domain.SettingsScope, name, value string, at time.Time) (domain.Variable, error)
823	// GetVariable returns a variable, or domain.ErrNotFound.
824	GetVariable(ctx context.Context, scope domain.SettingsScope, name string) (domain.Variable, error)
825	// ListVariables lists the variables of one scope ordered by name.
826	ListVariables(ctx context.Context, scope domain.SettingsScope) ([]domain.Variable, error)
827	// DeleteVariable deletes a variable, or returns domain.ErrNotFound.
828	DeleteVariable(ctx context.Context, scope domain.SettingsScope, name string) error
829	// CreateEnvironment creates the environment, or returns the existing one
830	// (compared without regard to case) unchanged.
831	CreateEnvironment(ctx context.Context, repo domain.RepoName, name string, at time.Time) (domain.Environment, error)
832	// ListEnvironments lists the environments of a repository ordered by name.
833	ListEnvironments(ctx context.Context, repo domain.RepoName) ([]domain.Environment, error)
834	// DeleteEnvironment deletes the environment with its secrets and
835	// variables, or returns domain.ErrNotFound.
836	DeleteEnvironment(ctx context.Context, repo domain.RepoName, name string) error
837}
838
839// JobSettingsRequest asks for the variables, and when wanted the secrets, a job
840// sees.
841type JobSettingsRequest struct {
842	Repo domain.RepoName
843	// Environment is the environment of the job, "" for none.
844	Environment string
845	// CreateEnvironment creates the environment first when it does not exist
846	// (a job that names one creates it on first use).
847	CreateEnvironment bool
848	// WantSecrets asks for secrets at all: false for a run that is not
849	// trusted. SecretNames are the (upper-cased) names to return, or AllSecrets
850	// for every secret of the scopes.
851	WantSecrets bool
852	SecretNames []string
853	AllSecrets  bool
854}
855
856// JobSettings are what a job sees: the merged variables and secrets, with the
857// environment over the repository over the instance.
858type JobSettings struct {
859	Vars    map[string]string
860	Secrets map[string]string
861	// SecretsOff is set when secrets were wanted but the server has no
862	// ERMITE_SECRETS_KEY, so none can be given.
863	SecretsOff bool
864}
865
866// CIJobSettings resolves the variables and secrets of a job. *usecase.CISettings
867// implements it.
868type CIJobSettings interface {
869	Resolve(ctx context.Context, req JobSettingsRequest) (JobSettings, error)
870}

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 4

Tags 45

↑↓ moveenter openesc close