detached 5278df2

ci_refevents.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase
4
5import (
6	"context"
7	"errors"
8	"fmt"
9	"strings"
10
11	"bnema.dev/ermite-actions/workflow"
12	"bnema.dev/ermite/internal/domain"
13)
14
15// The events create and delete (CI-4, D6): a branch or a tag is created or
16// deleted. As on GitHub ("Events that trigger workflows"), a create run is on
17// the created ref, its last commit, and runs the workflows of that commit; a
18// delete run is on the default branch, its last commit, and runs the workflows
19// of that commit: the deleted ref is gone. Neither has filters, and GitHub's
20// skip directives do not apply to them. A push that creates or deletes more
21// than three tags creates no event for them.
22//
23// Trust: a create run is trusted when the pusher is and the record of the
24// created ref (ci_tips.go) names the commit the run executes; a delete run when
25// the deleter is and a trusted principal set the default branch to the commit
26// it executes. The deleted ref's record went with the ref and is never looked
27// at. Job tokens start neither (OnPush).
28
29const (
30	eventCreate = "create"
31	eventDelete = "delete"
32)
33
34// startCreateRuns starts the create workflows of a ref a push created, from
35// the files of the commit it points to (files; an invalid file is reported by
36// the push run).
37func (c *CI) startCreateRuns(ctx context.Context, repo domain.Repo, t ciTrigger, f pushFacts, files []workflowFile) error {
38	newID, err := domain.ParseObjectID(f.update.New)
39	if err != nil {
40		return err
41	}
42	ct := t
43	ct.trusted = t.trusted && c.refTrusted(ctx, repo.Name, pinnedRef{ref: f.update.Ref, commit: f.commit, object: newID})
44	if t.trusted && !ct.trusted {
45		ctxLog(ctx).Info().Str("repo", repo.Name.String()).Str("ref", f.update.Ref).
46			Msg("ci create runs are not trusted: the created ref is not at a commit a trusted principal pushed")
47	}
48	cf := f
49	cf.payload = nil
50	cf.custom = &customEvent{name: eventCreate, ref: f.update.Ref}
51	return c.startRefEventRuns(ctx, repo, ct, cf, files)
52}
53
54// handleDelete starts the delete workflows of the default branch for a deleted
55// branch or tag; deletedTags is the number of tags the push deletes.
56func (c *CI) handleDelete(ctx context.Context, t ciTrigger, u domain.RefUpdate, deletedTags int) error {
57	if strings.HasPrefix(u.Ref, "refs/tags/") && deletedTags > workflow.MaxTagsPerPush {
58		return nil
59	}
60	repo, err := c.cfg.Meta.Get(ctx, t.repo)
61	if err != nil {
62		return nil // deleted meanwhile
63	}
64	tip := c.defaultTipOf(ctx, t.repo)
65	if tip.commit.IsZero() {
66		return nil // no default branch to run from
67	}
68	head, _, err := c.cfg.Git.ListRange(ctx, t.repo, domain.ObjectID{}, tip.commit, 1)
69	if err != nil {
70		return fmt.Errorf("read the head commit of %s: %w", tip.branch, err)
71	}
72	if len(head) == 0 {
73		return fmt.Errorf("read the head commit of %s: %w", tip.branch, domain.ErrNotFound)
74	}
75	files, err := c.workflowFiles(ctx, t.repo, tip.commit)
76	if err != nil || len(files) == 0 {
77		return err
78	}
79	dt := t
80	dt.trusted = t.trusted && tip.trusted
81	if t.trusted && !tip.trusted {
82		ctxLog(ctx).Info().Str("repo", t.repo.String()).Str("ref", u.Ref).Str("tip", tip.commit.String()).
83			Msg("ci delete runs are not trusted: the default branch tip was not pushed by a trusted principal")
84	}
85	f := pushFacts{
86		update: domain.RefUpdate{Ref: "refs/heads/" + tip.branch, Old: domain.ZeroOID, New: tip.commit.String()},
87		commit: tip.commit, head: head[0], defaultBranch: tip.branch,
88		custom: &customEvent{name: eventDelete, ref: u.Ref},
89	}
90	return c.startRefEventRuns(ctx, repo, dt, f, files)
91}
92
93// startRefEventRuns starts the workflows of files that are on the event of
94// f.custom (create or delete).
95func (c *CI) startRefEventRuns(ctx context.Context, repo domain.Repo, t ciTrigger, f pushFacts, files []workflowFile) error {
96	var errs []error
97	for _, file := range files {
98		if file.problem != "" {
99			continue
100		}
101		wf, perrs := parseWorkflow(file.path, file.content)
102		if len(perrs) > 0 || wf.On == nil || !wf.On.Has(f.custom.name) {
103			continue
104		}
105		if problem := c.matrixProblem(ctx, repo, t, f, file.path, wf); problem.text != "" {
106			name := file.path
107			if problem.named {
108				name = workflowName(wf, file.path)
109			}
110			errs = append(errs, c.startInvalid(ctx, repo, t, f, file.path, name, problem.text))
111			continue
112		}
113		errs = append(errs, c.startRun(ctx, repo, t, f, file.path, string(file.content), wf))
114	}
115	return errors.Join(errs...)
116}
117
118// refEventPayload is the payload of a create or a delete event: the ref by its
119// short name and its type; create adds the default branch and the description
120// of the repository (ermite has none: null). pusher_type is "user": a job
121// token, GitHub's "deploy key" case aside, starts no run.
122func refEventPayload(name, ref, defaultBranch string, event map[string]any) {
123	refName, refType := strings.TrimPrefix(ref, "refs/heads/"), "branch"
124	if tag, ok := strings.CutPrefix(ref, "refs/tags/"); ok {
125		refName, refType = tag, "tag"
126	}
127	event["ref"], event["ref_type"], event["pusher_type"] = refName, refType, "user"
128	if name == eventCreate {
129		event["master_branch"], event["description"] = defaultBranch, nil
130	}
131}

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 2

Tags 45

↑↓ moveenter openesc close