Skip to content

g1t/crates/contracts/src/actions.rs

1,203 lines42,686 bytesCodeBlame
1//! The actions service: GitHub Actions workflows, run on g1t as they are.
2//!
3//! A repository's `.g1t/workflows/*.yml`, in GitHub's format, are read
4//! from the commit an
5//! event is about (the default branch for issues, schedules and manual
6//! runs). Each workflow an event starts becomes a run; each job of the run
7//! (one per matrix combination) runs in a sandbox once the jobs it needs
8//! have finished. Jobs report their steps and logs back as they go, and a
9//! run on a pull request's head is a status on that pull request.
10//!
11//! Secrets and variables belong to a repository or to its workspace; a
12//! repository's override its workspace's of the same name. Secret values
13//! are sealed at rest and never returned.
14//!
15//! Mirrors `packages/contracts/src/actions.ts`.
16
17use serde::{Deserialize, Serialize};
18use serde_json::Value;
19
20use crate::repos::RepoPath;
21use crate::{User, Viewer};
22
23/// A note on something in a workflow that runs differently on g1t.
24#[derive(Clone, Debug, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct WorkflowNote {
27 /// `info`, `warning` or `unsupported`.
28 pub severity: String,
29 pub job: Option<String>,
30 pub message: String,
31}
32
33#[derive(Clone, Debug, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct Workflow {
36 pub id: String,
37 /// `.g1t/workflows/ci.yml`.
38 pub path: String,
39 pub name: String,
40 /// The events that start it, such as `push` and `pull_request`.
41 pub events: Vec<String>,
42 /// `active`, or `disabled` when a member turned it off.
43 pub state: String,
44 /// Why the file cannot be used, if it cannot.
45 pub error: Option<String>,
46 pub notes: Vec<WorkflowNote>,
47 /// `on.workflow_dispatch.inputs` as written, when it can be run by hand.
48 pub dispatch: Option<Value>,
49 pub last_run: Option<WorkflowRun>,
50}
51
52#[derive(Clone, Debug, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct WorkflowRun {
55 pub id: String,
56 pub workflow_id: String,
57 pub path: String,
58 /// The workflow's name.
59 pub name: String,
60 /// `run-name`, or what started it: a commit's subject, a pull request's title.
61 pub title: String,
62 /// Counts the workflow's runs: 1, 2, 3…
63 pub number: u64,
64 pub attempt: u64,
65 /// The GitHub event: `push`, `pull_request`, `schedule`…
66 pub event: String,
67 #[serde(rename = "ref")]
68 pub git_ref: String,
69 pub sha: String,
70 /// The pull request it ran for, if any.
71 pub pull: Option<u32>,
72 /// `queued`, `in_progress` or `completed`.
73 pub status: String,
74 /// When completed: `success`, `failure`, `cancelled` or `skipped`.
75 pub conclusion: Option<String>,
76 /// Why it could not start, such as a workflow file that does not read.
77 pub error: Option<String>,
78 /// Username of whoever caused it.
79 pub actor: Option<String>,
80 pub created_at: String,
81 pub started_at: Option<String>,
82 pub finished_at: Option<String>,
83}
84
85#[derive(Clone, Debug, Default, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub struct StepState {
88 /// From 1.
89 pub number: u32,
90 pub name: String,
91 /// `queued`, `in_progress` or `completed`.
92 pub status: String,
93 /// `success`, `failure`, `cancelled` or `skipped`.
94 pub conclusion: Option<String>,
95 pub started_at: Option<String>,
96 pub finished_at: Option<String>,
97}
98
99/// A message a step left with `::error::`, `::warning::` or `::notice::`.
100#[derive(Clone, Debug, Default, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct Annotation {
103 /// `error`, `warning` or `notice`.
104 pub level: String,
105 pub message: String,
106 pub title: Option<String>,
107 pub file: Option<String>,
108 pub line: Option<u32>,
109}
110
111#[derive(Clone, Debug, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct Job {
114 pub id: String,
115 pub run_id: String,
116 /// Its key under `jobs:`.
117 pub key: String,
118 /// With its matrix combination: `test (ubuntu-latest, 20)`.
119 pub name: String,
120 pub needs: Vec<String>,
121 /// `queued`, `waiting` (for the jobs it needs), `in_progress` or `completed`.
122 pub status: String,
123 pub conclusion: Option<String>,
124 pub steps: Vec<StepState>,
125 pub annotations: Vec<Annotation>,
126 /// Why it did not run, what stopped it, or what it waits for.
127 pub reason: Option<String>,
128 pub started_at: Option<String>,
129 pub finished_at: Option<String>,
130 /// The environment it names, once its needs are done (an expression
131 /// read by then). A job held by the environment's protection rules is
132 /// `pending` until they let it through.
133 #[serde(default)]
134 pub environment: Option<String>,
135 /// Its `runs-on` names self-hosted runners (see `runners`).
136 #[serde(default)]
137 pub self_hosted: bool,
138 /// The self-hosted runner that took it, by name.
139 #[serde(default)]
140 pub runner: Option<String>,
141}
142
143#[derive(Clone, Debug, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct RunDetail {
146 pub run: WorkflowRun,
147 pub jobs: Vec<Job>,
148 /// The workflow's notes, as of the run's commit.
149 pub notes: Vec<WorkflowNote>,
150 /// For a run of a pull request from outside: whether it waits for, or
151 /// had, someone's approval (`status` is `action_required` while it waits).
152 #[serde(default)]
153 pub approval: Option<RunApproval>,
154 /// The environments whose protection rules hold its jobs, this attempt.
155 #[serde(default)]
156 pub pending_deployments: Vec<PendingDeployment>,
157}
158
159/// A run that needed approval before it started.
160#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct RunApproval {
163 /// `required` while it waits, then `approved`.
164 pub state: String,
165 /// Why it waits, in words.
166 pub reason: String,
167 /// Who approved it.
168 pub approved_by: Option<String>,
169}
170
171/// One person or team who may approve a job's deployment to an
172/// environment.
173#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
174pub struct EnvironmentReviewer {
175 /// `user` or `team`.
176 #[serde(rename = "type")]
177 pub kind: String,
178 /// A username, or a team's slug in the repository's workspace.
179 pub name: String,
180}
181
182/// A branch or tag pattern an environment lets deploy.
183#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
184pub struct BranchPattern {
185 /// fnmatch-style, as branch filters are: `main`, `release/*`, `v*`.
186 pub name: String,
187 /// `branch` or `tag`.
188 #[serde(rename = "type", default = "branch_kind")]
189 pub kind: String,
190}
191
192fn branch_kind() -> String {
193 "branch".to_owned()
194}
195
196/// The most reviewers an environment may have, as on GitHub.
197pub const MAX_ENVIRONMENT_REVIEWERS: usize = 6;
198/// The longest wait timer, in minutes: 30 days.
199pub const MAX_WAIT_MINUTES: u32 = 43_200;
200
201/// An environment and its protection rules. Jobs that name it with
202/// `environment:` wait until the rules let them through; only then does the
203/// job get the environment's secrets.
204#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
205#[serde(rename_all = "camelCase")]
206pub struct Environment {
207 /// Lowercase.
208 pub name: String,
209 /// Who may approve its jobs; none means no review is needed.
210 pub reviewers: Vec<EnvironmentReviewer>,
211 /// Whoever started a run may not approve its jobs, even as a reviewer.
212 pub prevent_self_review: bool,
213 /// Minutes each job waits before it may start.
214 pub wait_minutes: u32,
215 /// Which refs may deploy: `all`, `protected` (branches the rules
216 /// protect, the default branch included) or `selected` (`branch_patterns`).
217 pub branch_policy: String,
218 pub branch_patterns: Vec<BranchPattern>,
219 /// Admins may approve without being reviewers, which also skips the wait.
220 pub admins_bypass: bool,
221 /// Whether it has rules saved; false for one only named by a workflow,
222 /// a secret or a deployment.
223 pub protected: bool,
224 pub updated_at: Option<String>,
225 pub updated_by: Option<String>,
226}
227
228/// An environment holding a run's jobs, and where its rules stand.
229#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct PendingDeployment {
232 pub environment: String,
233 /// `waiting`, `approved` or `rejected`.
234 pub state: String,
235 /// Whether a reviewer must approve it before its jobs start.
236 pub needs_review: bool,
237 /// When its wait timer lets its jobs start, if it has one.
238 pub wait_until: Option<String>,
239 pub reviewers: Vec<EnvironmentReviewer>,
240 /// The jobs it holds, by name.
241 pub jobs: Vec<String>,
242 /// Whether the viewer may approve or reject it now.
243 #[serde(default)]
244 pub can_review: bool,
245 pub reviewed_by: Option<String>,
246 pub comment: Option<String>,
247 pub reviewed_at: Option<String>,
248}
249
250/// A repository's choices for its workflows.
251#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
252#[serde(rename_all = "camelCase")]
253pub struct ActionsSettings {
254 /// What a workflow without `permissions:` gets: `read` (contents and
255 /// packages read) or `write` (every permission). Unchosen, a repository
256 /// made before restricted tokens keeps `write`; a newer one takes its
257 /// workspace's default. Never more than the workspace's maximum.
258 pub default_permissions: String,
259 /// Whether the repository chose it, rather than taking it as above.
260 #[serde(default)]
261 pub default_chosen: bool,
262 /// The most the workspace lets a repository's default be.
263 #[serde(default = "write")]
264 pub max_permissions: String,
265 /// Which pull requests' runs wait for approval: `first_time_contributors`,
266 /// `outside_contributors` (the default) or `all_external_contributors`.
267 pub approval_policy: String,
268 /// Whether a job's token may open pull requests and approve them. Off
269 /// unless the repository turns it on, and only where the workspace
270 /// allows it.
271 #[serde(default)]
272 pub can_approve_pull_requests: bool,
273 /// Whether the workspace lets its repositories turn that on.
274 #[serde(default)]
275 pub workspace_allows_pull_requests: bool,
276}
277
278fn write() -> String {
279 "write".to_owned()
280}
281
282/// A workspace's policy for its repositories' tokens.
283#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct WorkspaceActionsSettings {
286 /// What a repository made from now on gets by default: `read` (the
287 /// default) or `write`.
288 pub default_permissions: String,
289 /// The most any repository's default may be: `write` (the default) or
290 /// `read`, which holds every repository to read-only.
291 pub max_permissions: String,
292 /// Whether its repositories may let jobs open and approve pull
293 /// requests. Off by default.
294 pub can_approve_pull_requests: bool,
295}
296
297/// `workspace_actions_settings`: members only. Returns
298/// `Outcome<WorkspaceActionsSettings>`.
299#[derive(Debug, Serialize, Deserialize)]
300pub struct WorkspaceActionsSettingsArgs {
301 pub viewer: Viewer,
302 pub workspace: String,
303}
304
305/// `set_workspace_actions_settings`: owners only. Fields left out stay as
306/// they are. Returns `Outcome<WorkspaceActionsSettings>`.
307#[derive(Debug, Serialize, Deserialize)]
308#[serde(rename_all = "camelCase")]
309pub struct SetWorkspaceActionsSettingsArgs {
310 pub actor: User,
311 pub workspace: String,
312 #[serde(default)]
313 pub default_permissions: Option<String>,
314 #[serde(default)]
315 pub max_permissions: Option<String>,
316 #[serde(default)]
317 pub can_approve_pull_requests: Option<bool>,
318}
319
320/// The approval policies, least strict first.
321pub const APPROVAL_POLICIES: [&str; 3] = ["first_time_contributors", "outside_contributors", "all_external_contributors"];
322
323/// `actions_settings`. Returns `Outcome<ActionsSettings>`; anyone who can
324/// read the repository may see them.
325#[derive(Debug, Serialize, Deserialize)]
326pub struct ActionsSettingsArgs {
327 pub viewer: Viewer,
328 pub repo: RepoPath,
329}
330
331/// `set_actions_settings`: Admins only. Fields left out stay as they are.
332/// Returns `Outcome<ActionsSettings>`.
333#[derive(Debug, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct SetActionsSettingsArgs {
336 pub actor: User,
337 pub repo: RepoPath,
338 /// `read` or `write`; `inherit` goes back to the workspace's (or, for a
339 /// repository made before restricted tokens, `write`).
340 #[serde(default)]
341 pub default_permissions: Option<String>,
342 #[serde(default)]
343 pub approval_policy: Option<String>,
344 #[serde(default)]
345 pub can_approve_pull_requests: Option<bool>,
346}
347
348/// `environments`: every environment a repository's workflows, secrets,
349/// deployments or rules name, with its rules. `environment`: one, by
350/// `name`. Returns `Outcome<Vec<Environment>>` and `Outcome<Environment>`.
351#[derive(Debug, Serialize, Deserialize)]
352pub struct EnvironmentsArgs {
353 pub viewer: Viewer,
354 pub repo: RepoPath,
355 #[serde(default)]
356 pub name: Option<String>,
357}
358
359/// `set_environment`: create an environment's rules or change them. Fields
360/// left out stay as they are (none, for a new one). Admins only. Returns
361/// `Outcome<Environment>`.
362#[derive(Debug, Serialize, Deserialize)]
363#[serde(rename_all = "camelCase")]
364pub struct SetEnvironmentArgs {
365 pub actor: User,
366 pub repo: RepoPath,
367 pub name: String,
368 #[serde(default)]
369 pub reviewers: Option<Vec<EnvironmentReviewer>>,
370 #[serde(default)]
371 pub prevent_self_review: Option<bool>,
372 #[serde(default)]
373 pub wait_minutes: Option<u32>,
374 #[serde(default)]
375 pub branch_policy: Option<String>,
376 #[serde(default)]
377 pub branch_patterns: Option<Vec<BranchPattern>>,
378 #[serde(default)]
379 pub admins_bypass: Option<bool>,
380}
381
382/// `delete_environment`: its rules go; jobs naming it run without them.
383/// Its secrets' rows stay. Admins only. Returns `Outcome<bool>`.
384#[derive(Debug, Serialize, Deserialize)]
385pub struct DeleteEnvironmentArgs {
386 pub actor: User,
387 pub repo: RepoPath,
388 pub name: String,
389}
390
391/// `pending_deployments`: the environments holding a run's jobs. Returns
392/// `Outcome<Vec<PendingDeployment>>`.
393#[derive(Debug, Serialize, Deserialize)]
394pub struct PendingDeploymentsArgs {
395 pub viewer: Viewer,
396 pub repo: RepoPath,
397 pub id: String,
398}
399
400/// `review_deployments`: approve or reject a run's jobs for `environments`
401/// (every one waiting, if empty). Returns `Outcome<Vec<PendingDeployment>>`.
402#[derive(Debug, Serialize, Deserialize)]
403pub struct ReviewDeploymentsArgs {
404 pub actor: User,
405 pub repo: RepoPath,
406 pub id: String,
407 #[serde(default)]
408 pub environments: Vec<String>,
409 /// `approved` or `rejected`.
410 pub state: String,
411 #[serde(default)]
412 pub comment: Option<String>,
413}
414
415/// `repository_dispatch`: start the default branch's workflows that run
416/// `on: repository_dispatch` for `event_type`. Needs the Write role (a
417/// token's `code:write`). Returns `Outcome<u32>`: how many started.
418#[derive(Debug, Serialize, Deserialize)]
419#[serde(rename_all = "camelCase")]
420pub struct RepositoryDispatchArgs {
421 pub actor: User,
422 pub repo: RepoPath,
423 pub event_type: String,
424 #[serde(default)]
425 pub client_payload: Value,
426}
427
428#[derive(Clone, Debug, Serialize, Deserialize)]
429#[serde(rename_all = "camelCase")]
430pub struct LogChunk {
431 pub seq: u64,
432 /// The step it belongs to, from 1; 0 for the job's setup.
433 pub step: u32,
434 pub text: String,
435}
436
437#[derive(Clone, Debug, Serialize, Deserialize)]
438#[serde(rename_all = "camelCase")]
439pub struct JobLog {
440 pub chunks: Vec<LogChunk>,
441 /// Whether the job has finished, so no more will come.
442 pub done: bool,
443}
444
445/// Who may read a secret or variable: workflows (`secrets.*` and `vars.*`
446/// in GitHub Actions) and deployments (a deploy build's environment and the
447/// running app's bindings). Agents, checks and the merge queue read none.
448pub const CONSUMERS: [&str; 2] = ["workflows", "deployments"];
449
450/// One row of a repository's or workspace's secrets and variables, as
451/// Vercel lists environment variables: a key, its type, the environments
452/// it applies to and who reads it. A key may have one row per environment.
453/// Secrets' values are never returned.
454#[derive(Clone, Debug, Serialize, Deserialize)]
455#[serde(rename_all = "camelCase")]
456pub struct Setting {
457 #[serde(default)]
458 pub id: String,
459 pub name: String,
460 /// `secret`, or `variable` (shown as Config).
461 #[serde(default)]
462 pub kind: String,
463 /// A variable's value; secrets' are never returned.
464 pub value: Option<String>,
465 /// `project` (a repository's, which belong to its project) or
466 /// `workspace`.
467 pub scope: String,
468 pub updated_at: String,
469 /// `workflows` and/or `deployments`.
470 #[serde(default)]
471 pub available_to: Vec<String>,
472 /// The environments it applies to; empty is every environment.
473 #[serde(default)]
474 pub environments: Vec<String>,
475 /// A workspace's row: the projects it reaches, by slug; empty is every
476 /// project.
477 #[serde(default)]
478 pub projects: Vec<String>,
479 #[serde(default)]
480 pub note: Option<String>,
481 #[serde(default)]
482 pub updated_by: Option<String>,
483}
484
485// --- Methods ---------------------------------------------------------------
486
487/// `workflows`. Returns `Outcome<Vec<Workflow>>`.
488#[derive(Debug, Serialize, Deserialize)]
489pub struct WorkflowsArgs {
490 pub repo: RepoPath,
491 pub viewer: Viewer,
492}
493
494/// `runs`: newest first. Returns `Outcome<Vec<WorkflowRun>>`.
495#[derive(Debug, Serialize, Deserialize)]
496pub struct RunsArgs {
497 pub repo: RepoPath,
498 pub viewer: Viewer,
499 /// A workflow's id or file name.
500 #[serde(default)]
501 pub workflow: Option<String>,
502 #[serde(default)]
503 pub branch: Option<String>,
504 #[serde(default)]
505 pub event: Option<String>,
506 /// The pull request's number.
507 #[serde(default)]
508 pub pull: Option<u32>,
509 #[serde(default)]
510 pub sha: Option<String>,
511 #[serde(default)]
512 pub limit: Option<u32>,
513}
514
515/// `run`. Returns `Outcome<RunDetail>`.
516#[derive(Debug, Serialize, Deserialize)]
517pub struct RunArgs {
518 pub repo: RepoPath,
519 pub viewer: Viewer,
520 pub id: String,
521}
522
523/// `logs`: a job's log after `after`. Returns `Outcome<JobLog>`.
524#[derive(Debug, Serialize, Deserialize)]
525pub struct LogsArgs {
526 pub repo: RepoPath,
527 pub viewer: Viewer,
528 pub job: String,
529 #[serde(default)]
530 pub after: u64,
531}
532
533/// `dispatch`: run a workflow that has `workflow_dispatch`. Members only.
534/// Returns `Outcome<WorkflowRun>`.
535#[derive(Debug, Serialize, Deserialize)]
536pub struct DispatchArgs {
537 pub actor: User,
538 pub repo: RepoPath,
539 /// A workflow's id or file name.
540 pub workflow: String,
541 /// A branch or tag; the default branch when absent.
542 #[serde(default, rename = "ref")]
543 pub git_ref: Option<String>,
544 #[serde(default)]
545 pub inputs: serde_json::Map<String, Value>,
546}
547
548/// `cancel` and `rerun` (all jobs, or with `failed_only` the ones that did
549/// not succeed). Members only. Returns `Outcome<WorkflowRun>`.
550#[derive(Debug, Serialize, Deserialize)]
551pub struct RunActionArgs {
552 pub actor: User,
553 pub repo: RepoPath,
554 pub id: String,
555 #[serde(default)]
556 pub failed_only: bool,
557}
558
559/// `set_workflow_enabled`. Members only. Returns `Outcome<Workflow>`.
560#[derive(Debug, Serialize, Deserialize)]
561pub struct SetWorkflowEnabledArgs {
562 pub actor: User,
563 pub repo: RepoPath,
564 pub workflow: String,
565 pub enabled: bool,
566}
567
568/// Whose secrets or variables: a repository's, or with only `workspace`,
569/// a workspace's.
570#[derive(Clone, Debug, Serialize, Deserialize)]
571pub struct SettingsOwner {
572 #[serde(default)]
573 pub repo: Option<RepoPath>,
574 #[serde(default)]
575 pub workspace: Option<String>,
576}
577
578/// `settings`: the secrets (`kind: secret`) or variables (`kind: variable`)
579/// of a repository, with its workspace's, or of a workspace. Members only.
580/// Returns `Outcome<Vec<Setting>>`.
581#[derive(Debug, Serialize, Deserialize)]
582pub struct SettingsArgs {
583 pub actor: User,
584 #[serde(flatten)]
585 pub owner: SettingsOwner,
586 pub kind: String,
587}
588
589/// `set_setting`: add or replace one. A repository's need a member; a
590/// workspace's an owner. Returns `Outcome<Setting>`.
591#[derive(Debug, Serialize, Deserialize)]
592pub struct SetSettingArgs {
593 pub actor: User,
594 #[serde(flatten)]
595 pub owner: SettingsOwner,
596 /// `secret` or `variable`. Changing a variable's row to `secret` seals
597 /// it; a secret cannot become a variable.
598 pub kind: String,
599 pub name: String,
600 /// The row to change. Left out, the key's row for every environment, as
601 /// GitHub's API addresses a secret by name alone.
602 #[serde(default)]
603 pub id: Option<String>,
604 /// Needed for a new row; left out, an existing row keeps its value.
605 #[serde(default)]
606 pub value: Option<String>,
607 /// `workflows` and/or `deployments`; left out, unchanged (both, for a
608 /// new row).
609 // Named as callers send it: an `alias` is not honoured beside the
610 // flattened owner in the Worker's build.
611 #[serde(default, rename = "availableTo")]
612 pub available_to: Option<Vec<String>>,
613 /// The environments it applies to; empty is every one. Left out,
614 /// unchanged.
615 #[serde(default)]
616 pub environments: Option<Vec<String>>,
617 /// A workspace's row: project slugs; empty for every one.
618 #[serde(default)]
619 pub projects: Option<Vec<String>>,
620 #[serde(default)]
621 pub note: Option<String>,
622}
623
624/// `resolve_settings`: the secrets and variables one reader gets, for the
625/// services that hand them out (the deployments service). Returns
626/// `ResolvedSettings`.
627#[derive(Debug, Serialize, Deserialize)]
628#[serde(rename_all = "camelCase")]
629pub struct ResolveSettingsArgs {
630 pub repo_id: String,
631 pub repo: RepoPath,
632 /// The project being read for; its repository's primary project if left
633 /// out.
634 #[serde(default)]
635 pub project_id: Option<String>,
636 #[serde(default)]
637 pub project_slug: Option<String>,
638 /// `workflows` or `deployments`.
639 pub consumer: String,
640 /// The environment being read for, such as `production` or `preview`.
641 #[serde(default)]
642 pub environment: Option<String>,
643 /// Whether the run is trusted; an untrusted one gets no secrets.
644 pub trusted: bool,
645}
646
647#[derive(Debug, Default, Serialize, Deserialize)]
648pub struct ResolvedSettings {
649 pub secrets: serde_json::Map<String, serde_json::Value>,
650 pub variables: serde_json::Map<String, serde_json::Value>,
651}
652
653/// `delete_setting`. Returns `Outcome<bool>`.
654#[derive(Debug, Serialize, Deserialize)]
655pub struct DeleteSettingArgs {
656 pub actor: User,
657 #[serde(flatten)]
658 pub owner: SettingsOwner,
659 pub kind: String,
660 pub name: String,
661 /// One row; left out, every row of the key.
662 #[serde(default)]
663 pub id: Option<String>,
664}
665
666/// `job_spec` and `job_report`: the sandbox running a job, with the job's
667/// own token. `report` is one of:
668/// `{"kind": "step", "number", "status", "conclusion"}`,
669/// `{"kind": "log", "step", "text"}`,
670/// `{"kind": "annotation", "level", "message", "title", "file", "line"}`,
671/// `{"kind": "done", "conclusion", "outputs", "reason"}`.
672#[derive(Debug, Serialize, Deserialize)]
673pub struct JobCallArgs {
674 pub job: String,
675 pub token: String,
676 #[serde(default)]
677 pub report: Value,
678}
679
680/// What the runner needs to start a job's sandbox.
681#[derive(Debug, Serialize, Deserialize)]
682#[serde(rename_all = "camelCase")]
683pub struct StartJobArgs {
684 pub job: String,
685 pub token: String,
686 pub repo: RepoPath,
687 /// Minutes before the job is stopped.
688 pub timeout_minutes: u32,
689 /// The workflow file the job is in (`.g1t/workflows/deploy.yml`), for
690 /// the guardrails' workflow-only domains.
691 #[serde(default)]
692 pub workflow: Option<String>,
693 /// The environment the job names with `environment:`, when it names
694 /// one plainly (not with an expression).
695 #[serde(default)]
696 pub environment: Option<String>,
697 /// Whether its run is trusted: not a pull request from a fork. Only a
698 /// trusted run's jobs reach workflow-only domains.
699 #[serde(default)]
700 pub trusted: bool,
701 /// The machine its `runs-on` asked for, by label (`instance_for`):
702 /// `g1t-2core` or `g1t-4core`; absent, the standard one.
703 #[serde(default)]
704 pub instance: Option<String>,
705}
706
707/// A size of machine g1t runs workflow jobs on, asked for by a label in
708/// `runs-on`. Each is a Cloudflare Containers instance type; it costs what
709/// that instance costs g1t, plus the margin, like any sandbox time.
710#[derive(Clone, Copy, Debug, PartialEq)]
711pub struct InstanceType {
712 /// The `runs-on` label, or `standard` for the default.
713 pub label: &'static str,
714 /// The Containers instance type.
715 pub container: &'static str,
716 pub vcpu: f64,
717 pub memory_gib: f64,
718 pub disk_gb: f64,
719 /// What a second of it costs g1t as a multiple of the standard
720 /// machine's, with its vCPUs as busy (Cloudflare's list prices:
721 /// memory $0.0000025 a GiB-second, disk $0.00000007 a GB-second, vCPU
722 /// $0.00002 a second). Used to reserve before a job starts, and to
723 /// price a job that did not report its own CPU.
724 pub price_scale: f64,
725}
726
727/// The default: what `ubuntu-latest` and every other hosted label get.
728pub const STANDARD_INSTANCE: InstanceType =
729 InstanceType { label: "standard", container: "standard-1", vcpu: 0.5, memory_gib: 4.0, disk_gb: 8.0, price_scale: 1.0 };
730
731/// Every machine a workflow job can ask for, the default first.
732pub const INSTANCE_TYPES: [InstanceType; 3] = [
733 STANDARD_INSTANCE,
734 InstanceType { label: "g1t-2core", container: "standard-3", vcpu: 2.0, memory_gib: 8.0, disk_gb: 16.0, price_scale: 2.8 },
735 InstanceType { label: "g1t-4core", container: "standard-4", vcpu: 4.0, memory_gib: 12.0, disk_gb: 20.0, price_scale: 5.1 },
736];
737
738/// The machine a job's `runs-on` labels ask for: the largest named, or the
739/// standard one. Labels compare without regard to case.
740pub fn instance_for(labels: &[String]) -> InstanceType {
741 INSTANCE_TYPES
742 .iter()
743 .rev()
744 .find(|instance| instance.label != STANDARD_INSTANCE.label && labels.iter().any(|label| label.trim().eq_ignore_ascii_case(instance.label)))
745 .copied()
746 .unwrap_or(STANDARD_INSTANCE)
747}
748
749/// An instance type by its label, if it is one.
750pub fn instance_named(label: &str) -> Option<InstanceType> {
751 INSTANCE_TYPES.iter().find(|instance| instance.label.eq_ignore_ascii_case(label.trim())).copied()
752}
753
754// ── The cache (actions/cache) ─────────────────────────────────────────────
755//
756// Entries are kept in R2 by the API (the ACTIONS_CACHE bucket) and listed
757// here, by the actions service, which decides what is found, what fits and
758// what is evicted. A sandbox reaches these through the API with its job's
759// token: `/actions/jobs/{job}/cache` (see apps/api/src/blobs.rs).
760
761/// The largest one cache entry may be, compressed.
762pub const CACHE_MAX_ENTRY_BYTES: u64 = 2 * 1024 * 1024 * 1024;
763/// What one repository's entries may hold together. Saving past it evicts
764/// the entries restored longest ago.
765pub const CACHE_REPO_QUOTA_BYTES: u64 = 10 * 1024 * 1024 * 1024;
766/// An entry not restored for this long is deleted.
767pub const CACHE_UNUSED_DAYS: u64 = 7;
768/// An entry is deleted this long after it was saved, however often it is
769/// restored (the bucket's own lifecycle rule deletes objects at 30 days).
770pub const CACHE_MAX_AGE_DAYS: u64 = 28;
771/// An upload is sent in parts of this size (the last may be smaller).
772pub const CACHE_PART_BYTES: u64 = 32 * 1024 * 1024;
773/// What R2 charges g1t to store a GB for a month, in millionths of a
774/// dollar ($0.015): what the cache's storage is charged at, plus the margin.
775pub const CACHE_MICROS_PER_GB_MONTH: i64 = 15_000;
776
777/// `cache_lookup`: the entry a job restores: its key exactly, else the
778/// newest whose key starts with one of `restore`, in order.
779/// Returns `Outcome<Option<CacheHit>>`.
780#[derive(Debug, Serialize, Deserialize)]
781pub struct CacheLookupArgs {
782 pub job: String,
783 pub token: String,
784 pub key: String,
785 #[serde(default)]
786 pub restore: Vec<String>,
787 /// The entry's version, a hash of its paths and compression, as the
788 /// toolkit's client and g1t's runner both send it: only an entry of the
789 /// same version is found. `None` from runners that send none, whose
790 /// entries have none.
791 #[serde(default)]
792 pub version: Option<String>,
793}
794
795#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
796pub struct CacheHit {
797 pub key: String,
798 pub object: String,
799 pub size: u64,
800 /// When it was saved, RFC 3339.
801 #[serde(default)]
802 pub created_at: String,
803 /// A signed token for downloading it through the toolkit's blob
804 /// endpoint, when the lookup came with a version.
805 #[serde(default)]
806 pub blob: Option<String>,
807}
808
809/// `cache_reserve`: a job about to save `size` bytes under `key`. Refused
810/// when the key is taken (`conflict`: keys are written once) or the entry
811/// is too large. Returns `Outcome<CacheReservation>`.
812#[derive(Debug, Serialize, Deserialize)]
813pub struct CacheReserveArgs {
814 pub job: String,
815 pub token: String,
816 pub key: String,
817 /// Its size, when known before it is sent (the toolkit's newer client
818 /// says only when it finishes: 0 then).
819 pub size: u64,
820 /// As in `CacheLookupArgs`.
821 #[serde(default)]
822 pub version: Option<String>,
823}
824
825#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
826pub struct CacheReservation {
827 pub id: String,
828 /// Where the API puts it in R2.
829 pub object: String,
830 /// The entry's number, which the toolkit's older protocol names it by.
831 #[serde(default)]
832 pub number: u64,
833 /// Its R2 upload, once one is started.
834 #[serde(default)]
835 pub upload: Option<String>,
836 /// A signed token for sending its parts through the toolkit's blob
837 /// endpoint, once its upload is started.
838 #[serde(default)]
839 pub blob: Option<String>,
840}
841
842/// `cache_upload`: an entry a job is still uploading, by its number or by
843/// key and version. Returns `Outcome<CacheReservation>`, with `upload` and
844/// `blob` set once its upload has been started.
845#[derive(Debug, Serialize, Deserialize)]
846pub struct CacheUploadArgs {
847 pub job: String,
848 pub token: String,
849 #[serde(default)]
850 pub number: Option<u64>,
851 #[serde(default)]
852 pub key: Option<String>,
853 #[serde(default)]
854 pub version: Option<String>,
855}
856
857/// `cache_commit`: the upload of `id` is complete, at `size` bytes. Returns
858/// `Outcome<CacheCommitted>`: the objects of entries it evicted, which the
859/// API deletes from R2.
860#[derive(Debug, Serialize, Deserialize)]
861pub struct CacheCommitArgs {
862 pub job: String,
863 pub token: String,
864 pub id: String,
865 pub size: u64,
866}
867
868#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
869pub struct CacheCommitted {
870 pub evicted: Vec<String>,
871}
872
873/// `cache_abort`: an upload that will not finish; its reservation goes.
874/// Returns `Outcome<bool>`.
875#[derive(Debug, Serialize, Deserialize)]
876pub struct CacheAbortArgs {
877 pub job: String,
878 pub token: String,
879 pub id: String,
880}
881
882// ── Artifacts (actions/upload-artifact) ───────────────────────────────────
883//
884// Kept in R2 by the API (the ACTIONS_CACHE bucket, under `a/`) and listed
885// here, by the actions service, which decides names, sizes and how long
886// each is kept. A sandbox reaches them with its job's token
887// (`/actions/jobs/{job}/artifacts…`) or, through the toolkit's protocol,
888// with its runtime token (`ACTIONS_RUNTIME_TOKEN`); people through the
889// REST API and the run's page.
890
891/// The largest one artifact may be.
892pub const ARTIFACT_MAX_BYTES: u64 = 5 * 1024 * 1024 * 1024;
893/// What one run's artifacts may hold together.
894pub const RUN_ARTIFACTS_MAX_BYTES: u64 = 10 * 1024 * 1024 * 1024;
895/// How long artifacts are kept unless a repository says otherwise.
896pub const ARTIFACT_RETENTION_DEFAULT_DAYS: u32 = 14;
897/// The longest a repository may keep them.
898pub const ARTIFACT_RETENTION_MAX_DAYS: u32 = 90;
899/// A native upload is sent in parts of this size (the last may be smaller).
900pub const ARTIFACT_PART_BYTES: u64 = 32 * 1024 * 1024;
901
902/// An artifact, as the API and the site show it.
903#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
904pub struct Artifact {
905 pub id: u64,
906 pub name: String,
907 pub size: u64,
908 /// `sha256:<hex>`, when the uploader said.
909 pub digest: Option<String>,
910 /// `zip`, or `tgz` for one an older runner sent.
911 pub format: String,
912 pub run_id: String,
913 pub job_id: String,
914 pub repo_id: String,
915 /// Whether it has expired or been deleted (its bytes are gone).
916 pub expired: bool,
917 pub created_at: String,
918 pub updated_at: String,
919 pub expires_at: String,
920 /// The run's branch and commit, for the REST shape.
921 #[serde(default)]
922 pub head_branch: Option<String>,
923 #[serde(default)]
924 pub head_sha: Option<String>,
925}
926
927/// An artifact with where its bytes are, and a signed token for them.
928#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
929pub struct ArtifactBlob {
930 pub artifact: Artifact,
931 pub object: String,
932 /// For the toolkit's blob endpoint (`/actions/toolkit/blobs/{blob}`).
933 pub blob: String,
934}
935
936/// A page of artifacts, in GitHub's shape.
937#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
938pub struct ArtifactList {
939 pub total_count: u64,
940 pub artifacts: Vec<Artifact>,
941}
942
943/// `artifact_reserve`: a job about to upload an artifact. Refused when its
944/// run has one of that name and `overwrite` is not set (`conflict`), or it
945/// is too large. Returns `Outcome<ArtifactReservation>`.
946#[derive(Debug, Default, Serialize, Deserialize)]
947pub struct ArtifactReserveArgs {
948 pub job: String,
949 /// The job's token, or its runtime token.
950 pub token: String,
951 pub name: String,
952 /// Its size, when known before it is sent (0 otherwise).
953 #[serde(default)]
954 pub size: u64,
955 /// Days to keep it: 0 for the repository's default; at most the
956 /// repository's setting.
957 #[serde(default)]
958 pub retention_days: u32,
959 /// When to expire it, RFC 3339, as the toolkit says it (in place of
960 /// `retention_days`).
961 #[serde(default)]
962 pub expires_at: Option<String>,
963 #[serde(default)]
964 pub overwrite: bool,
965 /// `zip` (the default) or `tgz`.
966 #[serde(default)]
967 pub format: Option<String>,
968}
969
970#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
971pub struct ArtifactReservation {
972 pub id: u64,
973 /// Where the API puts it in R2.
974 pub object: String,
975 /// The days it will be kept, and until when.
976 pub retention_days: u32,
977 pub expires_at: String,
978}
979
980/// `artifact_commit`: its upload is complete, at `size` bytes. The artifact
981/// is named by `id`, or by `name` in the job's run (the toolkit's way).
982/// Returns `Outcome<Artifact>`.
983#[derive(Debug, Default, Serialize, Deserialize)]
984pub struct ArtifactCommitArgs {
985 pub job: String,
986 pub token: String,
987 #[serde(default)]
988 pub id: Option<u64>,
989 #[serde(default)]
990 pub name: Option<String>,
991 pub size: u64,
992 #[serde(default)]
993 pub digest: Option<String>,
994}
995
996/// `job_artifacts`: a running job listing the artifacts of its own run, or
997/// of another run of its repository (`run_id`), narrowed by `name` or
998/// `id`: `Outcome<Vec<Artifact>>`. `job_artifact` gives the one named, with
999/// a token to download it: `Outcome<ArtifactBlob>`. `job_delete_artifact`
1000/// deletes one of its own run's: `Outcome<Artifact>`. `artifact_abort`
1001/// gives up an upload by `id`: `Outcome<bool>`.
1002#[derive(Debug, Default, Serialize, Deserialize)]
1003pub struct JobArtifactsArgs {
1004 pub job: String,
1005 pub token: String,
1006 #[serde(default)]
1007 pub run_id: Option<String>,
1008 #[serde(default)]
1009 pub name: Option<String>,
1010 #[serde(default)]
1011 pub id: Option<u64>,
1012}
1013
1014/// `artifacts`: a repository's artifacts, newest first, or one run's.
1015/// Anyone who can see the repository. Returns `Outcome<ArtifactList>`.
1016#[derive(Debug, Serialize, Deserialize)]
1017pub struct ArtifactsArgs {
1018 pub repo: RepoPath,
1019 pub viewer: Viewer,
1020 #[serde(default)]
1021 pub run: Option<String>,
1022 #[serde(default)]
1023 pub name: Option<String>,
1024 #[serde(default)]
1025 pub page: Option<u32>,
1026 #[serde(default)]
1027 pub per_page: Option<u32>,
1028}
1029
1030/// `artifact` (`Outcome<Artifact>`) and `artifact_download`
1031/// (`Outcome<ArtifactBlob>`, with a token good for a few minutes): one
1032/// artifact by `id`, or by `name` within `run`. Anyone who can see the
1033/// repository.
1034#[derive(Debug, Serialize, Deserialize)]
1035pub struct ArtifactArgs {
1036 pub repo: RepoPath,
1037 pub viewer: Viewer,
1038 #[serde(default)]
1039 pub id: Option<u64>,
1040 #[serde(default)]
1041 pub run: Option<String>,
1042 #[serde(default)]
1043 pub name: Option<String>,
1044}
1045
1046/// `delete_artifact`: needs the Write role. Returns `Outcome<Artifact>`.
1047#[derive(Debug, Serialize, Deserialize)]
1048pub struct DeleteArtifactArgs {
1049 pub actor: User,
1050 pub repo: RepoPath,
1051 pub id: u64,
1052}
1053
1054/// `artifact_retention`: anyone who can see the repository. With `days`,
1055/// sets it, which needs the Maintain role. Returns
1056/// `Outcome<ArtifactRetention>`.
1057#[derive(Debug, Serialize, Deserialize)]
1058pub struct ArtifactRetentionArgs {
1059 pub repo: RepoPath,
1060 pub viewer: Viewer,
1061 #[serde(default)]
1062 pub days: Option<u32>,
1063}
1064
1065/// GitHub's shape: the days artifacts are kept by default, and the most a
1066/// repository may choose.
1067#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1068pub struct ArtifactRetention {
1069 pub days: u32,
1070 pub maximum_allowed_days: u32,
1071}
1072
1073// ── The toolkit's protocols ───────────────────────────────────────────────
1074//
1075// Actions built on GitHub's toolkit (`@actions/cache`, `@actions/artifact`,
1076// `@actions/core`'s `getIDToken`) reach g1t with the job's runtime token,
1077// `ACTIONS_RUNTIME_TOKEN`: a JSON Web Token whose `scp` names the run and
1078// job, signed with a key derived from the job's own token, so the actions
1079// service checks it without keeping another secret. Cache and artifact
1080// operations above take it in place of the job's token.
1081
1082/// `runtime_auth`: which job a runtime token is, while it runs:
1083/// `Outcome<RuntimeJob>`. `oidc_claims` takes the same and returns
1084/// `Outcome<Value>`: the claims of the job's OIDC token, less `iss`, `aud`,
1085/// `jti` and the times, or `forbidden` when the job's `permissions` do not
1086/// give it `id-token: write`.
1087#[derive(Debug, Serialize, Deserialize)]
1088pub struct RuntimeAuthArgs {
1089 pub job: String,
1090 pub token: String,
1091}
1092
1093#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1094pub struct RuntimeJob {
1095 pub job: String,
1096 pub run: String,
1097 pub repo_id: String,
1098 pub namespace: String,
1099 /// `owner/name`.
1100 pub repository: String,
1101}
1102
1103/// What a signed blob token lets its holder do.
1104#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1105pub struct BlobGrant {
1106 /// `cache` or `artifact`.
1107 pub kind: String,
1108 /// The entry's id: a cache entry's `cache_…`, an artifact's number.
1109 pub id: String,
1110 pub object: String,
1111 /// The R2 upload it sends parts to; `None` for a download.
1112 pub upload: Option<String>,
1113 /// For a download: what to call the file, and its type.
1114 #[serde(default)]
1115 pub filename: Option<String>,
1116 #[serde(default)]
1117 pub content_type: Option<String>,
1118}
1119
1120/// `blob_sign`: a token for uploading an entry the job reserved, to the R2
1121/// upload the API started for it. Returns `Outcome<String>`.
1122#[derive(Debug, Serialize, Deserialize)]
1123pub struct BlobSignArgs {
1124 pub job: String,
1125 pub token: String,
1126 /// `cache` or `artifact`.
1127 pub kind: String,
1128 pub id: String,
1129 pub upload: String,
1130}
1131
1132/// `blob_open`: what a signed token grants, while it is good and its entry
1133/// is there: `Outcome<BlobGrant>`. `blob_part` records a part sent with an
1134/// upload token (`part`, `etag`, `size`): `Outcome<bool>`. `blob_parts`
1135/// gives the parts recorded, in order: `Outcome<Vec<BlobPart>>`, and
1136/// `blob_done` forgets them: `Outcome<bool>`.
1137#[derive(Debug, Default, Serialize, Deserialize)]
1138pub struct BlobArgs {
1139 pub blob: String,
1140 #[serde(default)]
1141 pub part: u32,
1142 #[serde(default)]
1143 pub etag: String,
1144 #[serde(default)]
1145 pub size: u64,
1146}
1147
1148#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1149pub struct BlobPart {
1150 pub part: u32,
1151 pub etag: String,
1152 pub size: u64,
1153}
1154
1155#[cfg(test)]
1156mod instance_tests {
1157 use super::*;
1158
1159 fn labels(given: &[&str]) -> Vec<String> {
1160 given.iter().map(|l| (*l).to_owned()).collect()
1161 }
1162
1163 #[test]
1164 fn runs_on_picks_the_machine() {
1165 assert_eq!(instance_for(&labels(&["ubuntu-latest"])).container, "standard-1");
1166 assert_eq!(instance_for(&labels(&[])).label, "standard");
1167 assert_eq!(instance_for(&labels(&["g1t-4core"])).container, "standard-4");
1168 assert_eq!(instance_for(&labels(&["ubuntu-latest", "G1T-2Core"])).container, "standard-3");
1169 // Both named: the larger.
1170 assert_eq!(instance_for(&labels(&["g1t-2core", "g1t-4core"])).label, "g1t-4core");
1171 assert_eq!(instance_named("g1t-4core").map(|i| i.vcpu), Some(4.0));
1172 assert_eq!(instance_named("standard"), Some(STANDARD_INSTANCE));
1173 assert_eq!(instance_named("g1t-64core"), None);
1174 }
1175
1176 #[test]
1177 fn start_args_from_older_callers_read() {
1178 let args: StartJobArgs = serde_json::from_value(serde_json::json!({
1179 "job": "job_1", "token": "t", "repo": { "namespace": "acme", "name": "web" }, "timeoutMinutes": 30
1180 }))
1181 .unwrap();
1182 assert!(args.workflow.is_none() && args.environment.is_none() && !args.trusted && args.instance.is_none());
1183 }
1184}
1185
1186#[cfg(test)]
1187mod setting_args_tests {
1188 use super::*;
1189
1190 #[test]
1191 fn who_reads_a_row_is_read_as_the_site_and_api_send_it() {
1192 let args: SetSettingArgs = serde_json::from_value(serde_json::json!({
1193 "actor": { "id": "usr_1", "username": "a" },
1194 "repo": { "namespace": "acme", "name": "web" },
1195 "kind": "secret",
1196 "name": "STRIPE_KEY",
1197 "availableTo": ["deployments"],
1198 "environments": ["production"],
1199 }))
1200 .unwrap();
1201 assert_eq!(args.available_to, Some(vec!["deployments".to_owned()]));
1202 }
1203}