Skip to content
296 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.1//! GitHub's workflow syntax: the keys each mapping in a workflow may have,
2//! as GitHub's own schema lists them (`schema/workflow-v1.0.json`, from
3//! actions/languageservices; the tests below check these lists against
4//! it). [`crate::workflow::parse`] refuses any other key, as GitHub does,
5//! except `g1t`, g1t's own settings, on the workflow, a job and a step.
6
7/// g1t's own key, allowed on the workflow, each job and each step.
8pub const EXTENSION: &str = "g1t";
9
10/// The workflow's top level (`workflow-root-strict`).
11pub const WORKFLOW: &[&str] = &["on", "name", "description", "run-name", "defaults", "env", "permissions", "concurrency", "jobs"];
12
13/// A job that runs steps on a runner (`job-factory`).
14pub const JOB: &[&str] = &[
15 "needs",
16 "if",
17 "strategy",
18 "name",
19 "runs-on",
20 "timeout-minutes",
21 "cancel-timeout-minutes",
22 "continue-on-error",
23 "container",
24 "services",
25 "env",
26 "environment",
27 "permissions",
28 "concurrency",
29 "outputs",
30 "defaults",
31 "steps",
32 "snapshot",
33];
34
35/// A job that calls a reusable workflow (`workflow-job`).
36pub const CALLER_JOB: &[&str] = &["name", "uses", "with", "secrets", "needs", "if", "permissions", "concurrency", "strategy"];
37
38/// A step that runs a command (`run-step`).
39pub const RUN_STEP: &[&str] =
40 &["name", "id", "if", "timeout-minutes", "run", "continue-on-error", "env", "working-directory", "shell", "background"];
41
42/// A step that uses an action (`regular-step`).
43pub const ACTION_STEP: &[&str] = &["name", "id", "if", "continue-on-error", "timeout-minutes", "uses", "with", "env", "background"];
44
45/// Keys of GitHub's background steps (`background`, and the `wait-step`,
46/// `wait-all-step`, `cancel-step` and `parallel-step` steps), which g1t
47/// does not run yet.
48pub const BACKGROUND_STEPS: &[&str] = &["background", "wait", "wait-all", "cancel", "parallel"];
49
50/// `strategy`.
51pub const STRATEGY: &[&str] = &["fail-fast", "max-parallel", "matrix"];
52
53/// `runs-on` as a mapping (`runs-on-mapping`).
54pub const RUNS_ON: &[&str] = &["group", "labels"];
55
56/// `container` as a mapping (`container-mapping`).
57pub const CONTAINER: &[&str] = &["image", "options", "env", "ports", "volumes", "credentials"];
58
59/// Each of `services` as a mapping (`service-container-mapping`).
60pub const SERVICE: &[&str] = &["image", "options", "env", "ports", "volumes", "command", "entrypoint", "credentials"];
61
62/// `credentials` of a container or service (`container-registry-credentials`).
63pub const CREDENTIALS: &[&str] = &["username", "password"];
64
65/// `environment` as a mapping (`job-environment-mapping`).
66pub const ENVIRONMENT: &[&str] = &["name", "url", "deployment"];
67
68/// `concurrency` as a mapping (`concurrency-mapping`).
69pub const CONCURRENCY: &[&str] = &["group", "cancel-in-progress", "queue"];
70
71/// `defaults` (`workflow-defaults`, `job-defaults`).
72pub const DEFAULTS: &[&str] = &["run"];
73
74/// `defaults.run` (`workflow-defaults-run`, `job-defaults-run`).
75pub const DEFAULTS_RUN: &[&str] = &["shell", "working-directory"];
76
77/// `permissions` as a mapping (`permissions-mapping`).
78pub const PERMISSIONS: &[&str] = &[
79 "actions",
80 "artifact-metadata",
81 "attestations",
82 "checks",
83 "code-quality",
84 "contents",
85 "copilot-requests",
86 "deployments",
87 "discussions",
88 "drives",
89 "id-token",
90 "issues",
91 "models",
92 "packages",
93 "pages",
94 "pull-requests",
95 "repository-projects",
96 "security-events",
97 "statuses",
98 "vulnerability-alerts",
99];
100
101/// Each entry of `on.schedule` (`cron-mapping`).
102pub const CRON: &[&str] = &["cron", "timezone"];
103
104/// `on.workflow_dispatch` (`workflow-dispatch-mapping`).
105pub const WORKFLOW_DISPATCH: &[&str] = &["inputs"];
106
107/// Each of `on.workflow_dispatch.inputs` (`workflow-dispatch-input`).
108pub const DISPATCH_INPUT: &[&str] = &["description", "type", "required", "default", "options"];
109
110/// `on.workflow_call` (`workflow-call-mapping`).
111pub const WORKFLOW_CALL: &[&str] = &["inputs", "outputs", "secrets"];
112
113/// Each of `on.workflow_call.inputs` (`workflow-call-input-definition`).
114pub const CALL_INPUT: &[&str] = &["description", "type", "required", "default"];
115
116/// Each of `on.workflow_call.outputs` (`workflow-call-output-definition`).
117pub const CALL_OUTPUT: &[&str] = &["description", "value"];
118
119/// Each of `on.workflow_call.secrets` (`workflow-call-secret-mapping-definition`).
120pub const CALL_SECRET: &[&str] = &["description", "required"];
121
122/// What an event under `on` may be given.
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub enum EventSpec {
125 /// Nothing: `create:`.
126 Bare,
127 /// Nothing, or a mapping with these keys.
128 Filters(&'static [&'static str]),
129 /// `schedule`: a list of [`CRON`] mappings.
130 Schedule,
131}
132
133const TYPES: EventSpec = EventSpec::Filters(&["types"]);
134const PULL_REQUEST: EventSpec = EventSpec::Filters(&["types", "branches", "branches-ignore", "paths", "paths-ignore"]);
135
136/// Every event GitHub has, and what each takes (`on-mapping-strict`).
137pub const EVENTS: &[(&str, EventSpec)] = &[
138 ("branch_protection_rule", TYPES),
139 ("check_run", TYPES),
140 ("check_suite", TYPES),
141 ("create", EventSpec::Bare),
142 ("delete", EventSpec::Bare),
143 ("deployment", EventSpec::Bare),
144 ("deployment_status", EventSpec::Bare),
145 ("discussion", TYPES),
146 ("discussion_comment", TYPES),
147 ("fork", EventSpec::Bare),
148 ("gollum", EventSpec::Bare),
149 ("image_version", EventSpec::Filters(&["types", "names", "versions"])),
150 ("issue_comment", TYPES),
151 ("issues", TYPES),
152 ("label", TYPES),
153 ("merge_group", EventSpec::Filters(&["types", "branches", "branches-ignore"])),
154 ("milestone", TYPES),
155 ("page_build", EventSpec::Bare),
156 ("project", TYPES),
157 ("project_card", TYPES),
158 ("project_column", TYPES),
159 ("public", EventSpec::Bare),
160 ("pull_request", PULL_REQUEST),
161 ("pull_request_comment", TYPES),
162 ("pull_request_review", TYPES),
163 ("pull_request_review_comment", TYPES),
164 ("pull_request_target", PULL_REQUEST),
165 ("push", EventSpec::Filters(&["branches", "branches-ignore", "tags", "tags-ignore", "paths", "paths-ignore"])),
166 ("registry_package", TYPES),
167 ("release", TYPES),
168 ("repository_dispatch", TYPES),
169 ("schedule", EventSpec::Schedule),
170 ("status", EventSpec::Bare),
171 ("watch", TYPES),
172 ("workflow_call", EventSpec::Filters(WORKFLOW_CALL)),
173 ("workflow_dispatch", EventSpec::Filters(WORKFLOW_DISPATCH)),
174 ("workflow_run", EventSpec::Filters(&["types", "branches", "branches-ignore", "workflows"])),
175];
176
177/// What `event` takes, if GitHub has it.
178pub fn event(event: &str) -> Option<EventSpec> {
179 EVENTS.iter().find(|(name, _)| *name == event).map(|(_, spec)| *spec)
180}
181
182/// Whether `id` may name a job or a step: a letter or `_` first, then
183/// letters, digits, `-` and `_`, under 100 characters, and not starting
184/// with `__`, which GitHub keeps for itself.
185pub fn valid_id(id: &str) -> bool {
186 let mut chars = id.chars();
187 let Some(first) = chars.next() else { return false };
188 (first.is_ascii_alphabetic() || first == '_')
189 && chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
190 && id.len() < 100
191 && !id.starts_with("__")
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use serde_json::Value;
198 use std::collections::BTreeSet;
199
200 fn schema() -> Value {
201 serde_json::from_str(include_str!("../schema/workflow-v1.0.json")).unwrap()
202 }
203
204 /// The keys of the mapping `name` in GitHub's schema.
205 fn keys(schema: &Value, name: &str) -> BTreeSet<String> {
206 let definition = &schema["definitions"][name];
207 let properties = definition["mapping"]["properties"].as_object().unwrap_or_else(|| panic!("{name} is no mapping"));
208 properties.keys().cloned().collect()
209 }
210
211 fn set(list: &[&str]) -> BTreeSet<String> {
212 list.iter().map(|key| (*key).to_owned()).collect()
213 }
214
215 #[test]
216 fn the_lists_are_githubs() {
217 let schema = schema();
218 for (list, name) in [
219 (WORKFLOW, "workflow-root-strict"),
220 (JOB, "job-factory"),
221 (CALLER_JOB, "workflow-job"),
222 (RUN_STEP, "run-step"),
223 (ACTION_STEP, "regular-step"),
224 (STRATEGY, "strategy"),
225 (RUNS_ON, "runs-on-mapping"),
226 (CONTAINER, "container-mapping"),
227 (SERVICE, "service-container-mapping"),
228 (CREDENTIALS, "container-registry-credentials"),
229 (ENVIRONMENT, "job-environment-mapping"),
230 (CONCURRENCY, "concurrency-mapping"),
231 (DEFAULTS, "workflow-defaults"),
232 (DEFAULTS, "job-defaults"),
233 (DEFAULTS_RUN, "workflow-defaults-run"),
234 (DEFAULTS_RUN, "job-defaults-run"),
235 (PERMISSIONS, "permissions-mapping"),
236 (CRON, "cron-mapping"),
237 (WORKFLOW_DISPATCH, "workflow-dispatch-mapping"),
238 (DISPATCH_INPUT, "workflow-dispatch-input"),
239 (WORKFLOW_CALL, "workflow-call-mapping"),
240 (CALL_INPUT, "workflow-call-input-definition"),
241 (CALL_OUTPUT, "workflow-call-output-definition"),
242 (CALL_SECRET, "workflow-call-secret-mapping-definition"),
243 ] {
244 assert_eq!(set(list), keys(&schema, name), "{name}");
245 }
246 // The background steps' own keys.
247 let mut background = BTreeSet::from(["background".to_owned()]);
248 for name in ["wait-step", "wait-all-step", "cancel-step", "parallel-step"] {
249 background.extend(keys(&schema, name).into_iter().filter(|key| !["name", "id", "continue-on-error"].contains(&key.as_str())));
250 }
251 assert_eq!(set(BACKGROUND_STEPS), background);
252 // Every step kind GitHub has is one of these.
253 let kinds: Vec<&str> = schema["definitions"]["steps-item"]["one-of"].as_array().unwrap().iter().filter_map(Value::as_str).collect();
254 assert_eq!(kinds, ["run-step", "regular-step", "wait-step", "wait-all-step", "cancel-step", "parallel-step"]);
255 // A job is one or the other.
256 assert_eq!(schema["definitions"]["job"]["one-of"], serde_json::json!(["job-factory", "workflow-job"]));
257 }
258
259 #[test]
260 fn the_events_are_githubs() {
261 let schema = schema();
262 let definitions = &schema["definitions"];
263 let events = definitions["on-mapping-strict"]["mapping"]["properties"].as_object().unwrap();
264 assert_eq!(events.keys().cloned().collect::<BTreeSet<_>>(), EVENTS.iter().map(|(name, _)| (*name).to_owned()).collect());
265 for (name, kind) in events {
266 let kind = kind.as_str().or_else(|| kind["type"].as_str()).unwrap();
267 let definition = &definitions[kind];
268 let expected = if definition.get("null").is_some() {
269 EventSpec::Bare
270 } else if definition["sequence"]["item-type"] == "cron-mapping" {
271 EventSpec::Schedule
272 } else {
273 let options: Vec<&str> = definition["one-of"].as_array().unwrap().iter().filter_map(Value::as_str).collect();
274 assert_eq!(options.len(), 2, "{name}");
275 assert_eq!(options[0], "null", "{name}");
276 let ours = match event(name) {
277 Some(EventSpec::Filters(keys)) => set(keys),
278 other => panic!("{name}: {other:?}"),
279 };
280 assert_eq!(ours, keys(&schema, options[1]), "{name}");
281 continue;
282 };
283 assert_eq!(event(name), Some(expected), "{name}");
284 }
285 }
286
287 #[test]
288 fn ids_follow_githubs_rule() {
289 for id in ["build", "_", "_a", "a__", "build-2", "Test_all"] {
290 assert!(valid_id(id), "{id}");
291 }
292 for id in ["", "my.job", "my%job", ".job", "0job", " job", "-job", "__", "__a", &"a".repeat(100)] {
293 assert!(!valid_id(id), "{id}");
294 }
295 }
296}

This file's history is long; its oldest lines are credited to the oldest commit read.