main 98594d0

uses.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package action
4
5import (
6	"fmt"
7	"strings"
8)
9
10// UsesKind says where the action named by a "uses:" value lives.
11type UsesKind int
12
13// The kinds of "uses:" value.
14const (
15	// UsesDocker is "docker://image[:tag]".
16	UsesDocker UsesKind = iota + 1
17	// UsesLocal is "./path" or ".\path", relative to the workspace.
18	UsesLocal
19	// UsesRepository is "owner/repo[/path]@ref".
20	UsesRepository
21	// UsesSelfRepository is "$/[path]": an action in the repository of the
22	// workflow or action that contains the step.
23	UsesSelfRepository
24)
25
26// Uses is a parsed "uses:" value of a step.
27type Uses struct {
28	Kind UsesKind
29	// Image is the image of a UsesDocker value, without "docker://".
30	Image string
31	// Path is the value itself for UsesLocal ("./x"), the directory inside
32	// the repository for UsesRepository ("" for the repository's root, else
33	// "a/b" with / as separator), and the directory after "$/" for
34	// UsesSelfRepository.
35	Path string
36	// Owner and Repo name the repository of a UsesRepository value, and Name is
37	// "Owner/Repo". Ref is its ref: a branch, a tag or a commit sha.
38	Owner, Repo, Name, Ref string
39}
40
41// UsesError is the error of [ParseUses].
42type UsesError struct {
43	// Value is the "uses:" value that was refused.
44	Value string
45	// Message is the text GitHub's runner reports, without a position.
46	Message string
47}
48
49func (e *UsesError) Error() string { return e.Message }
50
51// ParseUses parses the "uses:" value of a step. It is the runner's check
52// (WorkflowTemplateConverter.ConvertToStep and the legacy
53// PipelineTemplateConverter.ConvertToStep, which agree) and returns its
54// messages:
55//
56//   - "Expected format {org}/{repo}[/path]@ref. Actual 'x'" for a value that is
57//     not docker://, ./ or .\ or $/ and does not have exactly one "@", a
58//     non-empty ref, and an owner and a repository before it. Path separators
59//     are / and \, and empty segments are ignored ("a//b@c" is a/b@c, and
60//     "/a/b@c" too).
61//   - "Self-repository references do not support an '@ref' suffix. Actual
62//     '$/x@v1'" for a $/ value with an "@".
63//
64// The prefixes docker:// are compared as written (case-sensitive), as the
65// converter does. The language service words the first message differently
66// ("{owner}/{repo}[/path]@{ref}") and refuses a blank value with another
67// text; PORTING.md lists the differences. Whether the repository exists, or
68// whether the ref does, is for whoever resolves the action to say.
69func ParseUses(s string) (Uses, error) {
70	switch {
71	case strings.HasPrefix(s, "docker://"):
72		return Uses{Kind: UsesDocker, Image: s[len("docker://"):]}, nil
73	case strings.HasPrefix(s, "./"), strings.HasPrefix(s, `.\`):
74		return Uses{Kind: UsesLocal, Path: s}, nil
75	case strings.HasPrefix(s, "$/"):
76		path := strings.TrimLeft(s[2:], "/")
77		if strings.Contains(path, "@") {
78			return Uses{}, &UsesError{Value: s, Message: fmt.Sprintf("Self-repository references do not support an '@ref' suffix. Actual '%s'", s)}
79		}
80		return Uses{Kind: UsesSelfRepository, Path: path}, nil
81	}
82	segments := strings.Split(s, "@")
83	path := strings.FieldsFunc(segments[0], func(r rune) bool { return r == '/' || r == '\\' })
84	if len(segments) != 2 || len(path) < 2 || segments[1] == "" {
85		return Uses{}, &UsesError{Value: s, Message: fmt.Sprintf("Expected format {org}/{repo}[/path]@ref. Actual '%s'", s)}
86	}
87	return Uses{
88		Kind:  UsesRepository,
89		Owner: path[0],
90		Repo:  path[1],
91		Name:  path[0] + "/" + path[1],
92		Path:  strings.Join(path[2:], "/"),
93		Ref:   segments[1],
94	}, nil
95}
96
97// ImageKind says what a docker action's "image" is.
98type ImageKind int
99
100// The kinds of "runs.image".
101const (
102	// ImageDockerfile is a path to a Dockerfile in the action's directory.
103	ImageDockerfile ImageKind = iota + 1
104	// ImageRegistry is "docker://image[:tag]": an image to pull.
105	ImageRegistry
106)
107
108// Image is a parsed "runs.image" of a docker action.
109type Image struct {
110	Kind ImageKind
111	// Path is the Dockerfile's path relative to the action's directory, for
112	// ImageDockerfile.
113	Path string
114	// Ref is the image to pull, without "docker://", for ImageRegistry.
115	Ref string
116}
117
118// ParseImage classifies the "image" of a docker action as the runner does
119// (ActionManager.PrepareRepositoryActionAsync and DockerUtil.IsDockerfile):
120// a path whose last segment starts with "Dockerfile." or ends with
121// "Dockerfile" (case-insensitively) is a Dockerfile, "docker://" (any case)
122// followed by an image is an image to pull, and anything else fails with
123// "'x' should be either '[path]/Dockerfile' or 'docker://image[:tag]'.".
124// [Parse] does not call it: the runner checks the image when it prepares the
125// action, after loading the manifest.
126func ParseImage(image string) (Image, error) {
127	if hasPrefixFold(image, "docker://") {
128		return Image{Kind: ImageRegistry, Ref: image[len("docker://"):]}, nil
129	}
130	last := image[strings.LastIndex(image, "/")+1:]
131	if hasPrefixFold(last, "Dockerfile.") || hasSuffixFold(last, "Dockerfile") {
132		return Image{Kind: ImageDockerfile, Path: image}, nil
133	}
134	return Image{}, &UsesError{Value: image, Message: fmt.Sprintf("'%s' should be either '[path]/Dockerfile' or 'docker://image[:tag]'.", image)}
135}
136
137func hasPrefixFold(s, prefix string) bool {
138	return len(s) >= len(prefix) && asciiEqualFold(s[:len(prefix)], prefix)
139}
140
141func hasSuffixFold(s, suffix string) bool {
142	return len(s) >= len(suffix) && asciiEqualFold(s[len(s)-len(suffix):], suffix)
143}

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 1

Tags 3

↑↓ moveenter openesc close