Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 29m ago 32290114
history blame raw 6.2 KiB
1package ci
2
3// Validation shared by the push-time workflow checks and the runner.
4//
5// The server validates a workflow when it is pushed, and then sends the runner a
6// job. The runner is the party that acts on that job, and the channel is the trust
7// boundary, so it re-checks the fields it is about to build from rather than
8// trusting that the two sides agree. Sharing the checks is what stops the two
9// copies from drifting apart.
10
11import (
12 "errors"
13 "fmt"
14 "path"
15 "path/filepath"
16 "strings"
17)
18
19// MaxSecretBytes is the most one secret may hold. It is the kernel's
20// MAX_ARG_STRLEN — 32 pages — which is the limit on a single string in a process's
21// environment. The limit is stated for the whole NAME=value pair, so a longer name
22// leaves less room for the value.
23const MaxSecretBytes = 32 * 4096
24
25// ValidateSecretName reports why name cannot be a secret. A secret's name is also
26// the variable a step reads it as, so it follows an environment variable's rule
27// with the case made explicit: one spelling, so a workflow that declares
28// deploy_key does not silently never match DEPLOY_KEY.
29func ValidateSecretName(name string) error {
30 switch {
31 case name == "":
32 return errors.New("a secret needs a name")
33 case len(name) > 64:
34 return fmt.Errorf("%q is %d bytes; a secret name is at most 64", name, len(name))
35 case !secretNameRE.MatchString(name):
36 return fmt.Errorf("%q is not a secret name (upper-case letters, digits and _, starting with a letter)", name)
37 }
38 return nil
39}
40
41// ReservedEnvName reports whether the runner sets a variable with this name
42// itself. A secret cannot replace one: the value would be sourced after the
43// runner's own, and the step would see a CI flag or a home directory the runner
44// never chose.
45func ReservedEnvName(name string) bool { return reservedEnv[name] }
46
47// ValidObjectID reports whether s is a git object id: 40 hexadecimal characters in
48// a SHA-1 repository, 64 in a SHA-256 one.
49func ValidObjectID(s string) bool {
50 if len(s) != 40 && len(s) != 64 {
51 return false
52 }
53 for i := 0; i < len(s); i++ {
54 c := s[i]
55 if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
56 return false
57 }
58 }
59 return true
60}
61
62// ValidRef reports whether ref is a full branch or tag name. The runner only ever
63// checks out these two namespaces, and every component is a plain path segment:
64// no empty or dot components, no characters git refuses, no control characters.
65func ValidRef(ref string) bool {
66 var name string
67 switch {
68 case strings.HasPrefix(ref, "refs/heads/"):
69 name = strings.TrimPrefix(ref, "refs/heads/")
70 case strings.HasPrefix(ref, "refs/tags/"):
71 name = strings.TrimPrefix(ref, "refs/tags/")
72 case strings.HasPrefix(ref, "refs/vole/candidates/"):
73 // A combined result is a server-published commit a runner is asked to
74 // build before it lands. It is not a branch or a tag, but it is a ref the
75 // server owns and the runner must fetch by id.
76 name = strings.TrimPrefix(ref, "refs/vole/candidates/")
77 default:
78 return false
79 }
80 return ValidRefName(name)
81}
82
83// ValidRefName reports whether name is a valid branch or tag name without the
84// namespace prefix. It is deliberately stricter than "git will take it": a name
85// that a workflow, a lease and a record can all agree on is short and plain.
86func ValidRefName(name string) bool {
87 if name == "" || len(name) > 255 {
88 return false
89 }
90 if strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") ||
91 strings.HasSuffix(name, ".") || strings.HasSuffix(name, ".lock") {
92 return false
93 }
94 if strings.Contains(name, "..") || strings.Contains(name, "//") ||
95 strings.Contains(name, "@{") || strings.Contains(name, "\\") {
96 return false
97 }
98 for _, r := range name {
99 switch r {
100 case ' ', '~', '^', ':', '?', '*', '[', '\x7f':
101 return false
102 }
103 if r < 0x20 {
104 return false
105 }
106 }
107 for _, part := range strings.Split(name, "/") {
108 if part == "" || part == "." || part == ".." {
109 return false
110 }
111 }
112 return true
113}
114
115// ValidEnvName reports whether name is a variable name an environment entry may
116// use. Values, not names, are what an image or a workflow spends; the name has to
117// be one a shell and a container runtime both accept.
118func ValidEnvName(name string) bool { return envNameRE.MatchString(name) }
119
120// ValidateArtifactPath reports why p is not a usable artifact pattern. A pattern is
121// relative to the checkout, names files rather than a directory, and cannot climb
122// out of the tree; the runner expands it, so this is the check that decides what it
123// is allowed to expand.
124func ValidateArtifactPath(p string) error {
125 switch {
126 case p == "":
127 return fmt.Errorf("path is required (a glob relative to the checkout, e.g. dist/*)")
128 case len(p) > MaxArtifactPathBytes:
129 return fmt.Errorf("path is %d bytes, limit %d", len(p), MaxArtifactPathBytes)
130 case strings.HasPrefix(p, "/") || filepath.IsAbs(p):
131 return fmt.Errorf("%q must be relative to the checkout", p)
132 case strings.Contains("/"+p+"/", "/../") || p == "..":
133 return fmt.Errorf("%q climbs out of the checkout", p)
134 case strings.HasSuffix(p, "/"):
135 return fmt.Errorf("%q names a directory; declare the files, e.g. %s*", p, p)
136 case strings.Contains(p, "**"):
137 return fmt.Errorf("%q uses **, which does not mean what it does elsewhere; use %s*",
138 p, strings.SplitN(p, "**", 2)[0])
139 }
140 if _, err := path.Match(p, "probe"); err != nil {
141 return fmt.Errorf("%q is not a glob", p)
142 }
143 return nil
144}
145
146// ValidatePagesPath reports why p is not a usable site directory. A site is one
147// directory inside the checkout: no glob, no absolute path, no climb out.
148func ValidatePagesPath(p string) error {
149 switch {
150 case p == "":
151 return fmt.Errorf("a directory is required, e.g. site/ or dist")
152 case len(p) > MaxArtifactPathBytes:
153 return fmt.Errorf("path is %d bytes, limit %d", len(p), MaxArtifactPathBytes)
154 case strings.HasPrefix(p, "/") || filepath.IsAbs(p):
155 return fmt.Errorf("%q must be relative to the checkout", p)
156 case strings.Contains("/"+p+"/", "/../") || p == "..":
157 return fmt.Errorf("%q climbs out of the checkout", p)
158 case strings.ContainsAny(p, "*?["):
159 return fmt.Errorf("%q is a glob; a site is one directory, not a set of them", p)
160 case strings.ContainsAny(p, "\x00\r\n:"):
161 return fmt.Errorf("%q contains a character the runtime cannot take", p)
162 }
163 return nil
164}
165