detached 5f5b000

ci_tips.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/internal/domain"
12	"bnema.dev/ermite/internal/ports"
13)
14
15// Who moves a branch or a tag (CI-3, D1; domain.CIRefTip). A scheduled run has
16// no pusher of its own: like GitHub's, it is the run of the last commit of the
17// default branch, and it takes the actor and the trust of whoever moved that
18// branch to that commit. A dispatched run is capped by the trust of the ref it
19// runs on. Without this a token limited to some branches could push the first
20// branch of an empty repository (which becomes the default branch) with a cron
21// workflow, and have it run as the owner with the owner's secrets.
22//
23// The record is written BEFORE the ref moves: by the pre-receive decision of a
24// push (Pushes.Check) and by a merge before it updates the target, under the
25// merge lock. Written after, a delayed write could leave an earlier pusher's
26// record on a tip moved back to the same commit (owner A, agent B, agent A).
27// Writing it is part of the decision: when it fails the push or the merge is
28// refused, never let through unrecorded. A push that is refused later leaves a
29// record that names an object the ref does not have, which is not trusted.
30
31// recordRefTips stores who is about to move the branches and tags of a push or a
32// merge, and removes the records of the refs it deletes. actor is what the runs
33// show, pusher and trusted are as for a push run. refs/pull/* is ermite's own
34// and has none.
35func recordRefTips(ctx context.Context, tips ports.CIRefTipStore, repo domain.RepoName, updates []domain.RefUpdate, actor string, pusher domain.CIPusher, trusted bool) error {
36	if tips == nil {
37		return nil
38	}
39	for _, u := range updates {
40		if !strings.HasPrefix(u.Ref, "refs/heads/") && !strings.HasPrefix(u.Ref, "refs/tags/") {
41			continue
42		}
43		var err error
44		if u.IsDelete() {
45			err = tips.DeleteRefTip(ctx, repo, u.Ref)
46		} else {
47			err = tips.PutRefTip(ctx, domain.CIRefTip{Repo: repo, Ref: u.Ref, Sha: u.New, Actor: actor, Pusher: pusher, Trusted: trusted})
48		}
49		if errors.Is(err, domain.ErrNotFound) {
50			continue // the repository is gone: nothing to record, nothing to run
51		}
52		if err != nil {
53			return fmt.Errorf("record who pushes %s: %w", u.Ref, err)
54		}
55	}
56	return nil
57}
58
59// refTip returns the record of a ref when it names the object the ref has now;
60// ok is false when there is none, it is for another object, or it cannot be
61// read (logged): the caller then treats the ref as not trusted.
62func (c *CI) refTip(ctx context.Context, repo domain.RepoName, ref string, objects ...domain.ObjectID) (domain.CIRefTip, bool) {
63	if c.cfg.RefTips == nil {
64		return domain.CIRefTip{}, false
65	}
66	t, err := c.cfg.RefTips.GetRefTip(ctx, repo, ref)
67	if err != nil {
68		if !errors.Is(err, domain.ErrNotFound) {
69			ctxLog(ctx).Error().Err(err).Str("repo", repo.String()).Str("ref", ref).Msg("ci: read who pushed a ref: it is not trusted")
70		}
71		return domain.CIRefTip{}, false
72	}
73	for _, o := range objects {
74		if !o.IsZero() && t.Sha == o.String() {
75			return t, true
76		}
77	}
78	return domain.CIRefTip{}, false
79}
80
81// scheduleActor returns the actor, the pusher and the trust of a scheduled run
82// on the tip of a branch: those of the record of the branch when it names this
83// very commit; otherwise the owner's name, no pusher and no trust.
84func (c *CI) scheduleActor(ctx context.Context, repo domain.RepoName, branch string, tip domain.ObjectID) (actor string, pusher domain.CIPusher, trusted bool) {
85	if t, ok := c.refTip(ctx, repo, "refs/heads/"+branch, tip); ok {
86		return t.Actor, t.Pusher, t.Trusted
87	}
88	return domain.AuthorName(domain.Owner()), domain.CIPusher{}, false
89}
90
91// refTrusted reports whether the ref was set to exactly the pinned object by a
92// trusted principal. The record must name the object the dispatch resolved (the
93// commit, or the tag object of an annotated tag), which the dispatch captured
94// together with the commit it runs: the refs are not read again here, so a tag
95// retargeted since cannot lend its new record to the old commit.
96func (c *CI) refTrusted(ctx context.Context, repo domain.RepoName, pin pinnedRef) bool {
97	t, ok := c.refTip(ctx, repo, pin.ref, pin.object)
98	return ok && t.Trusted
99}

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