Skip to content

Commit

Merge branch 'worktree-agent-a641fa0e2e4fffccb' into claude/cool-newton-7nm8r9

# Conflicts: # apps/docs/src/content/docs/guides/actions.md # crates/actions/src/workflow.rs

Claudeagentcommitted Parentsc28e1ac64dc0fcBrowse files
114 files+5241−230/114 viewed
+16−0
229229 macOS and Windows, on x64 and arm64.
230230 - **Status.** Not scheduled.
231231
232+### No background steps
233+
234+GitHub's background steps (`background: true` on a step, and `wait`,
235+`wait-all`, `cancel` and `parallel` steps) are refused: a workflow with one
236+does not run on g1t. A schedule's `timezone`, `concurrency.queue`,
237+`cancel-timeout-minutes` and `snapshot` are read but not applied: schedules
238+run in UTC, a concurrency group keeps one run waiting, a cancelled job stops
239+in its usual time, and no runner image is made from a job. The workflow's
240+page says so.
241+
242+- **Why.** They are newer than g1t's runner, which runs a job's steps one
243+ after another.
244+- **Instead.** Start a long-running process with `&` in a `run:` step and
245+ stop it in a later one; write a schedule's `cron` in UTC.
246+- **Status.** Not scheduled.
247+
232248 ### No `gh` command in jobs
233249
234250 The runner does not include the `gh` command, and pointing it at g1t
+40−0
9595 [actions built on the toolkit](#actions-built-on-the-toolkit).
9696 - **`on: delete`.** Deleting a branch or tag starts no workflows yet; the
9797 workflow's page says so.
98+- **Background steps.** A step with `background: true`, or a `wait`,
99+ `wait-all`, `cancel` or `parallel` step, is refused: g1t does not run
100+ them yet.
101+- **`timezone` on a schedule, `concurrency.queue`, `cancel-timeout-minutes`
102+ and `snapshot`.** The workflow runs without them, and its page says so:
103+ a schedule runs in UTC, a group keeps one run waiting, a cancelled job
104+ stops in its usual time, and no runner image is made.
98105
99106 Why each of these is missing, and what to use instead, is on
100107 [What g1t can't do yet](/about/limitations/#actions-and-runners).
117124 pending, so a pull request whose merge needs it waits for a push that
118125 runs it.
119126
127+## How g1t checks a workflow
128+
129+g1t reads a workflow as GitHub does, from GitHub's own schema for workflow
130+files, so a workflow GitHub refuses is refused on g1t too, and one GitHub
131+runs reads on g1t. When a workflow can't be read, its page says what is
132+wrong, with the job and step, and no run starts from it.
133+
134+| You write | g1t says |
135+| --- | --- |
136+| A key GitHub doesn't have, at any level, such as `runs_on:` or `with:` on a `run:` step | It is unexpected, and lists the keys that place can have. |
137+| A job without `runs-on` | The job has no `runs-on`. A job that calls a reusable workflow with `uses:` has no `runs-on` or `steps`. |
138+| A job id that doesn't start with a letter or `_`, has characters other than letters, digits, `-` and `_`, or starts with `__` | It is not a job id GitHub accepts. Step ids follow the same rule. Two jobs, or two steps of a job, whose ids differ only in case are the same id. |
139+| An event GitHub doesn't have, or a filter the event doesn't take, such as `types` on `push` | GitHub has no such event, or the event doesn't take that key. `branches` and `branches-ignore` (and `tags`, `paths`) can't both be on one event. |
140+| A YAML merge key (`<<: *defaults`) | GitHub doesn't read merge keys. Anchors and aliases (`&steps` and `*steps`) work, as on GitHub, for a whole value. |
141+| A `permissions` name GitHub's token doesn't have, such as `metadata` | It is unexpected. A name GitHub has that g1t has nothing behind yet reads, and grants nothing. |
142+
143+### g1t's own settings
144+
145+A `g1t:` mapping can go at the top of a workflow, on a job and on a step.
146+GitHub refuses a workflow with `g1t:` in it, so use it only in
147+`.g1t/workflows`. It follows two rules:
148+
149+- It only adds to a workflow. It never changes what a GitHub key means.
150+- Each setting in it has a GitHub equivalent, written here, so that a
151+ workflow can be moved back to GitHub without it.
152+
153+| Setting | Where | What it does | On GitHub |
154+| --- | --- | --- | --- |
155+| `deployment: false` | A job | The job reads its `environment:` values without the run recording a deployment. | `environment: { name: ..., deployment: false }` |
156+
157+g1t keeps any other key under `g1t:` as written, and does nothing with it
158+yet.
159+
120160 ## Schedules
121161
122162 A workflow with `on: schedule` runs on the default branch's latest commit,
+21−0
1+MIT License
2+
3+Copyright GitHub
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
11+
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
14+
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
+12−0
1+# GitHub's workflow schema
2+
3+`workflow-v1.0.json` is GitHub's schema for workflow files, copied unchanged from
4+[actions/languageservices](https://github.com/actions/languageservices), file
5+`workflow-parser/src/workflow-v1.0.json`, at commit
6+`4043eda158e16579cc5fb1b0b07a4bce2a76f0b5` (5 August 2026). It is MIT licensed;
7+`LICENSE` is that repository's licence, copied from the same commit.
8+
9+g1t does not read it at run time. `src/syntax.rs` lists the keys each part of a
10+workflow may have, and its tests check those lists against this file, so they
11+cannot drift from GitHub's. To update it, copy the file again, write the commit
12+here, and run `cargo test -p g1t-actions`.
+2797−0
1+{
2+ "version": "workflow-v1.0",
3+ "definitions": {
4+ "workflow-root": {
5+ "description": "A workflow file.",
6+ "mapping": {
7+ "properties": {
8+ "on": "on",
9+ "name": "workflow-name",
10+ "description": "workflow-description",
11+ "run-name": "run-name",
12+ "defaults": "workflow-defaults",
13+ "env": "workflow-env",
14+ "permissions": "permissions",
15+ "concurrency": "workflow-concurrency",
16+ "jobs": {
17+ "type": "jobs",
18+ "required": true
19+ }
20+ }
21+ }
22+ },
23+ "workflow-root-strict": {
24+ "description": "Workflow file with strict validation",
25+ "mapping": {
26+ "properties": {
27+ "on": {
28+ "type": "on-strict",
29+ "required": true
30+ },
31+ "name": "workflow-name",
32+ "description": "workflow-description",
33+ "run-name": "run-name",
34+ "defaults": "workflow-defaults",
35+ "env": "workflow-env",
36+ "permissions": "permissions",
37+ "concurrency": "workflow-concurrency",
38+ "jobs": {
39+ "type": "jobs",
40+ "required": true
41+ }
42+ }
43+ }
44+ },
45+ "workflow-name": {
46+ "description": "The name of the workflow that GitHub displays on your repository's 'Actions' tab.\n\n[Documentation](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#name)",
47+ "string": {}
48+ },
49+ "workflow-description": {
50+ "description": "A description for your workflow or reusable workflow",
51+ "string": {}
52+ },
53+ "run-name": {
54+ "context": [
55+ "github",
56+ "inputs",
57+ "vars"
58+ ],
59+ "string": {},
60+ "description": "The name for workflow runs generated from the workflow. GitHub displays the workflow run name in the list of workflow runs on your repository's 'Actions' tab.\n\n[Documentation](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#run-name)"
61+ },
62+ "on": {
63+ "description": "The GitHub event that triggers the workflow. Events can be a single string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. View a full list of [events that trigger workflows](https://docs.github.com/actions/using-workflows/events-that-trigger-workflows).\n\n[Documentation](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#on)",
64+ "one-of": [
65+ "string",
66+ "sequence",
67+ "on-mapping"
68+ ]
69+ },
70+ "on-mapping": {
71+ "mapping": {
72+ "properties": {
73+ "workflow_call": "workflow-call"
74+ },
75+ "loose-key-type": "non-empty-string",
76+ "loose-value-type": "any"
77+ }
78+ },
79+ "on-strict": {
80+ "description": "The GitHub event that triggers the workflow. Events can be a single string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. View a full list of [events that trigger workflows](https://docs.github.com/actions/using-workflows/events-that-trigger-workflows).\n\n[Documentation](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#on)",
81+ "one-of": [
82+ "on-string-strict",
83+ "on-sequence-strict",
84+ "on-mapping-strict"
85+ ]
86+ },
87+ "on-mapping-strict": {
88+ "description": "The GitHub event that triggers the workflow. Events can be a single string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. View a full list of [events that trigger workflows](https://docs.github.com/actions/using-workflows/events-that-trigger-workflows).\n\n[Documentation](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#on)",
89+ "mapping": {
90+ "properties": {
91+ "branch_protection_rule": "branch-protection-rule",
92+ "check_run": "check-run",
93+ "check_suite": "check-suite",
94+ "create": "create",
95+ "delete": "delete",
96+ "deployment": "deployment",
97+ "deployment_status": "deployment-status",
98+ "discussion": "discussion",
99+ "discussion_comment": "discussion-comment",
100+ "fork": "fork",
101+ "gollum": "gollum",
102+ "image_version": "image-version",
103+ "issue_comment": "issue-comment",
104+ "issues": "issues",
105+ "label": "label",
106+ "merge_group": "merge-group",
107+ "milestone": "milestone",
108+ "page_build": "page-build",
109+ "project": "project",
110+ "project_card": "project-card",
111+ "project_column": "project-column",
112+ "public": "public",
113+ "pull_request": "pull-request",
114+ "pull_request_comment": "pull-request-comment",
115+ "pull_request_review": "pull-request-review",
116+ "pull_request_review_comment": "pull-request-review-comment",
117+ "pull_request_target": "pull-request-target",
118+ "push": "push",
119+ "registry_package": "registry-package",
120+ "release": "release",
121+ "repository_dispatch": "repository-dispatch",
122+ "schedule": "schedule",
123+ "status": "status",
124+ "watch": "watch",
125+ "workflow_call": "workflow-call",
126+ "workflow_dispatch": "workflow-dispatch",
127+ "workflow_run": "workflow-run"
128+ }
129+ }
130+ },
131+ "on-string-strict": {
132+ "one-of": [
133+ "branch-protection-rule-string",
134+ "check-run-string",
135+ "check-suite-string",
136+ "create-string",
137+ "delete-string",
138+ "deployment-string",
139+ "deployment-status-string",
140+ "discussion-string",
141+ "discussion-comment-string",
142+ "fork-string",
143+ "gollum-string",
144+ "image-version-string",
145+ "issue-comment-string",
146+ "issues-string",
147+ "label-string",
148+ "merge-group-string",
149+ "milestone-string",
150+ "page-build-string",
151+ "project-string",
152+ "project-card-string",
153+ "project-column-string",
154+ "public-string",
155+ "pull-request-string",
156+ "pull-request-comment-string",
157+ "pull-request-review-string",
158+ "pull-request-review-comment-string",
159+ "pull-request-target-string",
160+ "push-string",
161+ "registry-package-string",
162+ "release-string",
163+ "repository-dispatch-string",
164+ "schedule-string",
165+ "status-string",
166+ "watch-string",
167+ "workflow-call-string",
168+ "workflow-dispatch-string",
169+ "workflow-run-string"
170+ ]
171+ },
172+ "on-sequence-strict": {
173+ "sequence": {
174+ "item-type": "on-string-strict"
175+ }
176+ },
177+ "branch-protection-rule-string": {
178+ "description": "Runs your workflow when branch protection rules in the workflow repository are changed.",
179+ "string": {
180+ "constant": "branch_protection_rule"
181+ }
182+ },
183+ "branch-protection-rule": {
184+ "description": "Runs your workflow when branch protection rules in the workflow repository are changed.",
185+ "one-of": [
186+ "null",
187+ "branch-protection-rule-mapping"
188+ ]
189+ },
190+ "branch-protection-rule-mapping": {
191+ "mapping": {
192+ "properties": {
193+ "types": "branch-protection-rule-activity"
194+ }
195+ }
196+ },
197+ "branch-protection-rule-activity": {
198+ "description": "The types of branch protection rule activity that trigger the workflow. Supported activity types: `created`, `edited`, `deleted`.",
199+ "one-of": [
200+ "branch-protection-rule-activity-type",
201+ "branch-protection-rule-activity-types"
202+ ]
203+ },
204+ "branch-protection-rule-activity-types": {
205+ "sequence": {
206+ "item-type": "branch-protection-rule-activity-type"
207+ }
208+ },
209+ "branch-protection-rule-activity-type": {
210+ "allowed-values": [
211+ "created",
212+ "edited",
213+ "deleted"
214+ ]
215+ },
216+ "check-run-string": {
217+ "description": "Runs your workflow when activity related to a check run occurs. A check run is an individual test that is part of a check suite.",
218+ "string": {
219+ "constant": "check_run"
220+ }
221+ },
222+ "check-run": {
223+ "description": "Runs your workflow when activity related to a check run occurs. A check run is an individual test that is part of a check suite.",
224+ "one-of": [
225+ "null",
226+ "check-run-mapping"
227+ ]
228+ },
229+ "check-run-mapping": {
230+ "mapping": {
231+ "properties": {
232+ "types": "check-run-activity"
233+ }
234+ }
235+ },
236+ "check-run-activity": {
237+ "description": "The types of check run activity that trigger the workflow. Supported activity types: `created`, `rerequested`, `completed`, `requested_action`.",
238+ "one-of": [
239+ "check-run-activity-type",
240+ "check-run-activity-types"
241+ ]
242+ },
243+ "check-run-activity-types": {
244+ "sequence": {
245+ "item-type": "check-run-activity-type"
246+ }
247+ },
248+ "check-run-activity-type": {
249+ "allowed-values": [
250+ "completed",
251+ "created",
252+ "rerequested",
253+ "requested_action"
254+ ]
255+ },
256+ "check-suite-string": {
257+ "description": "Runs your workflow when check suite activity occurs. A check suite is a collection of the check runs created for a specific commit. Check suites summarize the status and conclusion of the check runs that are in the suite.",
258+ "string": {
259+ "constant": "check_suite"
260+ }
261+ },
262+ "check-suite": {
263+ "description": "Runs your workflow when check suite activity occurs. A check suite is a collection of the check runs created for a specific commit. Check suites summarize the status and conclusion of the check runs that are in the suite.",
264+ "one-of": [
265+ "null",
266+ "check-suite-mapping"
267+ ]
268+ },
269+ "check-suite-mapping": {
270+ "mapping": {
271+ "properties": {
272+ "types": "check-suite-activity"
273+ }
274+ }
275+ },
276+ "check-suite-activity": {
277+ "description": "The types of check suite activity that trigger the workflow. Supported activity types: `completed`.",
278+ "one-of": [
279+ "check-suite-activity-type",
280+ "check-suite-activity-types"
281+ ]
282+ },
283+ "check-suite-activity-types": {
284+ "sequence": {
285+ "item-type": "check-suite-activity-type"
286+ }
287+ },
288+ "check-suite-activity-type": {
289+ "allowed-values": [
290+ "completed"
291+ ]
292+ },
293+ "create-string": {
294+ "description": "Runs your workflow when someone creates a Git reference (Git branch or tag) in the workflow's repository.",
295+ "string": {
296+ "constant": "create"
297+ }
298+ },
299+ "create": {
300+ "description": "Runs your workflow when someone creates a Git reference (Git branch or tag) in the workflow's repository.",
301+ "null": {}
302+ },
303+ "delete-string": {
304+ "description": "Runs your workflow when someone deletes a Git reference (Git branch or tag) in the workflow's repository.",
305+ "string": {
306+ "constant": "delete"
307+ }
308+ },
309+ "delete": {
310+ "description": "Runs your workflow when someone deletes a Git reference (Git branch or tag) in the workflow's repository.",
311+ "null": {}
312+ },
313+ "deployment-string": {
314+ "description": "Runs your workflow when someone creates a deployment in the workflow's repository. Deployments created with a commit SHA may not have a Git ref.",
315+ "string": {
316+ "constant": "deployment"
317+ }
318+ },
319+ "deployment": {
320+ "description": "Runs your workflow when someone creates a deployment in the workflow's repository. Deployments created with a commit SHA may not have a Git ref.",
321+ "null": {}
322+ },
323+ "deployment-status-string": {
324+ "description": "Runs your workflow when a third party provides a deployment status. Deployments created with a commit SHA may not have a Git ref.",
325+ "string": {
326+ "constant": "deployment_status"
327+ }
328+ },
329+ "deployment-status": {
330+ "description": "Runs your workflow when a third party provides a deployment status. Deployments created with a commit SHA may not have a Git ref.",
331+ "null": {}
332+ },
333+ "discussion-string": {
334+ "description": "Runs your workflow when a discussion in the workflow's repository is created or modified. For activity related to comments on a discussion, use the `discussion_comment` event.",
335+ "string": {
336+ "constant": "discussion"
337+ }
338+ },
339+ "discussion": {
340+ "description": "Runs your workflow when a discussion in the workflow's repository is created or modified. For activity related to comments on a discussion, use the `discussion_comment` event.",
341+ "one-of": [
342+ "null",
343+ "discussion-mapping"
344+ ]
345+ },
346+ "discussion-mapping": {
347+ "mapping": {
348+ "properties": {
349+ "types": "discussion-activity"
350+ }
351+ }
352+ },
353+ "discussion-activity": {
354+ "description": "The types of discussion activity that trigger the workflow. Supported activity types: `created`, `edited`, `deleted`, `transferred`, `pinned`, `unpinned`, `labeled`, `unlabeled`, `locked`, `unlocked`, `category_changed`, `answered`, `unanswered`.",
355+ "one-of": [
356+ "discussion-activity-type",
357+ "discussion-activity-types"
358+ ]
359+ },
360+ "discussion-activity-types": {
361+ "sequence": {
362+ "item-type": "discussion-activity-type"
363+ }
364+ },
365+ "discussion-activity-type": {
366+ "allowed-values": [
367+ "created",
368+ "edited",
369+ "deleted",
370+ "transferred",
371+ "pinned",
372+ "unpinned",
373+ "labeled",
374+ "unlabeled",
375+ "locked",
376+ "unlocked",
377+ "category_changed",
378+ "answered",
379+ "unanswered"
380+ ]
381+ },
382+ "discussion-comment-string": {
383+ "description": "Runs your workflow when a comment on a discussion in the workflow's repository is created or modified. For activity related to a discussion as opposed to comments on the discussion, use the `discussion` event.",
384+ "string": {
385+ "constant": "discussion_comment"
386+ }
387+ },
388+ "discussion-comment": {
389+ "description": "Runs your workflow when a comment on a discussion in the workflow's repository is created or modified. For activity related to a discussion as opposed to comments on the discussion, use the `discussion` event.",
390+ "one-of": [
391+ "null",
392+ "discussion-comment-mapping"
393+ ]
394+ },
395+ "discussion-comment-mapping": {
396+ "mapping": {
397+ "properties": {
398+ "types": "discussion-comment-activity"
399+ }
400+ }
401+ },
402+ "discussion-comment-activity": {
403+ "description": "The types of discussion comment activity that trigger the workflow. Supported activity types: `created`, `edited`, `deleted`.",
404+ "one-of": [
405+ "discussion-comment-activity-type",
406+ "discussion-comment-activity-types"
407+ ]
408+ },
409+ "discussion-comment-activity-types": {
410+ "sequence": {
411+ "item-type": "discussion-comment-activity-type"
412+ }
413+ },
414+ "discussion-comment-activity-type": {
415+ "allowed-values": [
416+ "created",
417+ "edited",
418+ "deleted"
419+ ]
420+ },
421+ "fork-string": {
422+ "description": "Runs your workflow when someone forks a repository.",
423+ "string": {
424+ "constant": "fork"
425+ }
426+ },
427+ "fork": {
428+ "description": "Runs your workflow when someone forks a repository.",
429+ "null": {}
430+ },
431+ "gollum-string": {
432+ "description": "Runs your workflow when someone creates or updates a Wiki page.",
433+ "string": {
434+ "constant": "gollum"
435+ }
436+ },
437+ "gollum": {
438+ "description": "Runs your workflow when someone creates or updates a Wiki page.",
439+ "null": {}
440+ },
441+ "image-version-string": {
442+ "description": "Runs your workflow when an image version is created or changes state.",
443+ "string": {
444+ "constant": "image_version"
445+ }
446+ },
447+ "image-version": {
448+ "description": "Runs your workflow when an image version is created or changes state.",
449+ "one-of": [
450+ "null",
451+ "image-version-mapping"
452+ ]
453+ },
454+ "image-version-mapping": {
455+ "mapping": {
456+ "properties": {
457+ "types": "image-version-activity",
458+ "names": "event-names",
459+ "versions": "event-versions"
460+ }
461+ }
462+ },
463+ "image-version-activity": {
464+ "description": "The types of image version activity that trigger the workflow. Supported activity types: `created`, `ready`, `deleted`.",
465+ "one-of": [
466+ "image-version-activity-type",
467+ "image-version-activity-types"
468+ ]
469+ },
470+ "image-version-activity-types": {
471+ "sequence": {
472+ "item-type": "image-version-activity-type"
473+ }
474+ },
475+ "image-version-activity-type": {
476+ "allowed-values": [
477+ "created",
478+ "ready",
479+ "deleted"
480+ ]
481+ },
482+ "issue-comment-string": {
483+ "description": "Runs your workflow when an issue or pull request comment is created, edited, or deleted.",
484+ "string": {
485+ "constant": "issue_comment"
486+ }
487+ },
488+ "issue-comment": {
489+ "description": "Runs your workflow when an issue or pull request comment is created, edited, or deleted.",
490+ "one-of": [
491+ "null",
492+ "issue-comment-mapping"
493+ ]
494+ },
495+ "issue-comment-mapping": {
496+ "mapping": {
497+ "properties": {
498+ "types": "issue-comment-activity"
499+ }
500+ }
501+ },
502+ "issue-comment-activity": {
503+ "description": "The types of issue comment activity that trigger the workflow. Supported activity types: `created`, `edited`, `deleted`.",
504+ "one-of": [
505+ "issue-comment-activity-type",
506+ "issue-comment-activity-types"
507+ ]
508+ },
509+ "issue-comment-activity-types": {
510+ "sequence": {
511+ "item-type": "issue-comment-activity-type"
512+ }
513+ },
514+ "issue-comment-activity-type": {
515+ "allowed-values": [
516+ "created",
517+ "edited",
518+ "deleted"
519+ ]
520+ },
521+ "issues-string": {
522+ "description": "Runs your workflow when an issue in the workflow's repository is created or modified. For activity related to comments in an issue, use the `issue_comment` event.",
523+ "string": {
524+ "constant": "issues"
525+ }
526+ },
527+ "issues": {
528+ "description": "Runs your workflow when an issue in the workflow's repository is created or modified. For activity related to comments in an issue, use the `issue_comment` event.",
529+ "one-of": [
530+ "null",
531+ "issues-mapping"
532+ ]
533+ },
534+ "issues-mapping": {
535+ "mapping": {
536+ "properties": {
537+ "types": "issues-activity"
538+ }
539+ }
540+ },
541+ "issues-activity": {
542+ "description": "The types of issue activity that trigger the workflow. Supported activity types: `opened`, `edited`, `deleted`, `transferred`, `pinned`, `unpinned`, `closed`, `reopened`, `assigned`, `unassigned`, `labeled`, `unlabeled`, `locked`, `unlocked`, `milestoned`, `demilestoned`, `field_added`, `field_removed`, `typed`, `untyped`.",
543+ "one-of": [
544+ "issues-activity-type",
545+ "issues-activity-types"
546+ ]
547+ },
548+ "issues-activity-types": {
549+ "sequence": {
550+ "item-type": "issues-activity-type"
551+ }
552+ },
553+ "issues-activity-type": {
554+ "allowed-values": [
555+ "opened",
556+ "edited",
557+ "deleted",
558+ "transferred",
559+ "pinned",
560+ "unpinned",
561+ "closed",
562+ "reopened",
563+ "assigned",
564+ "unassigned",
565+ "labeled",
566+ "unlabeled",
567+ "locked",
568+ "unlocked",
569+ "milestoned",
570+ "demilestoned",
571+ "field_added",
572+ "field_removed",
573+ "typed",
574+ "untyped"
575+ ]
576+ },
577+ "label-string": {
578+ "description": "Runs your workflow when a label in your workflow's repository is created or modified.",
579+ "string": {
580+ "constant": "label"
581+ }
582+ },
583+ "label": {
584+ "description": "Runs your workflow when a label in your workflow's repository is created or modified.",
585+ "one-of": [
586+ "null",
587+ "label-mapping"
588+ ]
589+ },
590+ "label-mapping": {
591+ "mapping": {
592+ "properties": {
593+ "types": "label-activity"
594+ }
595+ }
596+ },
597+ "label-activity": {
598+ "description": "The types of label activity that trigger the workflow. Supported activity types: `created`, `edited`, `deleted`.",
599+ "one-of": [
600+ "label-activity-type",
601+ "label-activity-types"
602+ ]
603+ },
604+ "label-activity-types": {
605+ "sequence": {
606+ "item-type": "label-activity-type"
607+ }
608+ },
609+ "label-activity-type": {
610+ "allowed-values": [
611+ "created",
612+ "edited",
613+ "deleted"
614+ ]
615+ },
616+ "merge-group-string": {
617+ "description": "Runs your workflow when a pull request is added to a merge queue, which adds the pull request to a merge group.",
618+ "string": {
619+ "constant": "merge_group"
620+ }
621+ },
622+ "merge-group": {
623+ "description": "Runs your workflow when a pull request is added to a merge queue, which adds the pull request to a merge group.",
624+ "one-of": [
625+ "null",
626+ "merge-group-mapping"
627+ ]
628+ },
629+ "merge-group-mapping": {
630+ "mapping": {
631+ "properties": {
632+ "types": "merge-group-activity",
633+ "branches": "event-branches",
634+ "branches-ignore": "event-branches-ignore"
635+ }
636+ }
637+ },
638+ "merge-group-activity": {
639+ "description": "The types of merge group activity that trigger the workflow. Supported activity types: `checks_requested`.",
640+ "one-of": [
641+ "merge-group-activity-type",
642+ "merge-group-activity-types"
643+ ]
644+ },
645+ "merge-group-activity-types": {
646+ "sequence": {
647+ "item-type": "merge-group-activity-type"
648+ }
649+ },
650+ "merge-group-activity-type": {
651+ "allowed-values": [
652+ "checks_requested"
653+ ]
654+ },
655+ "milestone-string": {
656+ "description": "Runs your workflow when a milestone in the workflow's repository is created or modified.",
657+ "string": {
658+ "constant": "milestone"
659+ }
660+ },
661+ "milestone": {
662+ "description": "Runs your workflow when a milestone in the workflow's repository is created or modified.",
663+ "one-of": [
664+ "null",
665+ "milestone-mapping"
666+ ]
667+ },
668+ "milestone-mapping": {
669+ "mapping": {
670+ "properties": {
671+ "types": "milestone-activity"
672+ }
673+ }
674+ },
675+ "milestone-activity": {
676+ "description": "The types of milestone activity that trigger the workflow. Supported activity types: `created`, `closed`, `opened`, `edited`, `deleted`.",
677+ "one-of": [
678+ "milestone-activity-type",
679+ "milestone-activity-types"
680+ ]
681+ },
682+ "milestone-activity-types": {
683+ "sequence": {
684+ "item-type": "milestone-activity-type"
685+ }
686+ },
687+ "milestone-activity-type": {
688+ "allowed-values": [
689+ "created",
690+ "closed",
691+ "opened",
692+ "edited",
693+ "deleted"
694+ ]
695+ },
696+ "page-build-string": {
697+ "description": "Runs your workflow when someone pushes to a branch that is the publishing source for GitHub Pages, if GitHub Pages is enabled for the repository.",
698+ "string": {
699+ "constant": "page_build"
700+ }
701+ },
702+ "page-build": {
703+ "description": "Runs your workflow when someone pushes to a branch that is the publishing source for GitHub Pages, if GitHub Pages is enabled for the repository.",
704+ "null": {}
705+ },
706+ "project-string": {
707+ "description": "Runs your workflow when a project board is created or modified. For activity related to cards or columns in a project board, use the `project_card` or `project_column` events instead.",
708+ "string": {
709+ "constant": "project"
710+ }
711+ },
712+ "project": {
713+ "description": "Runs your workflow when a project board is created or modified. For activity related to cards or columns in a project board, use the `project_card` or `project_column` events instead.",
714+ "one-of": [
715+ "null",
716+ "project-mapping"
717+ ]
718+ },
719+ "project-mapping": {
720+ "mapping": {
721+ "properties": {
722+ "types": "project-activity"
723+ }
724+ }
725+ },
726+ "project-activity": {
727+ "description": "The types of project activity that trigger the workflow. Supported activity types: `created`, `closed`, `reopened`, `edited`, `deleted`.",
728+ "one-of": [
729+ "project-activity-type",
730+ "project-activity-types"
731+ ]
732+ },
733+ "project-activity-types": {
734+ "sequence": {
735+ "item-type": "project-activity-type"
736+ }
737+ },
738+ "project-activity-type": {
739+ "allowed-values": [
740+ "created",
741+ "closed",
742+ "reopened",
743+ "edited",
744+ "deleted"
745+ ]
746+ },
747+ "project-card-string": {
748+ "description": "Runs your workflow when a card on a project board is created or modified. For activity related to project boards or columns in a project board, use the `project` or `project_column` event instead.",
749+ "string": {
750+ "constant": "project_card"
751+ }
752+ },
753+ "project-card": {
754+ "description": "Runs your workflow when a card on a project board is created or modified. For activity related to project boards or columns in a project board, use the `project` or `project_column` event instead.",
755+ "one-of": [
756+ "null",
757+ "project-card-mapping"
758+ ]
759+ },
760+ "project-card-mapping": {
761+ "mapping": {
762+ "properties": {
763+ "types": "project-card-activity"
764+ }
765+ }
766+ },
767+ "project-card-activity": {
768+ "description": "The types of project card activity that trigger the workflow. Supported activity types: `created`, `moved`, `converted`, `edited`, `deleted`.",
769+ "one-of": [
770+ "project-card-activity-type",
771+ "project-card-activity-types"
772+ ]
773+ },
774+ "project-card-activity-types": {
775+ "sequence": {
776+ "item-type": "project-card-activity-type"
777+ }
778+ },
779+ "project-card-activity-type": {
780+ "allowed-values": [
781+ "created",
782+ "moved",
783+ "converted",
784+ "edited",
785+ "deleted"
786+ ]
787+ },
788+ "project-column-string": {
789+ "description": "Runs your workflow when a column on a project board is created or modified. For activity related to project boards or cards in a project board, use the `project` or `project_card` event instead.",
790+ "string": {
791+ "constant": "project_column"
792+ }
793+ },
794+ "project-column": {
795+ "description": "Runs your workflow when a column on a project board is created or modified. For activity related to project boards or cards in a project board, use the `project` or `project_card` event instead.",
796+ "one-of": [
797+ "null",
798+ "project-column-mapping"
799+ ]
800+ },
801+ "project-column-mapping": {
802+ "mapping": {
803+ "properties": {
804+ "types": "project-column-activity"
805+ }
806+ }
807+ },
808+ "project-column-activity": {
809+ "description": "The types of project column activity that trigger the workflow. Supported activity types: `created`, `updated`, `moved`, `deleted`.",
810+ "one-of": [
811+ "project-column-activity-type",
812+ "project-column-activity-types"
813+ ]
814+ },
815+ "project-column-activity-types": {
816+ "sequence": {
817+ "item-type": "project-column-activity-type"
818+ }
819+ },
820+ "project-column-activity-type": {
821+ "allowed-values": [
822+ "created",
823+ "updated",
824+ "moved",
825+ "deleted"
826+ ]
827+ },
828+ "public-string": {
829+ "description": "Runs your workflow when your workflow's repository changes from private to public.",
830+ "string": {
831+ "constant": "public"
832+ }
833+ },
834+ "public": {
835+ "description": "Runs your workflow when your workflow's repository changes from private to public.",
836+ "null": {}
837+ },
838+ "pull-request-string": {
839+ "description": "Runs your workflow when activity on a pull request in the workflow's repository occurs. If no activity types are specified, the workflow runs when a pull request is opened, reopened, or when the head branch of the pull request is updated.",
840+ "string": {
841+ "constant": "pull_request"
842+ }
843+ },
844+ "pull-request": {
845+ "description": "Runs your workflow when activity on a pull request in the workflow's repository occurs. If no activity types are specified, the workflow runs when a pull request is opened, reopened, or when the head branch of the pull request is updated.",
846+ "one-of": [
847+ "null",
848+ "pull-request-mapping"
849+ ]
850+ },
851+ "pull-request-mapping": {
852+ "mapping": {
853+ "properties": {
854+ "types": "pull-request-activity",
855+ "branches": "event-branches",
856+ "branches-ignore": "event-branches-ignore",
857+ "paths": "event-paths",
858+ "paths-ignore": "event-paths-ignore"
859+ }
860+ }
861+ },
862+ "pull-request-activity": {
863+ "description": "The types of pull request activity that trigger the workflow. Supported activity types: `assigned`, `unassigned`, `labeled`, `unlabeled`, `opened`, `edited`, `closed`, `reopened`, `synchronize`, `converted_to_draft`, `locked`, `unlocked`, `enqueued`, `dequeued`, `milestoned`, `demilestoned`, `ready_for_review`, `review_requested`, `review_request_removed`, `auto_merge_enabled`, `auto_merge_disabled`, `stacked`.",
864+ "one-of": [
865+ "pull-request-activity-type",
866+ "pull-request-activity-types"
867+ ]
868+ },
869+ "pull-request-activity-types": {
870+ "sequence": {
871+ "item-type": "pull-request-activity-type"
872+ }
873+ },
874+ "pull-request-activity-type": {
875+ "allowed-values": [
876+ "assigned",
877+ "unassigned",
878+ "labeled",
879+ "unlabeled",
880+ "opened",
881+ "edited",
882+ "closed",
883+ "reopened",
884+ "synchronize",
885+ "converted_to_draft",
886+ "locked",
887+ "unlocked",
888+ "enqueued",
889+ "dequeued",
890+ "milestoned",
891+ "demilestoned",
892+ "ready_for_review",
893+ "review_requested",
894+ "review_request_removed",
895+ "auto_merge_enabled",
896+ "auto_merge_disabled",
897+ "stacked"
898+ ]
899+ },
900+ "pull-request-comment-string": {
901+ "description": "Please use the `issue_comment` event instead.",
902+ "string": {
903+ "constant": "pull_request_comment"
904+ }
905+ },
906+ "pull-request-comment": {
907+ "description": "Please use the `issue_comment` event instead.",
908+ "one-of": [
909+ "null",
910+ "issue-comment-mapping"
911+ ]
912+ },
913+ "pull-request-review-string": {
914+ "description": "Runs your workflow when a pull request review is submitted, edited, or dismissed. A pull request review is a group of pull request review comments in addition to a body comment and a state. For activity related to pull request review comments or pull request comments, use the `pull_request_review_comment` or `issue_comment` events instead.",
915+ "string": {
916+ "constant": "pull_request_review"
917+ }
918+ },
919+ "pull-request-review": {
920+ "description": "Runs your workflow when a pull request review is submitted, edited, or dismissed. A pull request review is a group of pull request review comments in addition to a body comment and a state. For activity related to pull request review comments or pull request comments, use the `pull_request_review_comment` or `issue_comment` events instead.",
921+ "one-of": [
922+ "null",
923+ "pull-request-review-mapping"
924+ ]
925+ },
926+ "pull-request-review-mapping": {
927+ "mapping": {
928+ "properties": {
929+ "types": "pull-request-review-activity"
930+ }
931+ }
932+ },
933+ "pull-request-review-activity": {
934+ "description": "The types of pull request review activity that trigger the workflow. Supported activity types: `submitted`, `edited`, `dismissed`.",
935+ "one-of": [
936+ "pull-request-review-activity-type",
937+ "pull-request-review-activity-types"
938+ ]
939+ },
940+ "pull-request-review-activity-types": {
941+ "sequence": {
942+ "item-type": "pull-request-review-activity-type"
943+ }
944+ },
945+ "pull-request-review-activity-type": {
946+ "allowed-values": [
947+ "submitted",
948+ "edited",
949+ "dismissed"
950+ ]
951+ },
952+ "pull-request-review-comment-string": {
953+ "description": "",
954+ "string": {
955+ "constant": "pull_request_review_comment"
956+ }
957+ },
958+ "pull-request-review-comment": {
959+ "description": "",
960+ "one-of": [
961+ "null",
962+ "pull-request-review-comment-mapping"
963+ ]
964+ },
965+ "pull-request-review-comment-mapping": {
966+ "mapping": {
967+ "properties": {
968+ "types": "pull-request-review-comment-activity"
969+ }
970+ }
971+ },
972+ "pull-request-review-comment-activity": {
973+ "description": "The types of pull request review comment activity that trigger the workflow. Supported activity types: `created`, `edited`, `deleted`.",
974+ "one-of": [
975+ "pull-request-review-comment-activity-type",
976+ "pull-request-review-comment-activity-types"
977+ ]
978+ },
979+ "pull-request-review-comment-activity-types": {
980+ "sequence": {
981+ "item-type": "pull-request-review-comment-activity-type"
982+ }
983+ },
984+ "pull-request-review-comment-activity-type": {
985+ "allowed-values": [
986+ "created",
987+ "edited",
988+ "deleted"
989+ ]
990+ },
991+ "pull-request-target-string": {
992+ "description": "Runs your workflow when activity on a pull request in the workflow's repository occurs. If no activity types are specified, the workflow runs when a pull request is opened, reopened, or when the head branch of the pull request is updated.\n\nThis event runs in the context of the base of the pull request, rather than in the context of the merge commit, as the `pull_request` event does. This prevents execution of unsafe code from the head of the pull request that could alter your repository or steal any secrets you use in your workflow. This event allows your workflow to do things like label or comment on pull requests from forks. Avoid using this event if you need to build or run code from the pull request.",
993+ "string": {
994+ "constant": "pull_request_target"
995+ }
996+ },
997+ "pull-request-target": {
998+ "description": "Runs your workflow when activity on a pull request in the workflow's repository occurs. If no activity types are specified, the workflow runs when a pull request is opened, reopened, or when the head branch of the pull request is updated.\n\nThis event runs in the context of the base of the pull request, rather than in the context of the merge commit, as the `pull_request` event does. This prevents execution of unsafe code from the head of the pull request that could alter your repository or steal any secrets you use in your workflow. This event allows your workflow to do things like label or comment on pull requests from forks. Avoid using this event if you need to build or run code from the pull request.",
999+ "one-of": [
1000+ "null",
1001+ "pull-request-target-mapping"
1002+ ]
1003+ },
1004+ "pull-request-target-mapping": {
1005+ "mapping": {
1006+ "properties": {
1007+ "types": "pull-request-target-activity",
1008+ "branches": "event-branches",
1009+ "branches-ignore": "event-branches-ignore",
1010+ "paths": "event-paths",
1011+ "paths-ignore": "event-paths-ignore"
1012+ }
1013+ }
1014+ },
1015+ "pull-request-target-activity": {
1016+ "description": "The types of pull request activity that trigger the workflow. Supported activity types: `assigned`, `unassigned`, `labeled`, `unlabeled`, `opened`, `edited`, `closed`, `reopened`, `synchronize`, `converted_to_draft`, `locked`, `unlocked`, `enqueued`, `dequeued`, `milestoned`, `demilestoned`, `ready_for_review`, `review_requested`, `review_request_removed`, `auto_merge_enabled`, `auto_merge_disabled`, `stacked`.",
1017+ "one-of": [
1018+ "pull-request-target-activity-type",
1019+ "pull-request-target-activity-types"
1020+ ]
1021+ },
1022+ "pull-request-target-activity-types": {
1023+ "sequence": {
1024+ "item-type": "pull-request-target-activity-type"
1025+ }
1026+ },
1027+ "pull-request-target-activity-type": {
1028+ "allowed-values": [
1029+ "assigned",
1030+ "unassigned",
1031+ "labeled",
1032+ "unlabeled",
1033+ "opened",
1034+ "edited",
1035+ "closed",
1036+ "reopened",
1037+ "synchronize",
1038+ "converted_to_draft",
1039+ "locked",
1040+ "unlocked",
1041+ "enqueued",
1042+ "dequeued",
1043+ "milestoned",
1044+ "demilestoned",
1045+ "ready_for_review",
1046+ "review_requested",
1047+ "review_request_removed",
1048+ "auto_merge_enabled",
1049+ "auto_merge_disabled",
1050+ "stacked"
1051+ ]
1052+ },
1053+ "push-string": {
1054+ "description": "Runs your workflow when you push a commit or tag.",
1055+ "string": {
1056+ "constant": "push"
1057+ }
1058+ },
1059+ "push": {
1060+ "description": "Runs your workflow when you push a commit or tag.",
1061+ "one-of": [
1062+ "null",
1063+ "push-mapping"
1064+ ]
1065+ },
1066+ "push-mapping": {
1067+ "mapping": {
1068+ "properties": {
1069+ "branches": "event-branches",
1070+ "branches-ignore": "event-branches-ignore",
1071+ "tags": "event-tags",
1072+ "tags-ignore": "event-tags-ignore",
1073+ "paths": "event-paths",
1074+ "paths-ignore": "event-paths-ignore"
1075+ }
1076+ }
1077+ },
1078+ "registry-package-string": {
1079+ "description": "Runs your workflow when activity related to GitHub Packages occurs in your repository.",
1080+ "string": {
1081+ "constant": "registry_package"
1082+ }
1083+ },
1084+ "registry-package": {
1085+ "description": "Runs your workflow when activity related to GitHub Packages occurs in your repository.",
1086+ "one-of": [
1087+ "null",
1088+ "registry-package-mapping"
1089+ ]
1090+ },
1091+ "registry-package-mapping": {
1092+ "mapping": {
1093+ "properties": {
1094+ "types": "registry-package-activity"
1095+ }
1096+ }
1097+ },
1098+ "registry-package-activity": {
1099+ "description": "The types of registry package activity that trigger the workflow. Supported activity types: `published`, `updated`.",
1100+ "one-of": [
1101+ "registry-package-activity-type",
1102+ "registry-package-activity-types"
1103+ ]
1104+ },
1105+ "registry-package-activity-types": {
1106+ "sequence": {
1107+ "item-type": "registry-package-activity-type"
1108+ }
1109+ },
1110+ "registry-package-activity-type": {
1111+ "allowed-values": [
1112+ "published",
1113+ "updated"
1114+ ]
1115+ },
1116+ "release-string": {
1117+ "description": "Runs your workflow when release activity in your repository occurs.",
1118+ "string": {
1119+ "constant": "release"
1120+ }
1121+ },
1122+ "release": {
1123+ "description": "Runs your workflow when release activity in your repository occurs.",
1124+ "one-of": [
1125+ "null",
1126+ "release-mapping"
1127+ ]
1128+ },
1129+ "release-mapping": {
1130+ "mapping": {
1131+ "properties": {
1132+ "types": "release-activity"
1133+ }
1134+ }
1135+ },
1136+ "release-activity": {
1137+ "description": "The types of release activity that trigger the workflow. Supported activity types: `published`, `unpublished`, `created`, `edited`, `deleted`, `prereleased`, `released`.",
1138+ "one-of": [
1139+ "release-activity-type",
1140+ "release-activity-types"
1141+ ]
1142+ },
1143+ "release-activity-types": {
1144+ "sequence": {
1145+ "item-type": "release-activity-type"
1146+ }
1147+ },
1148+ "release-activity-type": {
1149+ "allowed-values": [
1150+ "published",
1151+ "unpublished",
1152+ "created",
1153+ "edited",
1154+ "deleted",
1155+ "prereleased",
1156+ "released"
1157+ ]
1158+ },
1159+ "schedule-string": {
1160+ "description": "The `schedule` event allows you to trigger a workflow at a scheduled time.\n\nYou can schedule a workflow to run at specific UTC times using POSIX cron syntax. Scheduled workflows run on the latest commit on the default or base branch. The shortest interval you can run scheduled workflows is once every 5 minutes. GitHub Actions does not support the non-standard syntax `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`, and `@reboot`.",
1161+ "string": {
1162+ "constant": "schedule"
1163+ }
1164+ },
1165+ "schedule": {
1166+ "description": "The `schedule` event allows you to trigger a workflow at a scheduled time.\n\nYou can schedule a workflow to run at specific UTC times using POSIX cron syntax. Scheduled workflows run on the latest commit on the default or base branch. The shortest interval you can run scheduled workflows is once every 5 minutes. GitHub Actions does not support the non-standard syntax `@yearly`, `@monthly`, `@weekly`, `@daily`, `@hourly`, and `@reboot`.",
1167+ "sequence": {
1168+ "item-type": "cron-mapping"
1169+ }
1170+ },
1171+ "status-string": {
1172+ "description": "Runs your workflow when the status of a Git commit changes. For example, commits can be marked as `error`, `failure`, `pending`, or `success`. If you want to provide more details about the status change, you may want to use the `check_run` event.",
1173+ "string": {
1174+ "constant": "status"
1175+ }
1176+ },
1177+ "status": {
1178+ "description": "Runs your workflow when the status of a Git commit changes. For example, commits can be marked as `error`, `failure`, `pending`, or `success`. If you want to provide more details about the status change, you may want to use the `check_run` event.",
1179+ "null": {}
1180+ },
1181+ "watch-string": {
1182+ "description": "Runs your workflow when the workflow's repository is starred.",
1183+ "string": {
1184+ "constant": "watch"
1185+ }
1186+ },
1187+ "watch": {
1188+ "description": "Runs your workflow when the workflow's repository is starred.",
1189+ "one-of": [
1190+ "null",
1191+ "watch-mapping"
1192+ ]
1193+ },
1194+ "watch-mapping": {
1195+ "mapping": {
1196+ "properties": {
1197+ "types": "watch-activity"
1198+ }
1199+ }
1200+ },
1201+ "watch-activity": {
1202+ "description": "The types of watch activity that trigger the workflow. Supported activity types: `started`.",
1203+ "one-of": [
1204+ "watch-activity-type",
1205+ "watch-activity-types"
1206+ ]
1207+ },
1208+ "watch-activity-types": {
1209+ "sequence": {
1210+ "item-type": "watch-activity-type"
1211+ }
1212+ },
1213+ "watch-activity-type": {
1214+ "allowed-values": [
1215+ "started"
1216+ ]
1217+ },
1218+ "workflow-run-string": {
1219+ "description": "This event occurs when a workflow run is requested or completed. It allows you to execute a workflow based on execution or completion of another workflow. The workflow started by the `workflow_run` event is able to access secrets and write tokens, even if the previous workflow was not. This is useful in cases where the previous workflow is intentionally not privileged, but you need to take a privileged action in a later workflow.",
1220+ "string": {
1221+ "constant": "workflow_run"
1222+ }
1223+ },
1224+ "workflow-run": {
1225+ "description": "This event occurs when a workflow run is requested or completed. It allows you to execute a workflow based on execution or completion of another workflow. The workflow started by the `workflow_run` event is able to access secrets and write tokens, even if the previous workflow was not. This is useful in cases where the previous workflow is intentionally not privileged, but you need to take a privileged action in a later workflow.",
1226+ "one-of": [
1227+ "null",
1228+ "workflow-run-mapping"
1229+ ]
1230+ },
1231+ "workflow-run-mapping": {
1232+ "mapping": {
1233+ "properties": {
1234+ "types": "workflow-run-activity",
1235+ "workflows": "workflow-run-workflows",
1236+ "branches": "event-branches",
1237+ "branches-ignore": "event-branches-ignore"
1238+ }
1239+ }
1240+ },
1241+ "workflow-run-workflows": {
1242+ "description": "The name of the workflow that triggers the `workflow_run` event. The workflow must be in the same repository as the workflow that uses the `workflow_run` event.",
1243+ "one-of": [
1244+ "non-empty-string",
1245+ "sequence-of-non-empty-string"
1246+ ]
1247+ },
1248+ "workflow-run-activity": {
1249+ "description": "The types of workflow run activity that trigger the workflow. Supported activity types: `completed`, `requested`, `in_progress`.",
1250+ "one-of": [
1251+ "workflow-run-activity-type",
1252+ "workflow-run-activity-types"
1253+ ]
1254+ },
1255+ "workflow-run-activity-types": {
1256+ "sequence": {
1257+ "item-type": "workflow-run-activity-type"
1258+ }
1259+ },
1260+ "workflow-run-activity-type": {
1261+ "allowed-values": [
1262+ "requested",
1263+ "completed",
1264+ "in_progress"
1265+ ]
1266+ },
1267+ "event-branches": {
1268+ "description": "Use the `branches` filter when you want to include branch name patterns or when you want to both include and exclude branch name patterns. You cannot use both the `branches` and `branches-ignore` filters for the same event in a workflow.",
1269+ "one-of": [
1270+ "non-empty-string",
1271+ "sequence-of-non-empty-string"
1272+ ]
1273+ },
1274+ "event-branches-ignore": {
1275+ "description": "Use the `branches-ignore` filter when you only want to exclude branch name patterns. You cannot use both the `branches` and `branches-ignore` filters for the same event in a workflow.",
1276+ "one-of": [
1277+ "non-empty-string",
1278+ "sequence-of-non-empty-string"
1279+ ]
1280+ },
1281+ "event-names": {
1282+ "description": "Use the `names` filter when you want to include names via patterns or when you want to both include and exclude names using patterns. ",
1283+ "one-of": [
1284+ "non-empty-string",
1285+ "sequence-of-non-empty-string"
1286+ ]
1287+ },
1288+ "event-tags": {
1289+ "description": "Use the `tags` filter when you want to include tag name patterns or when you want to both include and exclude tag names patterns. You cannot use both the `tags` and `tags-ignore` filters for the same event in a workflow.",
1290+ "one-of": [
1291+ "non-empty-string",
1292+ "sequence-of-non-empty-string"
1293+ ]
1294+ },
1295+ "event-tags-ignore": {
1296+ "description": "Use the `tags-ignore` filter when you only want to exclude tag name patterns. You cannot use both the `tags` and `tags-ignore` filters for the same event in a workflow.",
1297+ "one-of": [
1298+ "non-empty-string",
1299+ "sequence-of-non-empty-string"
1300+ ]
1301+ },
1302+ "event-paths": {
1303+ "description": "Use the `paths` filter when you want to include file path patterns or when you want to both include and exclude file path patterns. You cannot use both the `paths` and `paths-ignore` filters for the same event in a workflow.",
1304+ "one-of": [
1305+ "non-empty-string",
1306+ "sequence-of-non-empty-string"
1307+ ]
1308+ },
1309+ "event-paths-ignore": {
1310+ "description": "Use the `paths-ignore` filter when you only want to exclude file path patterns. You cannot use both the `paths` and `paths-ignore` filters for the same event in a workflow.",
1311+ "one-of": [
1312+ "non-empty-string",
1313+ "sequence-of-non-empty-string"
1314+ ]
1315+ },
1316+ "event-versions": {
1317+ "description": "Use the `versions` filter when you want to include versions via patterns or when you want to both include and exclude versions using patterns. ",
1318+ "one-of": [
1319+ "non-empty-string",
1320+ "sequence-of-non-empty-string"
1321+ ]
1322+ },
1323+ "repository-dispatch-string": {
1324+ "description": "You can use the GitHub API to trigger a webhook event called `repository_dispatch` when you want to trigger a workflow for activity that happens outside of GitHub.",
1325+ "string": {
1326+ "constant": "branch_protection_rule"
1327+ }
1328+ },
1329+ "repository-dispatch": {
1330+ "description": "You can use the GitHub API to trigger a webhook event called `repository_dispatch` when you want to trigger a workflow for activity that happens outside of GitHub.",
1331+ "one-of": [
1332+ "null",
1333+ "repository-dispatch-mapping"
1334+ ]
1335+ },
1336+ "repository-dispatch-mapping": {
1337+ "mapping": {
1338+ "properties": {
1339+ "types": "sequence-of-non-empty-string"
1340+ }
1341+ }
1342+ },
1343+ "workflow-call-string": {
1344+ "description": "The `workflow_call` event is used to indicate that a workflow can be called by another workflow. When a workflow is triggered with the `workflow_call` event, the event payload in the called workflow is the same event payload from the calling workflow.",
1345+ "string": {
1346+ "constant": "workflow_call"
1347+ }
1348+ },
1349+ "workflow-call": {
1350+ "description": "The `workflow_call` event is used to indicate that a workflow can be called by another workflow. When a workflow is triggered with the `workflow_call` event, the event payload in the called workflow is the same event payload from the calling workflow.",
1351+ "one-of": [
1352+ "null",
1353+ "workflow-call-mapping"
1354+ ]
1355+ },
1356+ "workflow-call-mapping": {
1357+ "mapping": {
1358+ "properties": {
1359+ "inputs": "workflow-call-inputs",
1360+ "secrets": "workflow-call-secrets",
1361+ "outputs": "workflow-call-outputs"
1362+ }
1363+ }
1364+ },
1365+ "workflow-call-inputs": {
1366+ "description": "Inputs that are passed to the called workflow from the caller workflow.",
1367+ "mapping": {
1368+ "loose-key-type": "non-empty-string",
1369+ "loose-value-type": "workflow-call-input-definition"
1370+ }
1371+ },
1372+ "workflow-call-input-definition": {
1373+ "mapping": {
1374+ "properties": {
1375+ "description": {
1376+ "type": "string",
1377+ "description": "A string description of the input parameter."
1378+ },
1379+ "type": {
1380+ "type": "workflow-call-input-type",
1381+ "required": true
1382+ },
1383+ "required": {
1384+ "type": "boolean",
1385+ "description": "A boolean to indicate whether the action requires the input parameter. Set to `true` when the parameter is required."
1386+ },
1387+ "default": "workflow-call-input-default"
1388+ }
1389+ }
1390+ },
1391+ "workflow-call-input-type": {
1392+ "description": "Required if input is defined for the `on.workflow_call` keyword. The value of this parameter is a string specifying the data type of the input. This must be one of: `boolean`, `number`, or `string`.",
1393+ "one-of": [
1394+ "input-type-string",
1395+ "input-type-boolean",
1396+ "input-type-number"
1397+ ]
1398+ },
1399+ "input-type-string": {
1400+ "string": {
1401+ "constant": "string"
1402+ }
1403+ },
1404+ "input-type-boolean": {
1405+ "string": {
1406+ "constant": "boolean"
1407+ }
1408+ },
1409+ "input-type-number": {
1410+ "string": {
1411+ "constant": "number"
1412+ }
1413+ },
1414+ "input-type-choice": {
1415+ "string": {
1416+ "constant": "choice"
1417+ }
1418+ },
1419+ "input-type-environment": {
1420+ "string": {
1421+ "constant": "environment"
1422+ }
1423+ },
1424+ "workflow-call-input-default": {
1425+ "description": "If a `default` parameter is not set, the default value of the input is `false` for boolean, `0` for a number, and `\"\"` for a string.",
1426+ "context": [
1427+ "github",
1428+ "inputs",
1429+ "vars"
1430+ ],
1431+ "one-of": [
1432+ "string",
1433+ "boolean",
1434+ "number"
1435+ ]
1436+ },
1437+ "workflow-call-secrets": {
1438+ "description": "A map of the secrets that can be used in the called workflow. Within the called workflow, you can use the `secrets` context to refer to a secret.",
1439+ "mapping": {
1440+ "loose-key-type": "workflow-call-secret-name",
1441+ "loose-value-type": "workflow-call-secret-definition"
1442+ }
1443+ },
1444+ "workflow-call-secret-name": {
1445+ "string": {
1446+ "require-non-empty": true
1447+ },
1448+ "description": "A string identifier to associate with the secret."
1449+ },
1450+ "workflow-call-secret-definition": {
1451+ "one-of": [
1452+ "null",
1453+ "workflow-call-secret-mapping-definition"
1454+ ]
1455+ },
1456+ "workflow-call-secret-mapping-definition": {
1457+ "mapping": {
1458+ "properties": {
1459+ "description": {
1460+ "type": "string",
1461+ "description": "A string description of the secret parameter."
1462+ },
1463+ "required": {
1464+ "type": "boolean",
1465+ "description": "A boolean specifying whether the secret must be supplied."
1466+ }
1467+ }
1468+ }
1469+ },
1470+ "workflow-call-outputs": {
1471+ "description": "A reusable workflow may generate data that you want to use in the caller workflow. To use these outputs, you must specify them as the outputs of the reusable workflow.",
1472+ "mapping": {
1473+ "loose-key-type": "workflow-call-output-name",
1474+ "loose-value-type": "workflow-call-output-definition"
1475+ }
1476+ },
1477+ "workflow-call-output-name": {
1478+ "string": {
1479+ "require-non-empty": true
1480+ },
1481+ "description": "A string identifier to associate with the output. The value of `<output_id>` is a map of the input's metadata. The `<output_id>` must be a unique identifier within the outputs object and must start with a letter or _ and contain only alphanumeric characters, -, or _."
1482+ },
1483+ "workflow-call-output-definition": {
1484+ "mapping": {
1485+ "properties": {
1486+ "description": {
1487+ "type": "string",
1488+ "description": "A string description of the output parameter."
1489+ },
1490+ "value": {
1491+ "type": "workflow-output-context",
1492+ "required": true
1493+ }
1494+ }
1495+ }
1496+ },
1497+ "workflow-output-context": {
1498+ "description": "The value to assign to the output parameter.",
1499+ "context": [
1500+ "github",
1501+ "inputs",
1502+ "vars",
1503+ "jobs"
1504+ ],
1505+ "string": {}
1506+ },
1507+ "workflow-dispatch-string": {
1508+ "description": "The `workflow_dispatch` event allows you to manually trigger a workflow run. A workflow can be manually triggered using the GitHub API, GitHub CLI, or GitHub browser interface.",
1509+ "string": {
1510+ "constant": "workflow_dispatch"
1511+ }
1512+ },
1513+ "workflow-dispatch": {
1514+ "description": "The `workflow_dispatch` event allows you to manually trigger a workflow run. A workflow can be manually triggered using the GitHub API, GitHub CLI, or GitHub browser interface.",
1515+ "one-of": [
1516+ "null",
1517+ "workflow-dispatch-mapping"
1518+ ]
1519+ },
1520+ "workflow-dispatch-mapping": {
1521+ "mapping": {
1522+ "properties": {
1523+ "inputs": "workflow-dispatch-inputs"
1524+ }
1525+ }
1526+ },
1527+ "workflow-dispatch-inputs": {
1528+ "description": "You can configure custom-defined input properties, default input values, and required inputs for the event directly in your workflow. When you trigger the event, you can provide the `ref` and any `inputs`. When the workflow runs, you can access the input values in the `inputs` context.",
1529+ "mapping": {
1530+ "loose-key-type": "workflow-dispatch-input-name",
1531+ "loose-value-type": "workflow-dispatch-input"
1532+ }
1533+ },
1534+ "workflow-dispatch-input-name": {
1535+ "string": {
1536+ "require-non-empty": true
1537+ },
1538+ "description": "A string identifier to associate with the input. The value of <input_id> is a map of the input's metadata. The <input_id> must be a unique identifier within the inputs object. The <input_id> must start with a letter or _ and contain only alphanumeric characters, -, or _."
1539+ },
1540+ "workflow-dispatch-input": {
1541+ "mapping": {
1542+ "properties": {
1543+ "description": {
1544+ "type": "string",
1545+ "description": "A string description of the input parameter."
1546+ },
1547+ "type": {
1548+ "type": "workflow-dispatch-input-type"
1549+ },
1550+ "required": {
1551+ "type": "boolean",
1552+ "description": "A boolean to indicate whether the workflow requires the input parameter. Set to true when the parameter is required."
1553+ },
1554+ "default": "workflow-dispatch-input-default",
1555+ "options": {
1556+ "type": "sequence-of-string",
1557+ "description": "The options of the dropdown list, if the type is a choice."
1558+ }
1559+ }
1560+ }
1561+ },
1562+ "workflow-dispatch-input-type": {
1563+ "description": "A string representing the type of the input. This must be one of: `boolean`, `number`, `string`, `choice`, or `environment`.",
1564+ "one-of": [
1565+ "input-type-string",
1566+ "input-type-boolean",
1567+ "input-type-number",
1568+ "input-type-environment",
1569+ "input-type-choice"
1570+ ]
1571+ },
1572+ "workflow-dispatch-input-default": {
1573+ "description": "The default value is used when an input parameter isn't specified in a workflow file.",
1574+ "one-of": [
1575+ "string",
1576+ "boolean",
1577+ "number"
1578+ ]
1579+ },
1580+ "permissions": {
1581+ "description": "You can use `permissions` to modify the default permissions granted to the `GITHUB_TOKEN`, adding or removing access as required, so that you only allow the minimum required access.\n\n[Documentation](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#permissions)",
1582+ "one-of": [
1583+ "permissions-mapping",
1584+ "permission-level-shorthand-read-all",
1585+ "permission-level-shorthand-write-all"
1586+ ]
1587+ },
1588+ "permissions-mapping": {
1589+ "mapping": {
1590+ "properties": {
1591+ "actions": {
1592+ "type": "permission-level-any",
1593+ "description": "Actions workflows, workflow runs, and artifacts."
1594+ },
1595+ "artifact-metadata": {
1596+ "type": "permission-level-any",
1597+ "description": "Storage and deployment records for build artifacts."
1598+ },
1599+ "attestations": {
1600+ "type": "permission-level-any",
1601+ "description": "Artifact attestations."
1602+ },
1603+ "checks": {
1604+ "type": "permission-level-any",
1605+ "description": "Check runs and check suites."
1606+ },
1607+ "code-quality": {
1608+ "type": "permission-level-any",
1609+ "description": "Code quality data."
1610+ },
1611+ "contents": {
1612+ "type": "permission-level-any",
1613+ "description": "Repository contents, commits, branches, downloads, releases, and merges."
1614+ },
1615+ "copilot-requests": {
1616+ "type": "permission-level-write-or-no-access",
1617+ "description": "GitHub Copilot requests."
1618+ },
1619+ "deployments": {
1620+ "type": "permission-level-any",
1621+ "description": "Deployments and deployment statuses."
1622+ },
1623+ "discussions": {
1624+ "type": "permission-level-any",
1625+ "description": "Discussions and related comments and labels."
1626+ },
1627+ "drives": {
1628+ "type": "permission-level-any",
1629+ "description": "GitHub Drives persistent workspace storage. Use `read` for checkout and `write` to commit changes."
1630+ },
1631+ "id-token": {
1632+ "type": "permission-level-write-or-no-access",
1633+ "description": "Token to request an OpenID Connect token."
1634+ },
1635+ "issues": {
1636+ "type": "permission-level-any",
1637+ "description": "Issues and related comments, assignees, labels, and milestones."
1638+ },
1639+ "models": {
1640+ "type": "permission-level-read-or-no-access",
1641+ "description": "Call AI models with GitHub Models."
1642+ },
1643+ "packages": {
1644+ "type": "permission-level-any",
1645+ "description": "Packages published to the GitHub Package Platform."
1646+ },
1647+ "pages": {
1648+ "type": "permission-level-any",
1649+ "description": "Retrieve Pages statuses, configuration, and builds, as well as create new builds."
1650+ },
1651+ "pull-requests": {
1652+ "type": "permission-level-any",
1653+ "description": "Pull requests and related comments, assignees, labels, milestones, and merges."
1654+ },
1655+ "repository-projects": {
1656+ "type": "permission-level-any",
1657+ "description": "Classic projects within a repository."
1658+ },
1659+ "security-events": {
1660+ "type": "permission-level-any",
1661+ "description": "Code scanning alerts."
1662+ },
1663+ "statuses": {
1664+ "type": "permission-level-any",
1665+ "description": "Commit statuses."
1666+ },
1667+ "vulnerability-alerts": {
1668+ "type": "permission-level-read-or-no-access",
1669+ "description": "Dependabot alerts."
1670+ }
1671+ }
1672+ }
1673+ },
1674+ "permission-level-any": {
1675+ "description": "The permission level for the `GITHUB_TOKEN`.",
1676+ "one-of": [
1677+ "permission-level-read",
1678+ "permission-level-write",
1679+ "permission-level-no-access"
1680+ ]
1681+ },
1682+ "permission-level-read-or-no-access": {
1683+ "one-of": [
1684+ "permission-level-read",
1685+ "permission-level-no-access"
1686+ ]
1687+ },
1688+ "permission-level-write-or-no-access": {
1689+ "one-of": [
1690+ "permission-level-write",
1691+ "permission-level-no-access"
1692+ ]
1693+ },
1694+ "permission-level-read": {
1695+ "description": "The permission level for the `GITHUB_TOKEN`. Grants `read` permission for the specified scope.",
1696+ "string": {
1697+ "constant": "read"
1698+ }
1699+ },
1700+ "permission-level-write": {
1701+ "description": "The permission level for the `GITHUB_TOKEN`. Grants `write` permission for the specified scope.",
1702+ "string": {
1703+ "constant": "write"
1704+ }
1705+ },
1706+ "permission-level-no-access": {
1707+ "description": "The permission level for the `GITHUB_TOKEN`. Restricts all access for the specified scope.",
1708+ "string": {
1709+ "constant": "none"
1710+ }
1711+ },
1712+ "permission-level-shorthand-read-all": {
1713+ "description": "The permission level for the `GITHUB_TOKEN`. Grants `read` access for all scopes.",
1714+ "string": {
1715+ "constant": "read-all"
1716+ }
1717+ },
1718+ "permission-level-shorthand-write-all": {
1719+ "description": "The permission level for the `GITHUB_TOKEN`. Grants `write` access for all scopes.",
1720+ "string": {
1721+ "constant": "write-all"
1722+ }
1723+ },
1724+ "workflow-defaults": {
1725+ "description": "Use `defaults` to create a map of default settings that will apply to all jobs in the workflow. You can also set default settings that are only available to a job.\n\n[Documentation](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#defaults)",
1726+ "mapping": {
1727+ "properties": {
1728+ "run": "workflow-defaults-run"
1729+ }
1730+ }
1731+ },
1732+ "workflow-defaults-run": {
1733+ "mapping": {
1734+ "properties": {
1735+ "shell": "shell",
1736+ "working-directory": "working-directory"
1737+ }
1738+ }
1739+ },
1740+ "workflow-env": {
1741+ "description": "A map of environment variables that are available to the steps of all jobs in the workflow. You can also set environment variables that are only available to the steps of a single job or to a single step.\n\n[Documentation](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#env)",
1742+ "context": [
1743+ "github",
1744+ "inputs",
1745+ "vars",
1746+ "secrets"
1747+ ],
1748+ "mapping": {
1749+ "loose-key-type": "non-empty-string",
1750+ "loose-value-type": "string"
1751+ }
1752+ },
1753+ "jobs": {
1754+ "description": "A workflow run is made up of one or more `jobs`, which run in parallel by default. To run jobs sequentially, you can define dependencies on other jobs using the `jobs.<job_id>.needs` keyword. Each job runs in a runner environment specified by `runs-on`.\n\n[Documentation](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#jobs)",
1755+ "mapping": {
1756+ "loose-key-type": "job-id",
1757+ "loose-value-type": "job"
1758+ }
1759+ },
1760+ "job-id": {
1761+ "string": {
1762+ "require-non-empty": true
1763+ },
1764+ "description": "A unique identifier for the job. The identifier must start with a letter or _ and contain only alphanumeric characters, -, or _."
1765+ },
1766+ "job": {
1767+ "description": "Each job must have an id to associate with the job. The key `job_id` is a string and its value is a map of the job's configuration data. You must replace `<job_id>` with a string that is unique to the jobs object. The `<job_id>` must start with a letter or _ and contain only alphanumeric characters, -, or _.",
1768+ "one-of": [
1769+ "job-factory",
1770+ "workflow-job"
1771+ ]
1772+ },
1773+ "job-factory": {
1774+ "mapping": {
1775+ "properties": {
1776+ "needs": "needs",
1777+ "if": "job-if",
1778+ "strategy": "strategy",
1779+ "name": {
1780+ "type": "string-strategy-context",
1781+ "description": "The name of the job displayed on GitHub."
1782+ },
1783+ "runs-on": {
1784+ "type": "runs-on",
1785+ "required": true
1786+ },
1787+ "timeout-minutes": {
1788+ "type": "number-strategy-context",
1789+ "description": "The maximum number of minutes to let a workflow run before GitHub automatically cancels it. Default: 360"
1790+ },
1791+ "cancel-timeout-minutes": "number-strategy-context",
1792+ "continue-on-error": {
1793+ "type": "boolean-strategy-context",
1794+ "description": "Prevents a workflow run from failing when a job fails. Set to true to allow a workflow run to pass when this job fails."
1795+ },
1796+ "container": "container",
1797+ "services": "services",
1798+ "env": "job-env",
1799+ "environment": "job-environment",
1800+ "permissions": "permissions",
1801+ "concurrency": "job-concurrency",
1802+ "outputs": "job-outputs",
1803+ "defaults": "job-defaults",
1804+ "steps": "steps",
1805+ "snapshot": "snapshot"
1806+ }
1807+ }
1808+ },
1809+ "workflow-job": {
1810+ "mapping": {
1811+ "properties": {
1812+ "name": {
1813+ "type": "string-strategy-context",
1814+ "description": "The name of the job displayed on GitHub."
1815+ },
1816+ "uses": {
1817+ "description": "The location and version of a reusable workflow file to run as a job. Use one of the following formats:\n\n* `{owner}/{repo}/.github/workflows/{filename}@{ref}` for reusable workflows in public and private repositories.\n* `./.github/workflows/{filename}` for reusable workflows in the same repository.\n\n{ref} can be a SHA, a release tag, or a branch name. Using the commit SHA is the safest for stability and security.",
1818+ "type": "non-empty-string",
1819+ "required": true
1820+ },
1821+ "with": "workflow-job-with",
1822+ "secrets": "workflow-job-secrets",
1823+ "needs": "needs",
1824+ "if": "job-if",
1825+ "permissions": "permissions",
1826+ "concurrency": "job-concurrency",
1827+ "strategy": "strategy"
1828+ }
1829+ }
1830+ },
1831+ "workflow-job-with": {
1832+ "description": "When a job is used to call a reusable workflow, you can use `with` to provide a map of inputs that are passed to the called workflow.\n\nAny inputs that you pass must match the input specifications defined in the called workflow.",
1833+ "mapping": {
1834+ "loose-key-type": "non-empty-string",
1835+ "loose-value-type": "scalar-needs-context"
1836+ }
1837+ },
1838+ "workflow-job-secrets": {
1839+ "description": "When a job is used to call a reusable workflow, you can use `secrets` to provide a map of secrets that are passed to the called workflow.\n\nAny secrets that you pass must match the names defined in the called workflow.",
1840+ "one-of": [
1841+ "workflow-job-secrets-mapping",
1842+ "workflow-job-secrets-inherit"
1843+ ]
1844+ },
1845+ "workflow-job-secrets-mapping": {
1846+ "mapping": {
1847+ "loose-key-type": "non-empty-string",
1848+ "loose-value-type": "scalar-needs-context-with-secrets"
1849+ }
1850+ },
1851+ "workflow-job-secrets-inherit": {
1852+ "string": {
1853+ "constant": "inherit"
1854+ }
1855+ },
1856+ "needs": {
1857+ "description": "Use `needs` to identify any jobs that must complete successfully before this job will run. It can be a string or array of strings. If a job fails, all jobs that need it are skipped unless the jobs use a conditional expression that causes the job to continue. If a run contains a series of jobs that need each other, a failure applies to all jobs in the dependency chain from the point of failure onwards.",
1858+ "one-of": [
1859+ "sequence-of-non-empty-string",
1860+ "non-empty-string"
1861+ ]
1862+ },
1863+ "job-if": {
1864+ "description": "You can use the `if` conditional to prevent a job from running unless a condition is met. You can use any supported context and expression to create a conditional.",
1865+ "context": [
1866+ "github",
1867+ "inputs",
1868+ "vars",
1869+ "needs",
1870+ "always(0,0)",
1871+ "failure(0,MAX)",
1872+ "cancelled(0,0)",
1873+ "success(0,MAX)"
1874+ ],
1875+ "string": {}
1876+ },
1877+ "job-if-result": {
1878+ "context": [
1879+ "github",
1880+ "inputs",
1881+ "vars",
1882+ "needs",
1883+ "always(0,0)",
1884+ "failure(0,MAX)",
1885+ "cancelled(0,0)",
1886+ "success(0,MAX)"
1887+ ],
1888+ "one-of": [
1889+ "null",
1890+ "boolean",
1891+ "number",
1892+ "string",
1893+ "sequence",
1894+ "mapping"
1895+ ]
1896+ },
1897+ "strategy": {
1898+ "description": "Use `strategy` to use a matrix strategy for your jobs. A matrix strategy lets you use variables in a single job definition to automatically create multiple job runs that are based on the combinations of the variables. ",
1899+ "context": [
1900+ "github",
1901+ "inputs",
1902+ "vars",
1903+ "needs"
1904+ ],
1905+ "mapping": {
1906+ "properties": {
1907+ "fail-fast": {
1908+ "type": "boolean",
1909+ "description": "Setting `fail-fast` to `false` prevents GitHub from canceling all in-progress jobs if any matrix job fails. Default: `true`"
1910+ },
1911+ "max-parallel": {
1912+ "type": "number",
1913+ "description": "The maximum number of jobs that can run simultaneously when using a matrix job strategy. By default, GitHub will maximize the number of jobs run in parallel depending on runner availability."
1914+ },
1915+ "matrix": "matrix"
1916+ }
1917+ }
1918+ },
1919+ "matrix": {
1920+ "description": "Use `matrix` to define a matrix of different job configurations. Within your matrix, define one or more variables followed by an array of values.",
1921+ "mapping": {
1922+ "properties": {
1923+ "include": {
1924+ "type": "matrix-filter",
1925+ "description": "Use `include` to expand existing matrix configurations or to add new configurations. The value of `include` is a list of objects.\n\nFor each object in the `include` list, the key:value pairs in the object will be added to each of the matrix combinations if none of the key:value pairs overwrite any of the original matrix values. If the object cannot be added to any of the matrix combinations, a new matrix combination will be created instead. Note that the original matrix values will not be overwritten, but added matrix values can be overwritten."
1926+ },
1927+ "exclude": {
1928+ "type": "matrix-filter",
1929+ "description": "To remove specific configurations defined in the matrix, use `exclude`. An excluded configuration only has to be a partial match for it to be excluded."
1930+ }
1931+ },
1932+ "loose-key-type": "non-empty-string",
1933+ "loose-value-type": "sequence"
1934+ }
1935+ },
1936+ "matrix-filter": {
1937+ "sequence": {
1938+ "item-type": "matrix-filter-item"
1939+ }
1940+ },
1941+ "matrix-filter-item": {
1942+ "mapping": {
1943+ "loose-key-type": "non-empty-string",
1944+ "loose-value-type": "any"
1945+ }
1946+ },
1947+ "snapshot": {
1948+ "description": "Use `snapshot` to define a custom image you want to create or update after your job succeeds by taking a snapshot of your runner.",
1949+ "one-of": [
1950+ "non-empty-string",
1951+ "snapshot-mapping"
1952+ ]
1953+ },
1954+ "snapshot-mapping": {
1955+ "mapping": {
1956+ "properties": {
1957+ "image-name": {
1958+ "description": "The desired name of the custom image you want to create or update.",
1959+ "type": "non-empty-string",
1960+ "required": true
1961+ },
1962+ "if": "snapshot-if",
1963+ "version": {
1964+ "description": "The desired major version updates upon a new custom image version creation.",
1965+ "type": "non-empty-string"
1966+ }
1967+ }
1968+ }
1969+ },
1970+ "snapshot-if": {
1971+ "context": [
1972+ "github",
1973+ "inputs",
1974+ "vars",
1975+ "needs",
1976+ "strategy",
1977+ "matrix"
1978+ ],
1979+ "description": "Use the if conditional to prevent a snapshot from being taken unless a condition is met. Any supported context and expression can be used to create a conditional. Expressions in an `if` conditional do not require the bracketed expression syntax. When you use expressions in an `if` conditional, you may omit the expression syntax because GitHub automatically evaluates the `if` conditional as an expression.",
1980+ "string": {}
1981+ },
1982+ "runs-on": {
1983+ "description": "Use `runs-on` to define the type of machine to run the job on.\n* The destination machine can be either a GitHub-hosted runner, larger runner, or a self-hosted runner.\n* You can target runners based on the labels assigned to them, or their group membership, or a combination of these.\n* You can provide `runs-on` as a single string or as an array of strings.\n* If you specify an array of strings, your workflow will execute on any runner that matches all of the specified `runs-on` values.\n* If you would like to run your workflow on multiple machines, use `jobs.<job_id>.strategy`.",
1984+ "context": [
1985+ "github",
1986+ "inputs",
1987+ "vars",
1988+ "needs",
1989+ "strategy",
1990+ "matrix"
1991+ ],
1992+ "one-of": [
1993+ "non-empty-string",
1994+ "sequence-of-non-empty-string",
1995+ "runs-on-mapping"
1996+ ]
1997+ },
1998+ "runs-on-mapping": {
1999+ "mapping": {
2000+ "properties": {
2001+ "group": {
2002+ "description": "The group from which to select a runner.",
2003+ "type": "non-empty-string"
2004+ },
2005+ "labels": "runs-on-labels"
2006+ }
2007+ }
2008+ },
2009+ "runs-on-labels": {
2010+ "description": "The label by which to filter for available runners.",
2011+ "one-of": [
2012+ "non-empty-string",
2013+ "sequence-of-non-empty-string"
2014+ ]
2015+ },
2016+ "job-env": {
2017+ "description": "A map of variables that are available to all steps in the job.",
2018+ "context": [
2019+ "github",
2020+ "inputs",
2021+ "vars",
2022+ "needs",
2023+ "strategy",
2024+ "matrix",
2025+ "secrets"
2026+ ],
2027+ "mapping": {
2028+ "loose-key-type": "non-empty-string",
2029+ "loose-value-type": "string"
2030+ }
2031+ },
2032+ "workflow-concurrency": {
2033+ "description": "Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression.\n\nYou can also specify `concurrency` at the job level.\n\n[Documentation](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#concurrency)",
2034+ "context": [
2035+ "github",
2036+ "inputs",
2037+ "vars"
2038+ ],
2039+ "one-of": [
2040+ "string",
2041+ "concurrency-mapping"
2042+ ]
2043+ },
2044+ "job-concurrency": {
2045+ "description": "Concurrency ensures that only a single job using the same concurrency group will run at a time. A concurrency group can be any string or expression. The expression can use any context except for the `secrets` context.\n\nYou can also specify `concurrency` at the workflow level.",
2046+ "context": [
2047+ "github",
2048+ "inputs",
2049+ "vars",
2050+ "needs",
2051+ "strategy",
2052+ "matrix"
2053+ ],
2054+ "one-of": [
2055+ "non-empty-string",
2056+ "concurrency-mapping"
2057+ ]
2058+ },
2059+ "concurrency-mapping": {
2060+ "description": "Concurrency ensures that only a single job or workflow using the same concurrency group will run at a time. A concurrency group can be any string or expression.\n\nYou can also specify `concurrency` at the job level.\n\n[Documentation](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#concurrency)",
2061+ "mapping": {
2062+ "properties": {
2063+ "group": {
2064+ "type": "non-empty-string",
2065+ "required": true,
2066+ "description": "When a concurrent job or workflow is queued, if another job or workflow using the same concurrency group in the repository is in progress, the queued job or workflow will be `pending`. Any previously pending job or workflow in the concurrency group will be canceled. To also cancel any currently running job or workflow in the same concurrency group, specify `cancel-in-progress: true`."
2067+ },
2068+ "cancel-in-progress": {
2069+ "type": "boolean",
2070+ "description": "To cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true."
2071+ },
2072+ "queue": {
2073+ "type": "concurrency-queue",
2074+ "description": "The queuing mode for the concurrency group. When set to `max`, workflows or jobs will wait in a queue for the concurrency group up to the maximum queue length. Default: `single` meaning at most one item can be pending."
2075+ }
2076+ }
2077+ }
2078+ },
2079+ "concurrency-queue": {
2080+ "allowed-values": [
2081+ "single",
2082+ "max"
2083+ ]
2084+ },
2085+ "job-environment": {
2086+ "description": "The environment that the job references. All environment protection rules must pass before a job referencing the environment is sent to a runner.",
2087+ "context": [
2088+ "github",
2089+ "inputs",
2090+ "vars",
2091+ "needs",
2092+ "strategy",
2093+ "matrix"
2094+ ],
2095+ "one-of": [
2096+ "string",
2097+ "job-environment-mapping"
2098+ ]
2099+ },
2100+ "job-environment-mapping": {
2101+ "mapping": {
2102+ "properties": {
2103+ "name": {
2104+ "type": "job-environment-name",
2105+ "required": true
2106+ },
2107+ "url": {
2108+ "type": "string-runner-context-no-secrets",
2109+ "description": "The environment URL, which maps to `environment_url` in the deployments API."
2110+ },
2111+ "deployment": {
2112+ "type": "boolean",
2113+ "description": "Whether to create a deployment record for this environment. Defaults to true."
2114+ }
2115+ }
2116+ }
2117+ },
2118+ "job-environment-name": {
2119+ "description": "The name of the environment used by the job.",
2120+ "context": [
2121+ "github",
2122+ "inputs",
2123+ "vars",
2124+ "needs",
2125+ "strategy",
2126+ "matrix"
2127+ ],
2128+ "string": {}
2129+ },
2130+ "job-defaults": {
2131+ "description": "A map of default settings that will apply to all steps in the job. You can also set default settings for the entire workflow.",
2132+ "mapping": {
2133+ "properties": {
2134+ "run": "job-defaults-run"
2135+ }
2136+ }
2137+ },
2138+ "job-defaults-run": {
2139+ "context": [
2140+ "github",
2141+ "inputs",
2142+ "vars",
2143+ "strategy",
2144+ "matrix",
2145+ "needs",
2146+ "env"
2147+ ],
2148+ "mapping": {
2149+ "properties": {
2150+ "shell": "shell",
2151+ "working-directory": "working-directory"
2152+ }
2153+ }
2154+ },
2155+ "job-outputs": {
2156+ "description": "A map of outputs for a called workflow. Called workflow outputs are available to all downstream jobs in the caller workflow. Each output has an identifier, an optional `description,` and a `value`. The `value` must be set to the value of an output from a job within the called workflow.",
2157+ "mapping": {
2158+ "loose-key-type": "non-empty-string",
2159+ "loose-value-type": "string-runner-context"
2160+ }
2161+ },
2162+ "steps": {
2163+ "description": "A job contains a sequence of tasks called `steps`. Steps can run commands, run setup tasks, run an action in your repository, a public repository, or an action published in a Docker registry, wait for background steps to complete, or cancel a background step. Not all steps run actions, but all actions run as a step. Each step runs in its own process in the runner environment and has access to the workspace and filesystem. Because steps run in their own process, changes to environment variables are not preserved between steps. GitHub provides built-in steps to set up and complete a job.",
2164+ "sequence": {
2165+ "item-type": "steps-item"
2166+ }
2167+ },
2168+ "steps-item": {
2169+ "one-of": [
2170+ "run-step",
2171+ "regular-step",
2172+ "wait-step",
2173+ "wait-all-step",
2174+ "cancel-step",
2175+ "parallel-step"
2176+ ]
2177+ },
2178+ "run-step": {
2179+ "mapping": {
2180+ "properties": {
2181+ "name": "step-name",
2182+ "id": "step-id",
2183+ "if": "step-if",
2184+ "timeout-minutes": "step-timeout-minutes",
2185+ "run": {
2186+ "type": "string-steps-context",
2187+ "description": "Runs command-line programs using the operating system's shell. If you do not provide a `name`, the step name will default to the text specified in the `run` command. Commands run using non-login shells by default. You can choose a different shell and customize the shell used to run commands. Each `run` keyword represents a new process and shell in the virtual environment. When you provide multi-line commands, each line runs in the same shell.",
2188+ "required": true
2189+ },
2190+ "continue-on-error": "step-continue-on-error",
2191+ "env": "step-env",
2192+ "working-directory": "string-steps-context",
2193+ "shell": "shell",
2194+ "background": "step-background"
2195+ }
2196+ }
2197+ },
2198+ "regular-step": {
2199+ "mapping": {
2200+ "properties": {
2201+ "name": "step-name",
2202+ "id": "step-id",
2203+ "if": "step-if",
2204+ "continue-on-error": "step-continue-on-error",
2205+ "timeout-minutes": "step-timeout-minutes",
2206+ "uses": {
2207+ "type": "step-uses",
2208+ "required": true
2209+ },
2210+ "with": "step-with",
2211+ "env": "step-env",
2212+ "background": "step-background"
2213+ }
2214+ }
2215+ },
2216+ "wait-step": {
2217+ "mapping": {
2218+ "properties": {
2219+ "name": "step-name",
2220+ "id": "step-id",
2221+ "continue-on-error": "step-continue-on-error",
2222+ "wait": {
2223+ "type": "step-wait-target",
2224+ "required": true
2225+ }
2226+ }
2227+ }
2228+ },
2229+ "wait-all-step": {
2230+ "mapping": {
2231+ "properties": {
2232+ "name": "step-name",
2233+ "id": "step-id",
2234+ "continue-on-error": "step-continue-on-error",
2235+ "wait-all": {
2236+ "type": "step-wait-all-value",
2237+ "required": true
2238+ }
2239+ }
2240+ }
2241+ },
2242+ "cancel-step": {
2243+ "mapping": {
2244+ "properties": {
2245+ "name": "step-name",
2246+ "id": "step-id",
2247+ "continue-on-error": "step-continue-on-error",
2248+ "cancel": {
2249+ "type": "step-cancel-target",
2250+ "required": true
2251+ }
2252+ }
2253+ }
2254+ },
2255+ "parallel-step": {
2256+ "mapping": {
2257+ "properties": {
2258+ "parallel": {
2259+ "type": "parallel-steps",
2260+ "required": true,
2261+ "description": "Run a group of steps concurrently. All steps in the parallel block run as background steps and the workflow waits for all of them to complete before continuing."
2262+ }
2263+ }
2264+ }
2265+ },
2266+ "parallel-steps": {
2267+ "sequence": {
2268+ "item-type": "steps-item"
2269+ }
2270+ },
2271+ "step-uses": {
2272+ "description": "Selects an action to run as part of a step in your job. An action is a reusable unit of code. You can use an action defined in the same repository as the workflow, a public repository, a [private repository with access enabled](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#allowing-access-to-components-in-a-private-repository), or in a published Docker container image.",
2273+ "string": {
2274+ "require-non-empty": true
2275+ }
2276+ },
2277+ "step-background": {
2278+ "boolean": {},
2279+ "description": "When set to true, runs this step in the background. The workflow continues to the next step immediately without waiting for the background step to finish."
2280+ },
2281+ "step-wait-target": {
2282+ "description": "The step ID or IDs of background steps to wait for.",
2283+ "one-of": [
2284+ "non-empty-string",
2285+ "sequence-of-non-empty-string"
2286+ ]
2287+ },
2288+ "step-wait-all-value": {
2289+ "description": "Wait for all prior background steps to complete. Use as a bare key (`wait-all:`) or with a boolean value.",
2290+ "one-of": [
2291+ "null",
2292+ "boolean"
2293+ ]
2294+ },
2295+ "step-cancel-target": {
2296+ "description": "The step ID of a background step to cancel.",
2297+ "string": {
2298+ "require-non-empty": true
2299+ }
2300+ },
2301+ "step-continue-on-error": {
2302+ "context": [
2303+ "github",
2304+ "inputs",
2305+ "vars",
2306+ "needs",
2307+ "strategy",
2308+ "matrix",
2309+ "secrets",
2310+ "steps",
2311+ "job",
2312+ "runner",
2313+ "env",
2314+ "hashFiles(1,255)"
2315+ ],
2316+ "boolean": {},
2317+ "description": "Prevents a job from failing when a step fails. Set to `true` to allow a job to pass when this step fails."
2318+ },
2319+ "step-id": {
2320+ "string": {
2321+ "require-non-empty": true
2322+ },
2323+ "description": "A unique identifier for the step. You can use the `id` to reference the step in contexts."
2324+ },
2325+ "step-if": {
2326+ "context": [
2327+ "github",
2328+ "inputs",
2329+ "vars",
2330+ "needs",
2331+ "strategy",
2332+ "matrix",
2333+ "steps",
2334+ "job",
2335+ "runner",
2336+ "env",
2337+ "always(0,0)",
2338+ "failure(0,0)",
2339+ "cancelled(0,0)",
2340+ "success(0,0)",
2341+ "hashFiles(1,255)"
2342+ ],
2343+ "description": "Use the `if` conditional to prevent a step from running unless a condition is met. Any supported context and expression can be used to create a conditional. Expressions in an `if` conditional do not require the bracketed expression syntax. When you use expressions in an `if` conditional, you may omit the expression syntax because GitHub automatically evaluates the `if` conditional as an expression.",
2344+ "string": {}
2345+ },
2346+ "step-if-result": {
2347+ "context": [
2348+ "github",
2349+ "inputs",
2350+ "vars",
2351+ "strategy",
2352+ "matrix",
2353+ "steps",
2354+ "job",
2355+ "runner",
2356+ "env",
2357+ "always(0,0)",
2358+ "failure(0,0)",
2359+ "cancelled(0,0)",
2360+ "success(0,0)",
2361+ "hashFiles(1,255)"
2362+ ],
2363+ "one-of": [
2364+ "null",
2365+ "boolean",
2366+ "number",
2367+ "string",
2368+ "sequence",
2369+ "mapping"
2370+ ]
2371+ },
2372+ "step-env": {
2373+ "description": "Sets variables for steps to use in the runner environment. You can also set variables for the entire workflow or a job.",
2374+ "context": [
2375+ "github",
2376+ "inputs",
2377+ "vars",
2378+ "needs",
2379+ "strategy",
2380+ "matrix",
2381+ "secrets",
2382+ "steps",
2383+ "job",
2384+ "runner",
2385+ "env",
2386+ "hashFiles(1,255)"
2387+ ],
2388+ "mapping": {
2389+ "loose-key-type": "non-empty-string",
2390+ "loose-value-type": "string"
2391+ }
2392+ },
2393+ "step-name": {
2394+ "context": [
2395+ "github",
2396+ "inputs",
2397+ "vars",
2398+ "needs",
2399+ "strategy",
2400+ "matrix",
2401+ "secrets",
2402+ "steps",
2403+ "job",
2404+ "runner",
2405+ "env",
2406+ "hashFiles(1,255)"
2407+ ],
2408+ "string": {},
2409+ "description": "A name for your step to display on GitHub."
2410+ },
2411+ "step-timeout-minutes": {
2412+ "context": [
2413+ "github",
2414+ "inputs",
2415+ "vars",
2416+ "needs",
2417+ "strategy",
2418+ "matrix",
2419+ "secrets",
2420+ "steps",
2421+ "job",
2422+ "runner",
2423+ "env",
2424+ "hashFiles(1,255)"
2425+ ],
2426+ "number": {},
2427+ "description": "The maximum number of minutes to run the step before killing the process."
2428+ },
2429+ "step-with": {
2430+ "description": "A map of the input parameters defined by the action. Each input parameter is a key/value pair. Input parameters are set as variables. When you specify an input in a workflow file or use a default input value, GitHub creates a variable for the input with the name `INPUT_<VARIABLE_NAME>`. The variable created converts input names to uppercase letters and replaces spaces with `_`.",
2431+ "context": [
2432+ "github",
2433+ "inputs",
2434+ "vars",
2435+ "needs",
2436+ "strategy",
2437+ "matrix",
2438+ "secrets",
2439+ "steps",
2440+ "job",
2441+ "runner",
2442+ "env",
2443+ "hashFiles(1,255)"
2444+ ],
2445+ "mapping": {
2446+ "loose-key-type": "non-empty-string",
2447+ "loose-value-type": "string"
2448+ }
2449+ },
2450+ "container": {
2451+ "description": "A container to run any steps in a job that don't already specify a container. If you have steps that use both script and container actions, the container actions will run as sibling containers on the same network with the same volume mounts.\n\nIf you do not set a container, all steps will run directly on the host specified by runs-on unless a step refers to an action configured to run in a container.",
2452+ "context": [
2453+ "github",
2454+ "inputs",
2455+ "vars",
2456+ "needs",
2457+ "strategy",
2458+ "matrix"
2459+ ],
2460+ "one-of": [
2461+ "string",
2462+ "container-mapping"
2463+ ]
2464+ },
2465+ "container-mapping": {
2466+ "mapping": {
2467+ "properties": {
2468+ "image": {
2469+ "type": "non-empty-string",
2470+ "description": "Use `jobs.<job_id>.container.image` to define the Docker image to use as the container to run the action. The value can be the Docker Hub image or a registry name."
2471+ },
2472+ "options": {
2473+ "type": "string",
2474+ "description": "Use `jobs.<job_id>.container.options` to configure additional Docker container resource options."
2475+ },
2476+ "env": "container-env",
2477+ "ports": {
2478+ "type": "sequence-of-non-empty-string",
2479+ "description": "Use `jobs.<job_id>.container.ports` to set an array of ports to expose on the container."
2480+ },
2481+ "volumes": {
2482+ "type": "sequence-of-non-empty-string",
2483+ "description": "Use `jobs.<job_id>.container.volumes` to set an array of volumes for the container to use. You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host."
2484+ },
2485+ "credentials": "container-registry-credentials"
2486+ }
2487+ }
2488+ },
2489+ "services": {
2490+ "description": "Additional containers to host services for a job in a workflow. These are useful for creating databases or cache services like redis. The runner on the virtual machine will automatically create a network and manage the life cycle of the service containers. When you use a service container for a job or your step uses container actions, you don't need to set port information to access the service. Docker automatically exposes all ports between containers on the same network. When both the job and the action run in a container, you can directly reference the container by its hostname. The hostname is automatically mapped to the service name. When a step does not use a container action, you must access the service using localhost and bind the ports.",
2491+ "context": [
2492+ "github",
2493+ "inputs",
2494+ "vars",
2495+ "needs",
2496+ "strategy",
2497+ "matrix"
2498+ ],
2499+ "mapping": {
2500+ "loose-key-type": "non-empty-string",
2501+ "loose-value-type": "services-container"
2502+ }
2503+ },
2504+ "services-container": {
2505+ "context": [
2506+ "github",
2507+ "inputs",
2508+ "vars",
2509+ "needs",
2510+ "strategy",
2511+ "matrix"
2512+ ],
2513+ "one-of": [
2514+ "non-empty-string",
2515+ "service-container-mapping"
2516+ ]
2517+ },
2518+ "container-registry-credentials": {
2519+ "description": "If the image's container registry requires authentication to pull the image, you can use `jobs.<job_id>.container.credentials` to set a map of the username and password. The credentials are the same values that you would provide to the `docker login` command.",
2520+ "context": [
2521+ "github",
2522+ "inputs",
2523+ "vars",
2524+ "secrets",
2525+ "env"
2526+ ],
2527+ "mapping": {
2528+ "properties": {
2529+ "username": "non-empty-string",
2530+ "password": "non-empty-string"
2531+ }
2532+ }
2533+ },
2534+ "container-env": {
2535+ "description": "Use `jobs.<job_id>.container.env` to set a map of variables in the container.",
2536+ "mapping": {
2537+ "loose-key-type": "non-empty-string",
2538+ "loose-value-type": "string-runner-context"
2539+ }
2540+ },
2541+ "non-empty-string": {
2542+ "string": {
2543+ "require-non-empty": true
2544+ }
2545+ },
2546+ "sequence-of-non-empty-string": {
2547+ "sequence": {
2548+ "item-type": "non-empty-string"
2549+ }
2550+ },
2551+ "sequence-of-string": {
2552+ "sequence": {
2553+ "item-type": "string"
2554+ }
2555+ },
2556+ "boolean-needs-context": {
2557+ "context": [
2558+ "github",
2559+ "inputs",
2560+ "vars",
2561+ "needs"
2562+ ],
2563+ "boolean": {}
2564+ },
2565+ "number-needs-context": {
2566+ "context": [
2567+ "github",
2568+ "inputs",
2569+ "vars",
2570+ "needs"
2571+ ],
2572+ "number": {}
2573+ },
2574+ "string-needs-context": {
2575+ "context": [
2576+ "github",
2577+ "inputs",
2578+ "vars",
2579+ "needs"
2580+ ],
2581+ "string": {}
2582+ },
2583+ "scalar-needs-context": {
2584+ "context": [
2585+ "github",
2586+ "inputs",
2587+ "vars",
2588+ "needs",
2589+ "strategy",
2590+ "matrix"
2591+ ],
2592+ "one-of": [
2593+ "string",
2594+ "boolean",
2595+ "number"
2596+ ]
2597+ },
2598+ "scalar-needs-context-with-secrets": {
2599+ "context": [
2600+ "github",
2601+ "inputs",
2602+ "vars",
2603+ "needs",
2604+ "secrets",
2605+ "strategy",
2606+ "matrix"
2607+ ],
2608+ "one-of": [
2609+ "string",
2610+ "boolean",
2611+ "number"
2612+ ]
2613+ },
2614+ "boolean-strategy-context": {
2615+ "context": [
2616+ "github",
2617+ "inputs",
2618+ "vars",
2619+ "needs",
2620+ "strategy",
2621+ "matrix"
2622+ ],
2623+ "boolean": {}
2624+ },
2625+ "number-strategy-context": {
2626+ "context": [
2627+ "github",
2628+ "inputs",
2629+ "vars",
2630+ "needs",
2631+ "strategy",
2632+ "matrix"
2633+ ],
2634+ "number": {}
2635+ },
2636+ "string-strategy-context": {
2637+ "context": [
2638+ "github",
2639+ "inputs",
2640+ "vars",
2641+ "needs",
2642+ "strategy",
2643+ "matrix"
2644+ ],
2645+ "string": {}
2646+ },
2647+ "boolean-steps-context": {
2648+ "context": [
2649+ "github",
2650+ "inputs",
2651+ "vars",
2652+ "needs",
2653+ "strategy",
2654+ "matrix",
2655+ "secrets",
2656+ "steps",
2657+ "job",
2658+ "runner",
2659+ "env",
2660+ "hashFiles(1,255)"
2661+ ],
2662+ "boolean": {}
2663+ },
2664+ "number-steps-context": {
2665+ "context": [
2666+ "github",
2667+ "inputs",
2668+ "vars",
2669+ "needs",
2670+ "strategy",
2671+ "matrix",
2672+ "secrets",
2673+ "steps",
2674+ "job",
2675+ "runner",
2676+ "env",
2677+ "hashFiles(1,255)"
2678+ ],
2679+ "number": {}
2680+ },
2681+ "string-runner-context": {
2682+ "context": [
2683+ "github",
2684+ "inputs",
2685+ "vars",
2686+ "needs",
2687+ "strategy",
2688+ "matrix",
2689+ "secrets",
2690+ "steps",
2691+ "job",
2692+ "runner",
2693+ "env"
2694+ ],
2695+ "string": {}
2696+ },
2697+ "string-runner-context-no-secrets": {
2698+ "context": [
2699+ "github",
2700+ "inputs",
2701+ "vars",
2702+ "needs",
2703+ "strategy",
2704+ "matrix",
2705+ "steps",
2706+ "job",
2707+ "runner",
2708+ "env"
2709+ ],
2710+ "string": {}
2711+ },
2712+ "string-steps-context": {
2713+ "context": [
2714+ "github",
2715+ "inputs",
2716+ "vars",
2717+ "needs",
2718+ "strategy",
2719+ "matrix",
2720+ "secrets",
2721+ "steps",
2722+ "job",
2723+ "runner",
2724+ "env",
2725+ "hashFiles(1,255)"
2726+ ],
2727+ "string": {}
2728+ },
2729+ "shell": {
2730+ "string": {
2731+ "require-non-empty": true
2732+ },
2733+ "description": "Use `shell` to override the default shell settings in the runner's operating system. You can use built-in shell keywords, or you can define a custom set of shell options. The shell command that is run internally executes a temporary file that contains the commands specified in `run`."
2734+ },
2735+ "working-directory": {
2736+ "string": {
2737+ "require-non-empty": true
2738+ },
2739+ "description": "The `working-directory` keyword specifies the working directory where the command is run."
2740+ },
2741+ "cron-mapping": {
2742+ "mapping": {
2743+ "properties": {
2744+ "cron": {
2745+ "type": "cron-pattern",
2746+ "required": true
2747+ },
2748+ "timezone": "timezone-string"
2749+ }
2750+ }
2751+ },
2752+ "cron-pattern": {
2753+ "description": "A cron expression that represents a schedule. A scheduled workflow will run at most once every 5 minutes.",
2754+ "string": {
2755+ "require-non-empty": true
2756+ }
2757+ },
2758+ "timezone-string": {
2759+ "description": "A string that represents the time zone a scheduled workflow will run relative to in IANA format (e.g. 'America/New_York' or 'Europe/London'). If omitted, the workflow will run relative to midnight UTC.",
2760+ "string": {
2761+ "require-non-empty": true
2762+ }
2763+ },
2764+ "service-container-mapping": {
2765+ "mapping": {
2766+ "properties": {
2767+ "image": {
2768+ "type": "non-empty-string",
2769+ "description": "The Docker image to use as the container. The value can be the Docker Hub image or a registry name."
2770+ },
2771+ "options": {
2772+ "type": "string",
2773+ "description": "Additional Docker container resource options."
2774+ },
2775+ "env": "container-env",
2776+ "ports": {
2777+ "type": "sequence-of-non-empty-string",
2778+ "description": "An array of ports to expose on the container."
2779+ },
2780+ "volumes": {
2781+ "type": "sequence-of-non-empty-string",
2782+ "description": "An array of volumes for the container to use. You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host."
2783+ },
2784+ "credentials": "container-registry-credentials",
2785+ "entrypoint": {
2786+ "type": "string",
2787+ "description": "Override the default ENTRYPOINT in the service container image."
2788+ },
2789+ "command": {
2790+ "type": "string",
2791+ "description": "Override the default CMD in the service container image."
2792+ }
2793+ }
2794+ }
2795+ }
2796+ }
2797+}
+1−0
1717 pub mod mask;
1818 pub mod matrix;
1919 pub mod permissions;
20+pub mod syntax;
2021 pub mod workflow;
+296−0
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.
8+pub const EXTENSION: &str = "g1t";
9+
10+/// The workflow's top level (`workflow-root-strict`).
11+pub 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`).
14+pub 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`).
36+pub const CALLER_JOB: &[&str] = &["name", "uses", "with", "secrets", "needs", "if", "permissions", "concurrency", "strategy"];
37+
38+/// A step that runs a command (`run-step`).
39+pub 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`).
43+pub 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.
48+pub const BACKGROUND_STEPS: &[&str] = &["background", "wait", "wait-all", "cancel", "parallel"];
49+
50+/// `strategy`.
51+pub const STRATEGY: &[&str] = &["fail-fast", "max-parallel", "matrix"];
52+
53+/// `runs-on` as a mapping (`runs-on-mapping`).
54+pub const RUNS_ON: &[&str] = &["group", "labels"];
55+
56+/// `container` as a mapping (`container-mapping`).
57+pub const CONTAINER: &[&str] = &["image", "options", "env", "ports", "volumes", "credentials"];
58+
59+/// Each of `services` as a mapping (`service-container-mapping`).
60+pub const SERVICE: &[&str] = &["image", "options", "env", "ports", "volumes", "command", "entrypoint", "credentials"];
61+
62+/// `credentials` of a container or service (`container-registry-credentials`).
63+pub const CREDENTIALS: &[&str] = &["username", "password"];
64+
65+/// `environment` as a mapping (`job-environment-mapping`).
66+pub const ENVIRONMENT: &[&str] = &["name", "url", "deployment"];
67+
68+/// `concurrency` as a mapping (`concurrency-mapping`).
69+pub const CONCURRENCY: &[&str] = &["group", "cancel-in-progress", "queue"];
70+
71+/// `defaults` (`workflow-defaults`, `job-defaults`).
72+pub const DEFAULTS: &[&str] = &["run"];
73+
74+/// `defaults.run` (`workflow-defaults-run`, `job-defaults-run`).
75+pub const DEFAULTS_RUN: &[&str] = &["shell", "working-directory"];
76+
77+/// `permissions` as a mapping (`permissions-mapping`).
78+pub 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`).
102+pub const CRON: &[&str] = &["cron", "timezone"];
103+
104+/// `on.workflow_dispatch` (`workflow-dispatch-mapping`).
105+pub const WORKFLOW_DISPATCH: &[&str] = &["inputs"];
106+
107+/// Each of `on.workflow_dispatch.inputs` (`workflow-dispatch-input`).
108+pub const DISPATCH_INPUT: &[&str] = &["description", "type", "required", "default", "options"];
109+
110+/// `on.workflow_call` (`workflow-call-mapping`).
111+pub const WORKFLOW_CALL: &[&str] = &["inputs", "outputs", "secrets"];
112+
113+/// Each of `on.workflow_call.inputs` (`workflow-call-input-definition`).
114+pub const CALL_INPUT: &[&str] = &["description", "type", "required", "default"];
115+
116+/// Each of `on.workflow_call.outputs` (`workflow-call-output-definition`).
117+pub const CALL_OUTPUT: &[&str] = &["description", "value"];
118+
119+/// Each of `on.workflow_call.secrets` (`workflow-call-secret-mapping-definition`).
120+pub const CALL_SECRET: &[&str] = &["description", "required"];
121+
122+/// What an event under `on` may be given.
123+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124+pub 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+
133+const TYPES: EventSpec = EventSpec::Filters(&["types"]);
134+const 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`).
137+pub 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.
178+pub 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.
185+pub 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)]
195+mod 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+}
+371−13
77
88 use crate::filter::{Filter, Patterns};
99 use crate::permissions::{self, Permissions};
10+use crate::syntax;
1011
1112 /// Where workflows live: GitHub's `.github/workflows`, under g1t's own
1213 /// folder, so moving a repository to g1t is renaming `.github` to `.g1t`.
114115 pub condition: Option<String>,
115116 pub uses: Option<String>,
116117 pub run: Option<String>,
118+ /// Its `g1t` settings, as written: g1t's own, beside GitHub's keys.
119+ pub g1t: Option<Value>,
117120 /// The whole step as written, for the sandbox.
118121 pub raw: Value,
119122 }
151154 /// Its own `concurrency`: at most one job of its group runs at a time.
152155 pub concurrency: Option<Concurrency>,
153156 pub steps: Vec<Step>,
157+ /// Its `g1t` settings, as written: g1t's own, beside GitHub's keys.
158+ pub g1t: Option<Value>,
154159 /// The whole job as written, for the sandbox.
155160 pub raw: Value,
156161 }
177182 pub permissions: Option<Permissions>,
178183 pub jobs: Vec<Job>,
179184 pub notes: Vec<Note>,
185+ /// Its top-level `g1t` settings, as written: g1t's own, beside
186+ /// GitHub's keys.
187+ pub g1t: Option<Value>,
180188 /// The whole workflow as written.
181189 pub raw: Value,
182190 }
302310
303311 /// Reads a workflow. `Err` is what is wrong with the file, for the person
304312 /// who wrote it; what reads but runs differently is in `notes`.
313+///
314+/// It accepts what GitHub accepts and refuses what GitHub refuses: every
315+/// mapping has only the keys GitHub's schema gives it ([`syntax`]), plus
316+/// `g1t`, g1t's own settings, on the workflow, a job and a step.
305317 pub fn parse(source: &str) -> Result<Workflow, String> {
306318 let yaml: serde_yaml::Value = serde_yaml::from_str(source).map_err(|error| format!("It is not valid YAML: {error}"))?;
319+ if has_merge_key(&yaml) {
320+ return Err("It uses a YAML merge key (`<<`), which GitHub does not read, so g1t does not either. Write the keys out, or give a whole value an anchor (`&name`) and repeat it with `*name`.".to_owned());
321+ }
307322 let raw = yaml_to_json(&yaml);
308323 let Value::Object(root) = &raw else {
309324 return Err("A workflow is a mapping with `on` and `jobs`.".to_owned());
310325 };
311326 let mut notes = Vec::new();
312327 let mut note = |severity, job: Option<&str>, message: String| notes.push(Note { severity, job: job.map(str::to_owned), message });
328+ check_keys(root, syntax::WORKFLOW, true, "", "the workflow")?;
313329
314− // `on`, in any of its three shapes. YAML 1.1 readers turn `on` into
315− // `true`; this reader keeps it, and accepts both.
316− let on = root.get("on").or_else(|| root.get("true")).ok_or("`on` is missing: say which events start the workflow.")?;
330+ // `on`, in any of its three shapes.
331+ let on = root.get("on").ok_or("`on` is missing: say which events start the workflow.")?;
332+ let known = |event: &str| syntax::event(event).ok_or_else(|| format!("`on`: GitHub has no `{event}` event, so neither does g1t."));
317333 let mut triggers = Vec::new();
318334 match on {
319− Value::String(event) => triggers.push(trigger(event, &Value::Null)),
335+ Value::String(event) => {
336+ known(event)?;
337+ triggers.push(trigger(event, &Value::Null));
338+ }
320339 Value::Array(events) => {
321340 for event in events {
322341 let Value::String(event) = event else { return Err("`on` lists event names.".to_owned()) };
342+ known(event)?;
323343 triggers.push(trigger(event, &Value::Null));
324344 }
325345 }
326346 Value::Object(events) => {
327347 for (event, spec) in events {
348+ check_event(event, known(event)?, spec, &mut note)?;
328349 triggers.push(trigger(event, spec));
329350 }
330351 }
337358 note(
338359 Severity::Unsupported,
339360 None,
340− format!("g1t has no `{}` event, so that trigger never starts it.", trigger.event),
361+ format!("g1t does not send GitHub's `{}` event, so that trigger never starts it.", trigger.event),
341362 );
342363 }
343364 if trigger.event == "pull_request_target" {
356377 Some(Value::Object(env)) => env.clone(),
357378 _ => Map::new(),
358379 };
380+ if let Some(defaults) = root.get("defaults") {
381+ check_defaults(defaults, "")?;
382+ }
383+ if let Some(concurrency) = root.get("concurrency") {
384+ check_concurrency(concurrency, "", None, &mut note)?;
385+ }
359386 let concurrency = concurrency_of(root.get("concurrency"));
360387 let permissions = match root.get("permissions") {
361388 None => None,
362389 Some(value) => {
390+ if let Value::Object(map) = value {
391+ check_keys(map, syntax::PERMISSIONS, false, "", "`permissions`")?;
392+ }
363393 let (permissions, unknown) = permissions::parse(value)?;
364394 permission_notes(&unknown, None, &mut note);
365395 Some(permissions)
373403 return Err("`jobs` is empty: a workflow needs at least one job.".to_owned());
374404 }
375405 let mut jobs = Vec::new();
406+ let mut seen_ids: Vec<String> = Vec::new();
376407 for (id, spec) in job_specs {
408+ if !syntax::valid_id(id) {
409+ return Err(format!("`{id}` is not a job id GitHub accepts: {ID_RULE}"));
410+ }
411+ let lower = id.to_ascii_lowercase();
412+ if let Some(index) = seen_ids.iter().position(|seen| *seen == lower) {
413+ let other = job_specs.keys().nth(index).cloned().unwrap_or_default();
414+ return Err(format!("Job `{id}` is already defined as `{other}`: GitHub compares job ids without case."));
415+ }
416+ seen_ids.push(lower);
377417 let Value::Object(spec) = spec else {
378418 return Err(format!("Job `{id}` is a mapping."));
379419 };
420+ let place = format!("Job `{id}`");
380421 let uses = text(spec.get("uses"));
422+ if spec.contains_key("uses") {
423+ for key in ["runs-on", "steps"] {
424+ if spec.contains_key(key) {
425+ return Err(format!("Job `{id}` calls a reusable workflow with `uses`, so it has no `{key}`: the called workflow's jobs say that."));
426+ }
427+ }
428+ check_keys(spec, syntax::CALLER_JOB, true, &place, "a job that calls a reusable workflow")?;
429+ if uses.as_deref().is_none_or(|uses| uses.trim().is_empty()) {
430+ return Err(format!("Job `{id}`: `uses` names the reusable workflow it calls."));
431+ }
432+ match spec.get("secrets") {
433+ None | Some(Value::Object(_)) => {}
434+ Some(Value::String(inherit)) if inherit == "inherit" => {}
435+ Some(_) => return Err(format!("Job `{id}`: `secrets` is a mapping of the secrets it passes, or `inherit`.")),
436+ }
437+ } else {
438+ check_keys(spec, syntax::JOB, true, &place, "a job")?;
439+ if !spec.contains_key("runs-on") {
440+ return Err(format!("Job `{id}` has no `runs-on`: say which runner it runs on, such as `runs-on: ubuntu-latest`."));
441+ }
442+ }
443+ check_job_parts(id, spec, &mut note)?;
381444 let steps_raw = match spec.get("steps") {
382445 Some(Value::Array(steps)) => steps.clone(),
383446 None if uses.is_some() => Vec::new(),
391454 // own runners, which may be Linux, macOS or Windows.
392455 let self_hosted = runs_on.get("group").is_some() || labels.iter().any(|label| label.eq_ignore_ascii_case("self-hosted"));
393456 let mut steps = Vec::new();
457+ let mut step_ids: Vec<String> = Vec::new();
394458 for (index, step) in steps_raw.iter().enumerate() {
459+ let place = format!("Job `{id}`, step {}", index + 1);
395460 let Value::Object(fields) = step else {
396− return Err(format!("Job `{id}`, step {}: a step is a mapping.", index + 1));
461+ return Err(format!("{place}: a step is a mapping."));
397462 };
463+ if let Some(key) = syntax::BACKGROUND_STEPS.iter().find(|key| fields.contains_key(**key)) {
464+ return Err(format!(
465+ "{place}: `{key}` is one of GitHub's background steps (`background`, `wait`, `wait-all`, `cancel`, `parallel`), which g1t does not run yet."
466+ ));
467+ }
398468 let step = Step {
399469 id: text(fields.get("id")),
400470 name: text(fields.get("name")),
401471 condition: text(fields.get("if")),
402472 uses: text(fields.get("uses")),
403473 run: text(fields.get("run")),
474+ g1t: fields.get(syntax::EXTENSION).cloned(),
404475 raw: step.clone(),
405476 };
406477 match (&step.uses, &step.run) {
407− (Some(_), Some(_)) => return Err(format!("Job `{id}`, step {}: a step has `uses` or `run`, not both.", index + 1)),
408− (None, None) => return Err(format!("Job `{id}`, step {}: a step needs `uses` or `run`.", index + 1)),
409− _ => {}
478+ (Some(_), Some(_)) => return Err(format!("{place}: a step has `uses` or `run`, not both.")),
479+ (None, None) => return Err(format!("{place}: a step needs `uses` or `run`.")),
480+ (Some(_), None) => check_keys(fields, syntax::ACTION_STEP, true, &place, "a step that uses an action")?,
481+ (None, Some(_)) => check_keys(fields, syntax::RUN_STEP, true, &place, "a step that runs a command")?,
482+ }
483+ if let Some(step_id) = &step.id {
484+ if !syntax::valid_id(step_id) {
485+ return Err(format!("{place}: `{step_id}` is not a step id GitHub accepts: {ID_RULE}"));
486+ }
487+ let lower = step_id.to_ascii_lowercase();
488+ if step_ids.contains(&lower) {
489+ return Err(format!("{place}: another step of job `{id}` has the id `{step_id}` already (GitHub compares step ids without case)."));
490+ }
491+ step_ids.push(lower);
410492 }
411493 if let Some(uses) = &step.uses
412494 && let Some((severity, message)) = action_note(uses)
474556 let job_permissions = match spec.get("permissions") {
475557 None => None,
476558 Some(value) => {
559+ if let Value::Object(map) = value {
560+ check_keys(map, syntax::PERMISSIONS, false, &place, "`permissions`")?;
561+ }
477562 let (permissions, unknown) = permissions::parse(value).map_err(|problem| format!("Job `{id}`: {problem}"))?;
478563 permission_notes(&unknown, Some(id), &mut note);
479564 Some(permissions)
509594 permissions: job_permissions,
510595 concurrency: concurrency_of(spec.get("concurrency")),
511596 steps,
597+ g1t: spec.get(syntax::EXTENSION).cloned(),
512598 raw: Value::Object(spec.clone()),
513599 });
514600 }
528614 permissions,
529615 jobs,
530616 notes,
617+ g1t: root.get(syntax::EXTENSION).cloned(),
531618 raw,
532619 };
533620 if workflow.job_order().len() < workflow.jobs.len() {
536623 Ok(workflow)
537624 }
538625
626+/// Whether a job (as written) records a deployment to its environment:
627+/// unless it says `deployment: false`, in its `environment` as on GitHub,
628+/// or in its `g1t` settings.
629+pub fn records_deployment(job: &Value) -> bool {
630+ let off = |value: Option<&Value>| match value {
631+ Some(Value::Bool(flag)) => !flag,
632+ Some(Value::String(text)) => text.trim() == "false",
633+ _ => false,
634+ };
635+ !off(job.get("environment").and_then(|environment| environment.get("deployment")))
636+ && !off(job.get(syntax::EXTENSION).and_then(|settings| settings.get("deployment")))
637+}
638+
639+/// GitHub's rule for job and step ids, as said when one breaks it.
640+const ID_RULE: &str = "it starts with a letter or `_`, has only letters, digits, `-` and `_`, is under 100 characters, and does not start with `__`.";
641+
642+/// Whether a YAML merge key (`<<`) is anywhere in it.
643+fn has_merge_key(value: &serde_yaml::Value) -> bool {
644+ match value {
645+ serde_yaml::Value::Mapping(map) => map.iter().any(|(key, value)| key.as_str() == Some("<<") || has_merge_key(value)),
646+ serde_yaml::Value::Sequence(items) => items.iter().any(has_merge_key),
647+ serde_yaml::Value::Tagged(tagged) => has_merge_key(&tagged.value),
648+ _ => false,
649+ }
650+}
651+
652+/// Keys as a sentence: `a`, `b` or `c`.
653+fn listed(keys: &[&str]) -> String {
654+ let quoted: Vec<String> = keys.iter().map(|key| format!("`{key}`")).collect();
655+ match quoted.split_last() {
656+ None => String::new(),
657+ Some((last, [])) => last.clone(),
658+ Some((last, rest)) => format!("{} or {last}", rest.join(", ")),
659+ }
660+}
661+
662+/// `message`, said of `place` ("Job `build`, step 2"), or on its own for
663+/// the workflow's top level.
664+fn at(place: &str, message: &str) -> String {
665+ if place.is_empty() {
666+ let mut chars = message.chars();
667+ chars.next().map(|first| first.to_uppercase().chain(chars).collect()).unwrap_or_default()
668+ } else {
669+ format!("{place}: {message}")
670+ }
671+}
672+
673+/// Refuses any key of `map` that is not `allowed`, as GitHub does
674+/// ("Unexpected value"). With `extension`, `g1t` is allowed too, as a
675+/// mapping of anything. `what` names the mapping in the message.
676+fn check_keys(map: &Map<String, Value>, allowed: &[&str], extension: bool, place: &str, what: &str) -> Result<(), String> {
677+ for (key, value) in map {
678+ if extension && key == syntax::EXTENSION {
679+ if !value.is_object() {
680+ return Err(at(place, "`g1t` is a mapping of g1t's own settings."));
681+ }
682+ continue;
683+ }
684+ if !allowed.contains(&key.as_str()) {
685+ return Err(at(place, &format!("unexpected `{key}` in {what}; it can have {}.", listed(allowed))));
686+ }
687+ }
688+ Ok(())
689+}
690+
691+/// Refuses a mapping without `key`.
692+fn require(map: &Map<String, Value>, key: &str, place: &str, what: &str) -> Result<(), String> {
693+ if map.contains_key(key) { Ok(()) } else { Err(at(place, &format!("{what} needs `{key}`."))) }
694+}
695+
696+/// One event under `on` and what it is given.
697+fn check_event(event: &str, kind: syntax::EventSpec, spec: &Value, note: &mut impl FnMut(Severity, Option<&str>, String)) -> Result<(), String> {
698+ let what = format!("`on.{event}`");
699+ match (kind, spec) {
700+ (syntax::EventSpec::Schedule, Value::Array(entries)) => {
701+ for entry in entries {
702+ let Value::Object(entry) = entry else { return Err("`on.schedule` is a list of `cron:` entries.".to_owned()) };
703+ check_keys(entry, syntax::CRON, false, "", "an entry of `on.schedule`")?;
704+ require(entry, "cron", "", "Each entry of `on.schedule`")?;
705+ if entry.contains_key("timezone") {
706+ note(
707+ Severity::Warning,
708+ None,
709+ "`on.schedule`: g1t reads every `cron` in UTC and does not read `timezone` yet, so the schedule runs at those times in UTC.".to_owned(),
710+ );
711+ }
712+ }
713+ Ok(())
714+ }
715+ (syntax::EventSpec::Schedule, _) => Err("`on.schedule` is a list of `cron:` entries.".to_owned()),
716+ (_, Value::Null) => Ok(()),
717+ (syntax::EventSpec::Bare, _) => Err(format!("{what} takes no settings: write `{event}:` with nothing after it.")),
718+ (syntax::EventSpec::Filters(keys), Value::Object(map)) => {
719+ check_keys(map, keys, false, "", &what)?;
720+ for only in ["branches", "tags", "paths"] {
721+ let ignore = format!("{only}-ignore");
722+ if map.contains_key(only) && map.contains_key(&ignore) {
723+ return Err(format!(
724+ "{what} has both `{only}` and `{ignore}`: GitHub takes one or the other for an event. To leave some out of `{only}`, start a pattern with `!`."
725+ ));
726+ }
727+ }
728+ let definitions = |key: &str| -> Result<Vec<(&String, &Value)>, String> {
729+ match map.get(key) {
730+ None | Some(Value::Null) => Ok(Vec::new()),
731+ Some(Value::Object(entries)) => Ok(entries.iter().collect()),
732+ Some(_) => Err(format!("`on.{event}.{key}` is a mapping of names to their definitions.")),
733+ }
734+ };
735+ if event == "workflow_dispatch" {
736+ for (name, input) in definitions("inputs")? {
737+ match input {
738+ Value::Null => {}
739+ Value::Object(input) => check_keys(input, syntax::DISPATCH_INPUT, false, "", &format!("input `{name}` of {what}"))?,
740+ _ => return Err(format!("Input `{name}` of {what} is a mapping.")),
741+ }
742+ }
743+ }
744+ if event == "workflow_call" {
745+ for (name, input) in definitions("inputs")? {
746+ let Value::Object(input) = input else { return Err(format!("Input `{name}` of {what} is a mapping with its `type`.")) };
747+ let place = format!("input `{name}` of {what}");
748+ check_keys(input, syntax::CALL_INPUT, false, "", &place)?;
749+ require(input, "type", "", &format!("Input `{name}` of {what}"))?;
750+ }
751+ for (name, output) in definitions("outputs")? {
752+ let Value::Object(output) = output else { return Err(format!("Output `{name}` of {what} is a mapping with its `value`.")) };
753+ check_keys(output, syntax::CALL_OUTPUT, false, "", &format!("output `{name}` of {what}"))?;
754+ require(output, "value", "", &format!("Output `{name}` of {what}"))?;
755+ }
756+ for (name, secret) in definitions("secrets")? {
757+ match secret {
758+ Value::Null => {}
759+ Value::Object(secret) => check_keys(secret, syntax::CALL_SECRET, false, "", &format!("secret `{name}` of {what}"))?,
760+ _ => return Err(format!("Secret `{name}` of {what} is a mapping, or nothing.")),
761+ }
762+ }
763+ }
764+ Ok(())
765+ }
766+ (syntax::EventSpec::Filters(_), _) => Err(format!("{what} is a mapping of its filters, or nothing.")),
767+ }
768+}
769+
770+/// `defaults`, at the top level or on a job.
771+fn check_defaults(value: &Value, place: &str) -> Result<(), String> {
772+ let Value::Object(defaults) = value else { return Err(at(place, "`defaults` is a mapping with `run`.")) };
773+ check_keys(defaults, syntax::DEFAULTS, false, place, "`defaults`")?;
774+ match defaults.get("run") {
775+ None | Some(Value::Null) => Ok(()),
776+ Some(Value::Object(run)) => check_keys(run, syntax::DEFAULTS_RUN, false, place, "`defaults.run`"),
777+ Some(_) => Err(at(place, "`defaults.run` is a mapping with `shell` or `working-directory`.")),
778+ }
779+}
780+
781+/// `concurrency`, at the top level or on a job (`job`).
782+fn check_concurrency(value: &Value, place: &str, job: Option<&str>, note: &mut impl FnMut(Severity, Option<&str>, String)) -> Result<(), String> {
783+ let Value::Object(concurrency) = value else { return Ok(()) };
784+ check_keys(concurrency, syntax::CONCURRENCY, false, place, "`concurrency`")?;
785+ require(concurrency, "group", place, "`concurrency`")?;
786+ if let Some(queue) = concurrency.get("queue")
787+ && text(Some(queue)).as_deref() != Some("single")
788+ {
789+ note(
790+ Severity::Warning,
791+ job,
792+ "`concurrency.queue`: g1t keeps one run or job waiting in a group, as `queue: single` does, and does not read `queue` yet.".to_owned(),
793+ );
794+ }
795+ Ok(())
796+}
797+
798+/// A job's mappings other than its steps: each has only GitHub's keys.
799+fn check_job_parts(id: &str, spec: &Map<String, Value>, note: &mut impl FnMut(Severity, Option<&str>, String)) -> Result<(), String> {
800+ let place = format!("Job `{id}`");
801+ let credentials = |container: &Map<String, Value>, what: &str| -> Result<(), String> {
802+ match container.get("credentials") {
803+ Some(Value::Object(credentials)) => check_keys(credentials, syntax::CREDENTIALS, false, &place, &format!("the `credentials` of {what}")),
804+ _ => Ok(()),
805+ }
806+ };
807+ if let Some(Value::Object(runs_on)) = spec.get("runs-on") {
808+ check_keys(runs_on, syntax::RUNS_ON, false, &place, "`runs-on`")?;
809+ }
810+ if let Some(Value::Object(strategy)) = spec.get("strategy") {
811+ check_keys(strategy, syntax::STRATEGY, false, &place, "`strategy`")?;
812+ }
813+ if let Some(Value::Object(container)) = spec.get("container") {
814+ check_keys(container, syntax::CONTAINER, false, &place, "`container`")?;
815+ credentials(container, "`container`")?;
816+ }
817+ if let Some(Value::Object(services)) = spec.get("services") {
818+ for (name, service) in services {
819+ if let Value::Object(service) = service {
820+ let what = format!("service `{name}`");
821+ check_keys(service, syntax::SERVICE, false, &place, &what)?;
822+ credentials(service, &what)?;
823+ }
824+ }
825+ }
826+ if let Some(Value::Object(environment)) = spec.get("environment") {
827+ check_keys(environment, syntax::ENVIRONMENT, false, &place, "`environment`")?;
828+ require(environment, "name", &place, "`environment`")?;
829+ }
830+ if let Some(concurrency) = spec.get("concurrency") {
831+ check_concurrency(concurrency, &place, Some(id), note)?;
832+ }
833+ if let Some(defaults) = spec.get("defaults") {
834+ check_defaults(defaults, &place)?;
835+ }
836+ if spec.contains_key("snapshot") {
837+ note(Severity::Unsupported, Some(id), "`snapshot`: g1t does not make custom runner images from a job yet, so the job runs and no image is made.".to_owned());
838+ }
839+ if spec.contains_key("cancel-timeout-minutes") {
840+ note(Severity::Warning, Some(id), "`cancel-timeout-minutes`: g1t does not read it yet, so a cancelled job stops in its usual time.".to_owned());
841+ }
842+ Ok(())
843+}
844+
539845 /// `concurrency`, as a group's name or a mapping with `group` and
540846 /// `cancel-in-progress`.
541847 fn concurrency_of(value: Option<&Value>) -> Option<Concurrency> {
555861 /// How long a job may run when it names no `timeout-minutes`, as on GitHub.
556862 pub const DEFAULT_TIMEOUT_MINUTES: u32 = 360;
557863
558−/// What to say about `permissions` names the token does not have.
864+/// What to say about `permissions` names GitHub has that g1t's token has
865+/// nothing behind.
559866 fn permission_notes(unknown: &[String], job: Option<&str>, note: &mut impl FnMut(Severity, Option<&str>, String)) {
560867 for name in unknown {
561− note(Severity::Warning, job, format!("`permissions.{name}`: the token has no permission called that, so it grants nothing."));
868+ note(Severity::Warning, job, format!("`permissions.{name}`: g1t's token has nothing behind that permission yet, so it grants nothing."));
562869 }
563870 }
564871
7661073 assert_eq!(bare.jobs[0].permissions(&bare, TokenDefault::Restricted).get("issues"), Access::None);
7671074 assert_eq!(bare.jobs[0].permissions(&bare, TokenDefault::Permissive).get("issues"), Access::Write);
7681075 // What reads but grants nothing is said.
769− let odd = parse("on: push\npermissions: { id-token: write, wiki: read }\njobs:\n a:\n runs-on: x\n steps: [{ run: 'true' }]").unwrap();
1076+ let odd = parse("on: push\npermissions: { id-token: write, artifact-metadata: read }\njobs:\n a:\n runs-on: x\n steps: [{ run: 'true' }]").unwrap();
7701077 assert!(!odd.notes.iter().any(|n| n.message.contains("id-token")), "OIDC tokens are issued");
771− assert!(odd.notes.iter().any(|n| n.message.contains("`permissions.wiki`")));
1078+ assert!(odd.notes.iter().any(|n| n.severity == Severity::Warning && n.message.contains("`permissions.artifact-metadata`")));
1079+ // A name GitHub's token does not have is refused, as GitHub does;
1080+ // `metadata` is always `read` and cannot be written.
1081+ for name in ["wiki", "metadata"] {
1082+ let problem = parse(&format!("on: push\npermissions: {{ {name}: read }}\njobs:\n a:\n runs-on: x\n steps: [{{ run: 'true' }}]")).unwrap_err();
1083+ assert!(problem.contains(&format!("Unexpected `{name}` in `permissions`")), "{problem}");
1084+ }
7721085 assert!(parse("on: push\npermissions: read\njobs:\n a:\n runs-on: x\n steps: [{ run: 'true' }]").unwrap_err().contains("read-all"));
7731086 assert!(
7741087 parse("on: push\njobs:\n a:\n runs-on: x\n permissions: { contents: admin }\n steps: [{ run: 'true' }]")
8271140 assert!(problem("on: push\njobs: [1]").contains("`jobs`"));
8281141 assert!(problem(": : :").contains("not valid YAML"));
8291142 }
1143+
1144+ #[test]
1145+ fn only_githubs_keys_and_g1ts_own() {
1146+ let problem = |yaml: &str| parse(yaml).unwrap_err();
1147+ let job = "\njobs:\n build:\n runs-on: x\n steps: [{ run: 'true' }]";
1148+ assert!(problem(&format!("on: push\nrun_name: x{job}")).starts_with("Unexpected `run_name` in the workflow; it can have `on`, "));
1149+ assert!(problem("on: push\njobs:\n build:\n runs-on: x\n step: []").starts_with("Job `build`: unexpected `step` in a job;"));
1150+ let step = problem("on: push\njobs:\n build:\n runs-on: x\n steps:\n - run: 'true'\n - uses: actions/checkout@v5\n shell: bash");
1151+ assert!(step.starts_with("Job `build`, step 2: unexpected `shell` in a step that uses an action;"), "{step}");
1152+ assert!(problem("on: push\njobs:\n build:\n steps: [{ run: 'true' }]").contains("no `runs-on`"));
1153+ assert!(problem("on: push\njobs:\n build:\n uses: ./.g1t/workflows/b.yml\n runs-on: x").contains("no `runs-on`"));
1154+ assert!(problem("on: push\njobs:\n my.job:\n runs-on: x\n steps: [{ run: 'true' }]").contains("`my.job` is not a job id"));
1155+ assert!(problem("on: pushh\njobs:\n a:\n runs-on: x\n steps: [{ run: 'true' }]").contains("no `pushh` event"));
1156+
1157+ // `g1t` is g1t's own, at each level, and kept as written.
1158+ let workflow = parse(
1159+ "on: push
1160+g1t: { note: top }
1161+jobs:
1162+ deploy:
1163+ runs-on: x
1164+ environment: production
1165+ g1t: { deployment: false }
1166+ steps:
1167+ - run: 'true'
1168+ g1t: { anything: [1, 2] }",
1169+ )
1170+ .unwrap();
1171+ assert_eq!(workflow.g1t, Some(serde_json::json!({ "note": "top" })));
1172+ assert_eq!(workflow.jobs[0].g1t, Some(serde_json::json!({ "deployment": false })));
1173+ assert_eq!(workflow.jobs[0].steps[0].g1t, Some(serde_json::json!({ "anything": [1, 2] })));
1174+ assert!(problem(&format!("on: push\ng1t: yes{job}")).contains("`g1t` is a mapping"));
1175+ assert!(!records_deployment(&workflow.jobs[0].raw));
1176+ assert!(!records_deployment(&serde_json::json!({ "environment": { "name": "p", "deployment": false } })));
1177+ assert!(records_deployment(&serde_json::json!({ "environment": { "name": "p", "deployment": true } })));
1178+ assert!(records_deployment(&serde_json::json!({ "environment": "p" })));
1179+ }
1180+
1181+ #[test]
1182+ fn aliases_read_and_merge_keys_do_not() {
1183+ let aliased = parse("on: push\njobs:\n a:\n runs-on: x\n steps: &steps\n - run: 'true'\n b:\n runs-on: x\n steps: *steps").unwrap();
1184+ assert_eq!(aliased.jobs[1].steps.len(), 1);
1185+ let merged = parse("on: push\njobs:\n a: &job\n runs-on: x\n steps: [{ run: 'true' }]\n b:\n <<: *job\n name: B").unwrap_err();
1186+ assert!(merged.contains("merge key"), "{merged}");
1187+ }
8301188 }
+59−0
1+//! Workflows as people write them for GitHub (`tests/corpus/valid`), each
2+//! of which g1t reads, and workflows GitHub refuses
3+//! (`tests/corpus/invalid`), each of which g1t refuses too, with the
4+//! problem its first line names (`# error: ...`). Every file was written
5+//! for this test; the refused ones follow the cases in GitHub's own parser
6+//! tests (actions/languageservices, `workflow-parser/testdata/reader`).
7+
8+use std::path::{Path, PathBuf};
9+
10+use g1t_actions::workflow;
11+
12+fn files(kind: &str) -> Vec<(String, String)> {
13+ let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/corpus").join(kind);
14+ let mut files: Vec<(String, String)> = std::fs::read_dir(&dir)
15+ .unwrap()
16+ .map(|entry| entry.unwrap().path())
17+ .filter(|path| path.extension().and_then(|e| e.to_str()) == Some("yml"))
18+ .map(|path: PathBuf| (name(&path), std::fs::read_to_string(&path).unwrap()))
19+ .collect();
20+ files.sort();
21+ files
22+}
23+
24+fn name(path: &Path) -> String {
25+ path.file_name().unwrap().to_string_lossy().into_owned()
26+}
27+
28+#[test]
29+fn every_github_workflow_reads() {
30+ let files = files("valid");
31+ assert!(files.len() >= 40, "{}", files.len());
32+ let mut problems = Vec::new();
33+ for (name, source) in &files {
34+ if let Err(problem) = workflow::parse(source) {
35+ problems.push(format!("{name}: {problem}"));
36+ }
37+ }
38+ assert!(problems.is_empty(), "{}", problems.join("\n"));
39+}
40+
41+#[test]
42+fn what_github_refuses_is_refused() {
43+ let files = files("invalid");
44+ assert!(files.len() >= 40, "{}", files.len());
45+ let mut problems = Vec::new();
46+ for (name, source) in &files {
47+ let expected = source
48+ .lines()
49+ .next()
50+ .and_then(|line| line.strip_prefix("# error: "))
51+ .unwrap_or_else(|| panic!("{name}: its first line is `# error: <what the problem says>`"));
52+ match workflow::parse(source) {
53+ Ok(_) => problems.push(format!("{name}: reads, and should not")),
54+ Err(problem) if !problem.contains(expected) => problems.push(format!("{name}: says {problem:?}, not {expected:?}")),
55+ Err(_) => {}
56+ }
57+ }
58+ assert!(problems.is_empty(), "{}", problems.join("\n"));
59+}
+9−0
1+# error: `background` is one of GitHub's background steps
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - id: server
8+ run: npm start
9+ background: true
+9−0
1+# error: `on.create` takes no settings
2+on:
3+ create:
4+ branches: [main]
5+jobs:
6+ build:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - run: echo hi
+10−0
1+# error: `on.push` has both `branches` and `branches-ignore`
2+on:
3+ push:
4+ branches: [main]
5+ branches-ignore: [dev]
6+jobs:
7+ build:
8+ runs-on: ubuntu-latest
9+ steps:
10+ - run: echo hi
+11−0
1+# error: Input `username` of `on.workflow_call` needs `type`
2+on:
3+ workflow_call:
4+ inputs:
5+ username:
6+ default: john-doe
7+jobs:
8+ build:
9+ runs-on: ubuntu-latest
10+ steps:
11+ - run: echo hi
+13−0
1+# error: Unexpected `deprecationMessage` in input `app_name` of `on.workflow_call`
2+on:
3+ workflow_call:
4+ inputs:
5+ app_name:
6+ required: true
7+ type: string
8+ deprecationMessage: blah
9+jobs:
10+ build:
11+ runs-on: ubuntu-latest
12+ steps:
13+ - run: echo hi
+11−0
1+# error: Output `artifact` of `on.workflow_call` needs `value`
2+on:
3+ workflow_call:
4+ outputs:
5+ artifact:
6+ description: no value
7+jobs:
8+ build:
9+ runs-on: ubuntu-latest
10+ steps:
11+ - run: echo hi
+11−0
1+# error: Unexpected `default` in secret `token` of `on.workflow_call`
2+on:
3+ workflow_call:
4+ secrets:
5+ token:
6+ default: abc
7+jobs:
8+ build:
9+ runs-on: ubuntu-latest
10+ steps:
11+ - run: echo hi
+7−0
1+# error: unexpected `env` in a job that calls a reusable workflow
2+on: push
3+jobs:
4+ call:
5+ uses: ./.github/workflows/foo.yml
6+ env:
7+ A: b
+6−0
1+# error: `secrets` is a mapping of the secrets it passes, or `inherit`
2+on: push
3+jobs:
4+ call:
5+ uses: ./.github/workflows/foo.yml
6+ secrets: all
+10−0
1+# error: Unexpected `cancel_in_progress` in `concurrency`
2+on: push
3+concurrency:
4+ group: ci
5+ cancel_in_progress: true
6+jobs:
7+ build:
8+ runs-on: ubuntu-latest
9+ steps:
10+ - run: echo hi
+9−0
1+# error: `concurrency` needs `group`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ concurrency:
7+ cancel-in-progress: true
8+ steps:
9+ - run: echo hi
+10−0
1+# error: unexpected `args` in `container`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ container:
7+ image: node:22
8+ args: --rm
9+ steps:
10+ - run: echo hi
+11−0
1+# error: unexpected `token` in the `credentials` of `container`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ container:
7+ image: ghcr.io/a/b
8+ credentials:
9+ token: abc
10+ steps:
11+ - run: echo hi
+10−0
1+# error: unexpected `env` in `defaults.run`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ defaults:
7+ run:
8+ env: {}
9+ steps:
10+ - run: echo hi
+9−0
1+# error: Unexpected `shell` in `defaults`
2+on: push
3+defaults:
4+ shell: bash
5+jobs:
6+ build:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - run: echo hi
+11−0
1+# error: Unexpected `another_unknown_value` in input `name` of `on.workflow_dispatch`
2+on:
3+ workflow_dispatch:
4+ inputs:
5+ name:
6+ another_unknown_value: 123
7+jobs:
8+ build:
9+ runs-on: ubuntu-latest
10+ steps:
11+ - run: echo hi
+9−0
1+# error: Unexpected `unknown_value` in `on.workflow_dispatch`
2+on:
3+ workflow_dispatch:
4+ unknown_value: test
5+jobs:
6+ build:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - run: echo hi
+10−0
1+# error: unexpected `reviewers` in `environment`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ environment:
7+ name: production
8+ reviewers: [a]
9+ steps:
10+ - run: echo hi
+9−0
1+# error: `environment` needs `name`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ environment:
7+ url: https://example.com
8+ steps:
9+ - run: echo hi
+8−0
1+# error: `g1t` is a mapping of g1t's own settings
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ g1t: true
7+ steps:
8+ - run: echo hi
+9−0
1+# error: unexpected `g1t` in `strategy`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ strategy:
7+ g1t: {}
8+ steps:
9+ - run: echo hi
+11−0
1+# error: Job `MY-JOB` is already defined as `my-job`
2+on: push
3+jobs:
4+ my-job:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - run: echo hi
8+ MY-JOB:
9+ runs-on: ubuntu-latest
10+ steps:
11+ - run: echo hi
+7−0
1+# error: `0job` is not a job id GitHub accepts
2+on: push
3+jobs:
4+ 0job:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - run: echo hi
+7−0
1+# error: `my.job` is not a job id GitHub accepts
2+on: push
3+jobs:
4+ my.job:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - run: echo hi
+7−0
1+# error: `__internal` is not a job id GitHub accepts
2+on: push
3+jobs:
4+ __internal:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - run: echo hi
+13−0
1+# error: YAML merge key (`<<`)
2+on: push
3+x-env: &a
4+ A: 1
5+x-more: &b
6+ B: 2
7+jobs:
8+ build:
9+ runs-on: ubuntu-latest
10+ env:
11+ <<: [*a, *b]
12+ steps:
13+ - run: echo hi
+10−0
1+# error: YAML merge key (`<<`)
2+on: push
3+jobs:
4+ build: &build
5+ runs-on: ubuntu-latest
6+ steps:
7+ - run: echo hi
8+ build2:
9+ <<: *build
10+ name: Second
+6−0
1+# error: `on` is missing
2+jobs:
3+ build:
4+ runs-on: ubuntu-latest
5+ steps:
6+ - run: echo hi
+9−0
1+# error: Job `build`: unexpected `metadata` in `permissions`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ permissions:
7+ metadata: read
8+ steps:
9+ - run: echo hi
+9−0
1+# error: Unexpected `wiki` in `permissions`
2+on: push
3+permissions:
4+ wiki: write
5+jobs:
6+ build:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - run: echo hi
+9−0
1+# error: Unexpected `branch` in `on.push`
2+on:
3+ push:
4+ branch: main
5+jobs:
6+ build:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - run: echo hi
+11−0
1+# error: Unexpected `inputs` in `on.push`
2+on:
3+ push:
4+ inputs:
5+ name:
6+ type: string
7+jobs:
8+ build:
9+ runs-on: ubuntu-latest
10+ steps:
11+ - run: echo hi
+9−0
1+# error: Unexpected `types` in `on.push`
2+on:
3+ push:
4+ types: [created]
5+jobs:
6+ build:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - run: echo hi
+8−0
1+# error: Unexpected `true` in the workflow
2+on: push
3+true: 1
4+jobs:
5+ build:
6+ runs-on: ubuntu-latest
7+ steps:
8+ - run: echo hi
+8−0
1+# error: unexpected `label` in `runs-on`
2+on: push
3+jobs:
4+ build:
5+ runs-on:
6+ label: big
7+ steps:
8+ - run: echo hi
+6−0
1+# error: Job `build` has no `runs-on`
2+on: push
3+jobs:
4+ build:
5+ steps:
6+ - run: echo hi
+9−0
1+# error: `on.schedule` is a list of `cron:` entries
2+on:
3+ schedule:
4+ cron: '0 0 * * *'
5+jobs:
6+ build:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - run: echo hi
+9−0
1+# error: Each entry of `on.schedule` needs `cron`
2+on:
3+ schedule:
4+ - timezone: Europe/Paris
5+jobs:
6+ build:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - run: echo hi
+11−0
1+# error: unexpected `healthcheck` in service `db`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ services:
7+ db:
8+ image: postgres
9+ healthcheck: pg_isready
10+ steps:
11+ - run: echo hi
+8−0
1+# error: unexpected `shell` in a step that uses an action
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - uses: actions/checkout@v4
8+ shell: bash
+10−0
1+# error: Job `build`, step 2: another step of job `build` has the id `STEP1`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - id: step1
8+ run: echo hi
9+ - id: STEP1
10+ run: echo hi
+8−0
1+# error: `-step!` is not a step id GitHub accepts
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - id: -step!
8+ run: echo hi
+8−0
1+# error: a step has `uses` or `run`, not both
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - uses: actions/checkout@v4
8+ run: echo hi
+7−0
1+# error: `steps` is a list
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ not: a-sequence
+11−0
1+# error: Job `build`: unexpected `fail_fast` in `strategy`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ strategy:
7+ fail_fast: false
8+ matrix:
9+ a: [1]
10+ steps:
11+ - run: echo hi
+7−0
1+# error: GitHub has no `pull-request` event
2+on: [push, pull-request]
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - run: echo hi
+9−0
1+# error: GitHub has no `commit` event
2+on:
3+ commit:
4+ branches: [main]
5+jobs:
6+ build:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - run: echo hi
+7−0
1+# error: `on`: GitHub has no `pushh` event
2+on: pushh
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - run: echo hi
+8−0
1+# error: Job `build`: unexpected `run-on` in a job
2+on: push
3+jobs:
4+ build:
5+ run-on: ubuntu-latest
6+ runs-on: ubuntu-latest
7+ steps:
8+ - run: echo hi
+9−0
1+# error: Unexpected `job` in the workflow
2+on: push
3+job:
4+ build: {}
5+jobs:
6+ build:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - run: echo hi
+9−0
1+# error: Job `build`, step 1: unexpected `width` in a step that uses an action
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - uses: actions/checkout@v4
8+ width:
9+ fetch-depth: 0
+6−0
1+# error: calls a reusable workflow with `uses`, so it has no `runs-on`
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ uses: ./.github/workflows/foo.yml
+7−0
1+# error: so it has no `steps`
2+on: push
3+jobs:
4+ build:
5+ uses: ./.github/workflows/foo.yml
6+ steps:
7+ - run: echo hi
+7−0
1+# error: `wait` is one of GitHub's background steps
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - wait: server
+9−0
1+# error: unexpected `with` in a step that runs a command
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - run: echo hi
8+ with:
9+ a: b
+19−0
1+name: Anchors and aliases
2+on:
3+ push:
4+ branches: &branches [main, 'release/**']
5+ pull_request:
6+ branches: *branches
7+jobs:
8+ test:
9+ runs-on: ubuntu-latest
10+ env: &env
11+ CI: "true"
12+ LANG: C.UTF-8
13+ steps: &steps
14+ - uses: actions/checkout@v4
15+ - run: make test
16+ test-again:
17+ runs-on: ubuntu-latest
18+ env: *env
19+ steps: *steps
+20−0
1+name: Artifacts
2+on: [push]
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - run: mkdir -p out && echo hello > out/a.txt
8+ - uses: actions/upload-artifact@v4
9+ with:
10+ name: out
11+ path: out/
12+ if-no-files-found: error
13+ use:
14+ needs: build
15+ runs-on: ubuntu-latest
16+ steps:
17+ - uses: actions/download-artifact@v4
18+ with:
19+ name: out
20+ - run: cat a.txt
+15−0
1+name: Not on release branches
2+on:
3+ push:
4+ branches-ignore:
5+ - 'release/**'
6+ paths-ignore:
7+ - '**/*.md'
8+ - 'docs/**'
9+ pull_request:
10+ branches-ignore: [gh-pages]
11+jobs:
12+ build:
13+ runs-on: ubuntu-latest
14+ steps:
15+ - run: echo build
+31−0
1+name: Caching
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - uses: actions/checkout@v4
8+ - name: Cache node modules
9+ id: cache-npm
10+ uses: actions/cache@v4
11+ env:
12+ cache-name: cache-node-modules
13+ with:
14+ path: ~/.npm
15+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
16+ restore-keys: |
17+ ${{ runner.os }}-build-${{ env.cache-name }}-
18+ ${{ runner.os }}-build-
19+ ${{ runner.os }}-
20+ - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
21+ name: List the state of node modules
22+ continue-on-error: true
23+ run: npm list
24+ - uses: actions/cache/restore@v4
25+ with:
26+ path: dist
27+ key: dist-${{ github.sha }}
28+ - uses: actions/cache/save@v4
29+ with:
30+ path: dist
31+ key: dist-${{ github.sha }}
+43−0
1+name: "CodeQL"
2+on:
3+ push:
4+ branches: [ "main" ]
5+ pull_request:
6+ branches: [ "main" ]
7+ schedule:
8+ - cron: '36 1 * * 3'
9+jobs:
10+ analyze:
11+ name: Analyze (${{ matrix.language }})
12+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
13+ timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
14+ permissions:
15+ security-events: write
16+ packages: read
17+ actions: read
18+ contents: read
19+ strategy:
20+ fail-fast: false
21+ matrix:
22+ include:
23+ - language: javascript-typescript
24+ build-mode: none
25+ - language: python
26+ build-mode: none
27+ steps:
28+ - name: Checkout repository
29+ uses: actions/checkout@v4
30+ - name: Initialize CodeQL
31+ uses: github/codeql-action/init@v3
32+ with:
33+ languages: ${{ matrix.language }}
34+ build-mode: ${{ matrix.build-mode }}
35+ - if: matrix.build-mode == 'manual'
36+ shell: bash
37+ run: |
38+ echo 'Replace this with your build commands'
39+ exit 1
40+ - name: Perform CodeQL Analysis
41+ uses: github/codeql-action/analyze@v3
42+ with:
43+ category: "/language:${{matrix.language}}"
+20−0
1+name: Concurrency
2+on:
3+ pull_request:
4+ types: [opened, synchronize, reopened, ready_for_review]
5+concurrency:
6+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
7+ cancel-in-progress: true
8+jobs:
9+ lint:
10+ runs-on: ubuntu-latest
11+ concurrency: lint-${{ github.ref }}
12+ steps:
13+ - run: echo lint
14+ deploy-preview:
15+ runs-on: ubuntu-latest
16+ concurrency:
17+ group: preview-${{ github.head_ref }}
18+ cancel-in-progress: ${{ github.event.action != 'closed' }}
19+ steps:
20+ - run: echo preview
+30−0
1+name: In a container
2+on: push
3+jobs:
4+ build:
5+ runs-on: ubuntu-latest
6+ container:
7+ image: ghcr.io/example/builder:1.4
8+ credentials:
9+ username: ${{ github.actor }}
10+ password: ${{ secrets.GITHUB_TOKEN }}
11+ env:
12+ NODE_ENV: development
13+ ports:
14+ - 80
15+ volumes:
16+ - my_docker_volume:/volume_mount
17+ options: --cpus 1
18+ services:
19+ cache:
20+ image: memcached:1.6
21+ command: ["memcached", "-m", "64"]
22+ entrypoint: /usr/local/bin/docker-entrypoint.sh
23+ steps:
24+ - uses: actions/checkout@v4
25+ - run: make
26+ short-container:
27+ runs-on: ubuntu-latest
28+ container: node:22-bookworm
29+ steps:
30+ - run: node --version
+20−0
1+name: Defaults
2+on: push
3+defaults:
4+ run:
5+ shell: bash
6+ working-directory: ./app
7+jobs:
8+ build:
9+ runs-on: ubuntu-latest
10+ defaults:
11+ run:
12+ working-directory: ./app/web
13+ steps:
14+ - uses: actions/checkout@v4
15+ - run: pwd
16+ - run: ls
17+ working-directory: ./
18+ shell: sh -e {0}
19+ - run: print("hi")
20+ shell: python
+21−0
1+name: Dependabot auto-merge
2+on: pull_request
3+permissions:
4+ contents: write
5+ pull-requests: write
6+jobs:
7+ dependabot:
8+ runs-on: ubuntu-latest
9+ if: github.event.pull_request.user.login == 'dependabot[bot]'
10+ steps:
11+ - name: Dependabot metadata
12+ id: metadata
13+ uses: dependabot/fetch-metadata@v2
14+ with:
15+ github-token: "${{ secrets.GITHUB_TOKEN }}"
16+ - name: Enable auto-merge for Dependabot PRs
17+ if: steps.metadata.outputs.update-type == 'semver-patch'
18+ run: gh pr merge --auto --merge "$PR_URL"
19+ env:
20+ PR_URL: ${{github.event.pull_request.html_url}}
21+ GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
+16−0
1+name: Described
2+description: A workflow with a description, which GitHub's schema allows.
3+on:
4+ push:
5+env:
6+ GLOBAL: one
7+ NUMBER: 2
8+ FLAG: true
9+ FROM_VAR: ${{ vars.SOMETHING }}
10+jobs:
11+ print:
12+ runs-on: ubuntu-latest
13+ env:
14+ JOB_LEVEL: ${{ env.GLOBAL }}-job
15+ steps:
16+ - run: env | sort
+14−0
1+name: Docker actions
2+on: push
3+jobs:
4+ docker:
5+ runs-on: ubuntu-latest
6+ steps:
7+ - uses: docker://alpine:3.20
8+ with:
9+ entrypoint: /bin/echo
10+ args: hello from alpine
11+ - uses: ./.github/actions/local-action
12+ with:
13+ who-to-greet: Mona
14+ - uses: actions/aws/ec2@main
+46−0
1+name: Docker
2+on:
3+ schedule:
4+ - cron: '27 4 * * *'
5+ push:
6+ branches: [ "main" ]
7+ tags: [ 'v*.*.*' ]
8+ pull_request:
9+ branches: [ "main" ]
10+env:
11+ REGISTRY: ghcr.io
12+ IMAGE_NAME: ${{ github.repository }}
13+jobs:
14+ build:
15+ runs-on: ubuntu-latest
16+ permissions:
17+ contents: read
18+ packages: write
19+ id-token: write
20+ steps:
21+ - name: Checkout repository
22+ uses: actions/checkout@v4
23+ - name: Set up Docker Buildx
24+ uses: docker/setup-buildx-action@v3
25+ - name: Log into registry ${{ env.REGISTRY }}
26+ if: github.event_name != 'pull_request'
27+ uses: docker/login-action@v3
28+ with:
29+ registry: ${{ env.REGISTRY }}
30+ username: ${{ github.actor }}
31+ password: ${{ secrets.GITHUB_TOKEN }}
32+ - name: Extract Docker metadata
33+ id: meta
34+ uses: docker/metadata-action@v5
35+ with:
36+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
37+ - name: Build and push Docker image
38+ id: build-and-push
39+ uses: docker/build-push-action@v6
40+ with:
41+ context: .
42+ push: ${{ github.event_name != 'pull_request' }}
43+ tags: ${{ steps.meta.outputs.tags }}
44+ labels: ${{ steps.meta.outputs.labels }}
45+ cache-from: type=gha
46+ cache-to: type=gha,mode=max
+28−0
1+name: Environments
2+on:
3+ push:
4+ branches: [main]
5+jobs:
6+ staging:
7+ runs-on: ubuntu-latest
8+ environment:
9+ name: staging
10+ url: https://staging.example.com
11+ steps:
12+ - run: ./deploy.sh staging
13+ plan:
14+ runs-on: ubuntu-latest
15+ environment:
16+ name: production
17+ deployment: false
18+ steps:
19+ - run: ./plan.sh
20+ production:
21+ needs: [staging, plan]
22+ runs-on: ubuntu-latest
23+ environment:
24+ name: production
25+ url: ${{ steps.deploy.outputs.url }}
26+ steps:
27+ - id: deploy
28+ run: echo "url=https://example.com" >> "$GITHUB_OUTPUT"
+14−0
1+name: g1t settings
2+on: push
3+g1t:
4+ anything: goes here
5+jobs:
6+ plan:
7+ runs-on: ubuntu-latest
8+ environment: production
9+ g1t:
10+ deployment: false
11+ steps:
12+ - run: echo plan
13+ g1t:
14+ note: kept as written
+25−0
1+name: Go
2+on:
3+ push:
4+ branches: [ "main" ]
5+ tags: [ "v*" ]
6+ pull_request:
7+ branches: [ "main" ]
8+jobs:
9+ build:
10+ runs-on: ubuntu-latest
11+ steps:
12+ - uses: actions/checkout@v4
13+ - name: Set up Go
14+ uses: actions/setup-go@v5
15+ with:
16+ go-version-file: go.mod
17+ - name: Build
18+ run: go build -v ./...
19+ - name: Test
20+ run: go test -race -coverprofile=coverage.out ./...
21+ - uses: actions/upload-artifact@v4
22+ with:
23+ name: coverage
24+ path: coverage.out
25+ retention-days: 5
+17−0
1+name: Conditions
2+on:
3+ pull_request:
4+ types: [labeled, unlabeled, opened, synchronize]
5+jobs:
6+ labeled:
7+ if: contains(github.event.pull_request.labels.*.name, 'deploy')
8+ runs-on: ubuntu-latest
9+ steps:
10+ - if: ${{ github.event.action == 'labeled' && github.event.label.name == 'deploy' }}
11+ run: echo just labeled
12+ - if: failure()
13+ run: echo failed
14+ - if: cancelled()
15+ run: echo cancelled
16+ - if: "!startsWith(github.head_ref, 'renovate/')"
17+ run: echo not renovate
+20−0
1+name: Comment commands
2+on:
3+ issue_comment:
4+ types: [created, edited]
5+ issues:
6+ types: [opened, labeled]
7+jobs:
8+ respond:
9+ if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/retest') }}
10+ runs-on: ubuntu-latest
11+ steps:
12+ - uses: actions/github-script@v7
13+ with:
14+ script: |
15+ await github.rest.reactions.createForIssueComment({
16+ owner: context.repo.owner,
17+ repo: context.repo.repo,
18+ comment_id: context.payload.comment.id,
19+ content: 'rocket',
20+ })
+33−0
1+name: Java CI with Gradle
2+on:
3+ push:
4+ branches: [ "main" ]
5+ pull_request:
6+ branches: [ "main" ]
7+jobs:
8+ build:
9+ runs-on: ubuntu-latest
10+ permissions:
11+ contents: read
12+ steps:
13+ - uses: actions/checkout@v4
14+ - name: Set up JDK 21
15+ uses: actions/setup-java@v4
16+ with:
17+ java-version: '21'
18+ distribution: 'temurin'
19+ - name: Setup Gradle
20+ uses: gradle/actions/setup-gradle@v4
21+ - name: Build with Gradle Wrapper
22+ run: ./gradlew build
23+ dependency-submission:
24+ runs-on: ubuntu-latest
25+ permissions:
26+ contents: write
27+ steps:
28+ - uses: actions/checkout@v4
29+ - uses: actions/setup-java@v4
30+ with:
31+ java-version: '21'
32+ distribution: 'temurin'
33+ - uses: gradle/actions/dependency-submission@v4
+27−0
1+name: Job options
2+on: push
3+jobs:
4+ first:
5+ name: The first job
6+ runs-on: ubuntu-latest
7+ timeout-minutes: 15
8+ continue-on-error: false
9+ env:
10+ LEVEL: job
11+ steps:
12+ - run: echo "$LEVEL"
13+ second:
14+ name: ${{ format('Second for {0}', github.ref_name) }}
15+ needs: first
16+ if: github.event_name == 'push'
17+ runs-on: ubuntu-latest
18+ steps:
19+ - run: echo second
20+ _private_helper:
21+ runs-on: ubuntu-latest
22+ steps:
23+ - run: echo underscore ids are allowed
24+ Mixed-Case_2:
25+ runs-on: ubuntu-latest
26+ steps:
27+ - run: echo mixed
+12−0
1+name: Labeler
2+on: [pull_request_target]
3+jobs:
4+ label:
5+ runs-on: ubuntu-latest
6+ permissions:
7+ contents: read
8+ pull-requests: write
9+ steps:
10+ - uses: actions/labeler@v5
11+ with:
12+ repo-token: "${{ secrets.GITHUB_TOKEN }}"
+40−0
1+name: Many events
2+on:
3+ branch_protection_rule:
4+ types: [created, deleted]
5+ check_run:
6+ types: [rerequested, completed]
7+ check_suite:
8+ types: [completed]
9+ create:
10+ delete:
11+ deployment:
12+ deployment_status:
13+ discussion:
14+ types: [created, edited, answered]
15+ discussion_comment:
16+ types: [created, deleted]
17+ fork:
18+ gollum:
19+ label:
20+ types: [created, deleted]
21+ milestone:
22+ types: [opened, deleted]
23+ page_build:
24+ public:
25+ pull_request_review:
26+ types: [submitted, edited]
27+ pull_request_review_comment:
28+ types: [created, deleted]
29+ registry_package:
30+ types: [published, updated]
31+ release:
32+ types: [published]
33+ status:
34+ watch:
35+ types: [started]
36+jobs:
37+ log:
38+ runs-on: ubuntu-latest
39+ steps:
40+ - run: echo "${{ github.event_name }}"
+26−0
1+name: Dynamic matrix
2+on: push
3+jobs:
4+ plan:
5+ runs-on: ubuntu-latest
6+ outputs:
7+ matrix: ${{ steps.set.outputs.matrix }}
8+ steps:
9+ - id: set
10+ run: echo 'matrix={"include":[{"project":"foo","config":"Debug"},{"project":"bar","config":"Release"}]}' >> "$GITHUB_OUTPUT"
11+ build:
12+ needs: plan
13+ runs-on: ubuntu-latest
14+ strategy:
15+ matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
16+ steps:
17+ - run: echo "${{ matrix.project }} ${{ matrix.config }}"
18+ whole-strategy:
19+ needs: plan
20+ runs-on: ubuntu-latest
21+ strategy:
22+ fail-fast: ${{ github.event_name == 'push' }}
23+ matrix:
24+ include: ${{ fromJSON(needs.plan.outputs.matrix).include }}
25+ steps:
26+ - run: echo ok
+20−0
1+name: Bounded matrix
2+on: push
3+jobs:
4+ shard:
5+ runs-on: ubuntu-latest
6+ strategy:
7+ max-parallel: 2
8+ fail-fast: true
9+ matrix:
10+ shard: [1, 2, 3, 4]
11+ flavour: [a, b]
12+ exclude:
13+ - shard: 4
14+ flavour: b
15+ include:
16+ - shard: 5
17+ flavour: c
18+ extra: true
19+ steps:
20+ - run: echo "${{ matrix.shard }}/${{ matrix.flavour }}"
+12−0
1+name: Merge queue
2+on:
3+ pull_request:
4+ merge_group:
5+ types: [checks_requested]
6+ branches: [main]
7+jobs:
8+ check:
9+ runs-on: ubuntu-latest
10+ steps:
11+ - uses: actions/checkout@v4
12+ - run: make check
+19−0
1+name: 'Quoting: every style'
2+'on':
3+ "push":
4+ 'branches':
5+ - "main"
6+jobs:
7+ "quoted-job":
8+ 'runs-on': "ubuntu-latest"
9+ steps:
10+ - name: Folded
11+ run: >
12+ echo one
13+ two three
14+ - name: Literal keep
15+ run: |+
16+ echo kept
17+
18+ - name: Flow
19+ run: "echo \"quoted\" && echo 'single'"
+27−0
1+name: Outputs
2+on: push
3+jobs:
4+ job1:
5+ runs-on: ubuntu-latest
6+ outputs:
7+ output1: ${{ steps.step1.outputs.test }}
8+ output2: ${{ steps.step2.outputs.test }}
9+ steps:
10+ - id: step1
11+ run: echo "test=hello" >> "$GITHUB_OUTPUT"
12+ - id: step2
13+ run: echo "test=world" >> "$GITHUB_OUTPUT"
14+ job2:
15+ runs-on: ubuntu-latest
16+ needs: job1
17+ steps:
18+ - env:
19+ OUTPUT1: ${{needs.job1.outputs.output1}}
20+ OUTPUT2: ${{needs.job1.outputs.output2}}
21+ run: echo "$OUTPUT1 $OUTPUT2"
22+ job3:
23+ runs-on: ubuntu-latest
24+ needs: [job1, job2]
25+ if: ${{ always() && !cancelled() }}
26+ steps:
27+ - run: echo done
+23−0
1+name: Node CI
2+on: [push, pull_request]
3+jobs:
4+ build:
5+ runs-on: ${{ matrix.os }}
6+ strategy:
7+ matrix:
8+ os: [ubuntu-latest, ubuntu-22.04]
9+ node-version: [18.x, 20.x, 22.x]
10+ exclude:
11+ - os: ubuntu-22.04
12+ node-version: 18.x
13+ max-parallel: 4
14+ steps:
15+ - uses: actions/checkout@v4
16+ - name: Use Node.js ${{ matrix.node-version }}
17+ uses: actions/setup-node@v4
18+ with:
19+ node-version: ${{ matrix.node-version }}
20+ cache: npm
21+ - run: npm ci
22+ - run: npm run build --if-present
23+ - run: npm test
+10−0
1+on:
2+ push:
3+ pull_request:
4+ workflow_dispatch:
5+ workflow_call:
6+jobs:
7+ noop:
8+ runs-on: ubuntu-latest
9+ steps:
10+ - run: "true"
+30−0
1+name: Deploy static content to Pages
2+on:
3+ push:
4+ branches: ["main"]
5+ workflow_dispatch:
6+permissions:
7+ contents: read
8+ pages: write
9+ id-token: write
10+concurrency:
11+ group: "pages"
12+ cancel-in-progress: false
13+jobs:
14+ deploy:
15+ environment:
16+ name: github-pages
17+ url: ${{ steps.deployment.outputs.page_url }}
18+ runs-on: ubuntu-latest
19+ steps:
20+ - name: Checkout
21+ uses: actions/checkout@v4
22+ - name: Setup Pages
23+ uses: actions/configure-pages@v5
24+ - name: Upload artifact
25+ uses: actions/upload-pages-artifact@v3
26+ with:
27+ path: '.'
28+ - name: Deploy to GitHub Pages
29+ id: deployment
30+ uses: actions/deploy-pages@v4
+34−0
1+name: Permissions
2+on: push
3+permissions: read-all
4+jobs:
5+ write:
6+ runs-on: ubuntu-latest
7+ permissions: write-all
8+ steps:
9+ - run: echo write
10+ none:
11+ runs-on: ubuntu-latest
12+ permissions: {}
13+ steps:
14+ - run: echo none
15+ many:
16+ runs-on: ubuntu-latest
17+ permissions:
18+ actions: read
19+ attestations: write
20+ checks: write
21+ contents: read
22+ deployments: write
23+ discussions: read
24+ id-token: write
25+ issues: write
26+ models: read
27+ packages: read
28+ pages: write
29+ pull-requests: write
30+ repository-projects: read
31+ security-events: write
32+ statuses: write
33+ steps:
34+ - run: echo many
+12−0
1+name: Review events
2+on:
3+ pull_request_review:
4+ types: [submitted]
5+ pull_request_review_comment:
6+ types: [created]
7+jobs:
8+ approved:
9+ if: github.event.review.state == 'approved'
10+ runs-on: ubuntu-latest
11+ steps:
12+ - run: echo "approved by ${{ github.event.review.user.login }}"
+16−0
1+name: Docs preview
2+on:
3+ pull_request_target:
4+ types: [opened, synchronize, closed]
5+ branches:
6+ - main
7+ - 'release/**'
8+ paths:
9+ - 'docs/**'
10+jobs:
11+ preview:
12+ runs-on: ubuntu-latest
13+ permissions:
14+ pull-requests: write
15+ steps:
16+ - run: echo "PR ${{ github.event.number }}"
+30−0
1+name: Publish package
2+on:
3+ release:
4+ types: [created]
5+jobs:
6+ build:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - uses: actions/checkout@v4
10+ - uses: actions/setup-node@v4
11+ with:
12+ node-version: 22
13+ - run: npm ci
14+ - run: npm test
15+ publish-npm:
16+ needs: build
17+ runs-on: ubuntu-latest
18+ permissions:
19+ contents: read
20+ id-token: write
21+ steps:
22+ - uses: actions/checkout@v4
23+ - uses: actions/setup-node@v4
24+ with:
25+ node-version: 22
26+ registry-url: https://registry.npmjs.org/
27+ - run: npm ci
28+ - run: npm publish --provenance
29+ env:
30+ NODE_AUTH_TOKEN: ${{secrets.npm_token}}
+31−0
1+name: Python package
2+on:
3+ push:
4+ branches: ["main"]
5+ pull_request:
6+ branches: ["main"]
7+permissions:
8+ contents: read
9+jobs:
10+ build:
11+ runs-on: ubuntu-latest
12+ strategy:
13+ fail-fast: false
14+ matrix:
15+ python-version: ["3.10", "3.11", "3.12", "3.13"]
16+ steps:
17+ - uses: actions/checkout@v4
18+ - name: Set up Python ${{ matrix.python-version }}
19+ uses: actions/setup-python@v5
20+ with:
21+ python-version: ${{ matrix.python-version }}
22+ - name: Install dependencies
23+ run: |
24+ python -m pip install --upgrade pip
25+ python -m pip install flake8 pytest
26+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
27+ - name: Lint with flake8
28+ run: |
29+ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
30+ - name: Test with pytest
31+ run: pytest
+19−0
1+name: Release
2+on:
3+ push:
4+ tags:
5+ - "v[0-9]+.[0-9]+.[0-9]+"
6+permissions:
7+ contents: write
8+jobs:
9+ release:
10+ runs-on: ubuntu-latest
11+ steps:
12+ - uses: actions/checkout@v4
13+ with:
14+ fetch-depth: 0
15+ - name: Create release
16+ env:
17+ GH_TOKEN: ${{ github.token }}
18+ TAG: ${{ github.ref_name }}
19+ run: gh release create "$TAG" --generate-notes
+9−0
1+name: Dispatch
2+on:
3+ repository_dispatch:
4+ types: [deploy, rebuild]
5+jobs:
6+ run:
7+ runs-on: ubuntu-latest
8+ steps:
9+ - run: echo "${{ github.event.client_payload.ref }}"
+35−0
1+name: Reusable build
2+on:
3+ workflow_call:
4+ inputs:
5+ config-path:
6+ description: Where the configuration is
7+ required: true
8+ type: string
9+ debug:
10+ type: boolean
11+ default: false
12+ retries:
13+ type: number
14+ default: 3
15+ secrets:
16+ token:
17+ description: A token to publish with
18+ required: true
19+ optional-secret:
20+ outputs:
21+ artifact:
22+ description: The artifact that was built
23+ value: ${{ jobs.build.outputs.artifact }}
24+jobs:
25+ build:
26+ runs-on: ubuntu-latest
27+ outputs:
28+ artifact: ${{ steps.pack.outputs.name }}
29+ steps:
30+ - uses: actions/checkout@v4
31+ - id: pack
32+ run: echo "name=build-${{ github.sha }}" >> "$GITHUB_OUTPUT"
33+ env:
34+ CONFIG: ${{ inputs.config-path }}
35+ TOKEN: ${{ secrets.token }}
+29−0
1+name: Call reusable
2+on:
3+ push:
4+ branches: [main]
5+jobs:
6+ call-local:
7+ uses: ./.github/workflows/reusable-called.yml
8+ with:
9+ config-path: .github/labeler.yml
10+ debug: true
11+ secrets:
12+ token: ${{ secrets.PUBLISH_TOKEN }}
13+ call-remote:
14+ needs: call-local
15+ if: ${{ always() }}
16+ uses: octo-org/example-repo/.github/workflows/reusable.yml@v1
17+ secrets: inherit
18+ permissions:
19+ contents: read
20+ strategy:
21+ matrix:
22+ target: [dev, stage]
23+ concurrency:
24+ group: remote-${{ matrix.target }}
25+ report:
26+ needs: [call-local]
27+ runs-on: ubuntu-latest
28+ steps:
29+ - run: echo "${{ needs.call-local.outputs.artifact }}"
+21−0
1+name: Runner forms
2+on: push
3+jobs:
4+ label:
5+ runs-on: ubuntu-24.04
6+ steps: [{ run: uname -a }]
7+ labels:
8+ runs-on: [self-hosted, linux, x64]
9+ steps: [{ run: uname -a }]
10+ group:
11+ runs-on:
12+ group: ubuntu-runners
13+ labels: ubuntu-20.04-16core
14+ steps: [{ run: uname -a }]
15+ group-only:
16+ runs-on:
17+ group: large
18+ steps: [{ run: uname -a }]
19+ expression:
20+ runs-on: ${{ github.repository == 'example/app' && 'big-runner' || 'ubuntu-latest' }}
21+ steps: [{ run: uname -a }]
+30−0
1+name: Rust
2+on:
3+ push:
4+ branches: [main]
5+ pull_request:
6+env:
7+ CARGO_TERM_COLOR: always
8+ RUSTFLAGS: -D warnings
9+jobs:
10+ test:
11+ name: Test (${{ matrix.toolchain }})
12+ runs-on: ubuntu-latest
13+ strategy:
14+ fail-fast: false
15+ matrix:
16+ toolchain: [stable, beta, nightly]
17+ include:
18+ - toolchain: nightly
19+ experimental: true
20+ continue-on-error: ${{ matrix.experimental == true }}
21+ steps:
22+ - uses: actions/checkout@v4
23+ - uses: dtolnay/rust-toolchain@master
24+ with:
25+ toolchain: ${{ matrix.toolchain }}
26+ components: clippy, rustfmt
27+ - uses: Swatinem/rust-cache@v2
28+ - run: cargo fmt --all -- --check
29+ - run: cargo clippy --all-targets
30+ - run: cargo test --all-features
+18−0
1+name: Build with a wrapper
2+on:
3+ push:
4+ branches: [main]
5+ pull_request:
6+jobs:
7+ rust:
8+ runs-on: ubuntu-latest
9+ env:
10+ SCCACHE_GHA_ENABLED: "true"
11+ RUSTC_WRAPPER: sccache
12+ steps:
13+ - uses: actions/checkout@v4
14+ - uses: mozilla-actions/sccache-action@v0.0.9
15+ - run: cargo build --locked --release
16+ - name: Statistics
17+ if: always()
18+ run: sccache --show-stats
+15−0
1+name: Nightly
2+on:
3+ schedule:
4+ - cron: "0 2 * * *"
5+ - cron: "30 14 * * 1-5"
6+ workflow_dispatch:
7+jobs:
8+ nightly:
9+ runs-on: ubuntu-latest
10+ if: github.repository_owner == 'example'
11+ steps:
12+ - uses: actions/checkout@v4
13+ with:
14+ ref: main
15+ - run: make nightly
+32−0
1+name: Integration
2+on:
3+ pull_request:
4+ paths:
5+ - "src/**"
6+ - "migrations/**"
7+jobs:
8+ integration:
9+ runs-on: ubuntu-latest
10+ timeout-minutes: 30
11+ services:
12+ postgres:
13+ image: postgres:16
14+ env:
15+ POSTGRES_PASSWORD: postgres
16+ POSTGRES_DB: app_test
17+ ports:
18+ - 5432:5432
19+ options: >-
20+ --health-cmd pg_isready
21+ --health-interval 10s
22+ --health-timeout 5s
23+ --health-retries 5
24+ redis:
25+ image: redis:7
26+ ports: ["6379:6379"]
27+ steps:
28+ - uses: actions/checkout@v4
29+ - run: ./scripts/test-integration.sh
30+ env:
31+ DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test
32+ REDIS_URL: redis://localhost:6379
+6−0
1+on: workflow_dispatch
2+jobs:
3+ hello:
4+ runs-on: ubuntu-latest
5+ steps:
6+ - run: echo hello
+18−0
1+name: Mark stale issues and pull requests
2+on:
3+ schedule:
4+ - cron: '21 3 * * *'
5+jobs:
6+ stale:
7+ runs-on: ubuntu-latest
8+ permissions:
9+ issues: write
10+ pull-requests: write
11+ steps:
12+ - uses: actions/stale@v9
13+ with:
14+ repo-token: ${{ secrets.GITHUB_TOKEN }}
15+ stale-issue-message: 'Stale issue message'
16+ stale-pr-message: 'Stale pull request message'
17+ stale-issue-label: 'no-issue-activity'
18+ days-before-stale: 60
+31−0
1+name: Step options
2+on: push
3+jobs:
4+ steps:
5+ runs-on: ubuntu-latest
6+ timeout-minutes: 10
7+ steps:
8+ - name: Long one
9+ id: long
10+ if: ${{ success() }}
11+ timeout-minutes: 5
12+ continue-on-error: ${{ github.ref != 'refs/heads/main' }}
13+ env:
14+ A: 1
15+ B: "two"
16+ working-directory: src
17+ shell: bash
18+ run: |
19+ echo "$A $B"
20+ echo "done=true" >> "$GITHUB_OUTPUT"
21+ - name: With action
22+ id: act
23+ if: always()
24+ timeout-minutes: 2
25+ continue-on-error: true
26+ uses: actions/github-script@v7
27+ env:
28+ C: three
29+ with:
30+ script: console.log(process.env.C)
31+ - run: echo "${{ steps.long.outputs.done }}"
+18−0
1+name: Both
2+on:
3+ workflow_call:
4+ inputs:
5+ target:
6+ type: string
7+ required: false
8+ default: all
9+ workflow_dispatch:
10+ inputs:
11+ target:
12+ type: string
13+ default: all
14+jobs:
15+ run:
16+ runs-on: ubuntu-latest
17+ steps:
18+ - run: echo "${{ inputs.target }}"
+32−0
1+name: Manual deploy
2+run-name: Deploy to ${{ inputs.environment }} by @${{ github.actor }}
3+on:
4+ workflow_dispatch:
5+ inputs:
6+ environment:
7+ description: Where to deploy
8+ type: environment
9+ required: true
10+ log-level:
11+ description: Log level
12+ required: true
13+ default: warning
14+ type: choice
15+ options:
16+ - info
17+ - warning
18+ - debug
19+ dry-run:
20+ type: boolean
21+ default: true
22+ count:
23+ type: number
24+ default: 1
25+ notes:
26+ description: Free text
27+jobs:
28+ deploy:
29+ runs-on: ubuntu-latest
30+ environment: ${{ inputs.environment }}
31+ steps:
32+ - run: echo "Deploying with ${{ inputs.log-level }}"
+17−0
1+name: After CI
2+on:
3+ workflow_run:
4+ workflows: ["CI", "Build"]
5+ types: [completed]
6+ branches: [main]
7+jobs:
8+ on-success:
9+ runs-on: ubuntu-latest
10+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
11+ steps:
12+ - run: echo 'The triggering workflow passed'
13+ on-failure:
14+ runs-on: ubuntu-latest
15+ if: ${{ github.event.workflow_run.conclusion == 'failure' }}
16+ steps:
17+ - run: echo 'The triggering workflow failed'
+6−10
425425 (!text.is_empty()).then_some(text)
426426 };
427427 match raw.get("environment")? {
428− name @ Value::String(_) => Some(JobEnvironment { name: plain(name)?, url: None, deploys: true }),
428+ name @ Value::String(_) => Some(JobEnvironment { name: plain(name)?, url: None, deploys: workflow::records_deployment(raw) }),
429429 Value::Object(env) => {
430430 let name = plain(env.get("name")?)?;
431431 let url = env
432432 .get("url")
433433 .and_then(plain)
434434 .filter(|url| url.starts_with("https://") || url.starts_with("http://"));
435− let deploys = !matches!(env.get("deployment"), Some(Value::Bool(false)))
436− && !matches!(env.get("deployment"), Some(Value::String(text)) if text.trim() == "false");
437− Some(JobEnvironment { name, url, deploys })
435+ Some(JobEnvironment { name, url, deploys: workflow::records_deployment(raw) })
438436 }
439437 _ => None,
440438 }
467465 let env = match (read, &job.environment) {
468466 (Some(env), Some(name)) => Some(JobEnvironment { name: name.clone(), ..env }),
469467 (Some(env), None) => Some(env),
470− (None, Some(name)) => {
471− let deployment = spec.raw.get("environment").and_then(|env| env.get("deployment"));
472− let deploys = !matches!(deployment, Some(Value::Bool(false)))
473− && !matches!(deployment, Some(Value::String(text)) if text.trim() == "false");
474− Some(JobEnvironment { name: name.clone(), url: None, deploys })
475− }
468+ (None, Some(name)) => Some(JobEnvironment { name: name.clone(), url: None, deploys: workflow::records_deployment(&spec.raw) }),
476469 (None, None) => None,
477470 };
478471 env.filter(|env| env.deploys)
27392732 );
27402733 // Secrets only: no deployment.
27412734 assert!(!read(json!({ "environment": { "name": "production", "deployment": false } })).unwrap().deploys);
2735+ // The same, said in the job's g1t settings.
2736+ assert!(!read(json!({ "environment": "production", "g1t": { "deployment": false } })).unwrap().deploys);
2737+ assert!(!read(json!({ "environment": { "name": "production" }, "g1t": { "deployment": false } })).unwrap().deploys);
27422738 // Only http(s) addresses.
27432739 assert_eq!(read(json!({ "environment": { "name": "production", "url": "javascript:alert(1)" } })).unwrap().url, None);
27442740 }