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.

Blame · ci/validate.go

Line attribution on main · 164 line(s)

f5456470
Keenan Nemetz
13d ago
1 package 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
11 import (
4ac42680
Keenan Nemetz
7d ago
12 "errors"
f5456470
Keenan Nemetz
13d ago
13 "fmt"
14 "path"
15 "path/filepath"
16 "strings"
17 )
18
4ac42680
Keenan Nemetz
7d ago
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.
23 const 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.
29 func 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.
45 func ReservedEnvName(name string) bool { return reservedEnv[name] }
46
f5456470
Keenan Nemetz
13d ago
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.
49 func 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.
65 func 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/")
e5478085
Keenan Nemetz
11d ago
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/")
f5456470
Keenan Nemetz
13d ago
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.
86 func 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.
118 func 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.
124 func 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.
148 func 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 }