Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

1,869 linesCodeBlameRaw
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 work items, 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 /// For a managed workflow (`managed/<name>.yml`): what installed it.
51 #[serde(default)]
52 pub managed_by: Option<ManagedBy>,
53}
54
55/// What installed a managed workflow in a repository.
56#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct ManagedBy {
59 /// `g1t` for one g1t's services installed (such as a project's deploy
60 /// workflow), or the workspace's name for one its owners added.
61 pub origin: String,
62 /// The workspace's: `required` or `default`. Empty for g1t's.
63 #[serde(default)]
64 pub mode: String,
65 /// The workspace's: the file it was copied from, as
66 /// `owner/repo/path@ref`.
67 #[serde(default)]
68 pub source: Option<String>,
69}
70
71#[derive(Clone, Debug, Default, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct WorkflowRun {
74 pub id: String,
75 pub workflow_id: String,
76 pub path: String,
77 /// The workflow's name.
78 pub name: String,
79 /// `run-name`, or what started it: a commit's subject, a pull request's title.
80 pub title: String,
81 /// Counts the workflow's runs: 1, 2, 3…
82 pub number: u64,
83 pub attempt: u64,
84 /// The GitHub event: `push`, `pull_request`, `schedule`…
85 pub event: String,
86 #[serde(rename = "ref")]
87 pub git_ref: String,
88 pub sha: String,
89 /// The pull request it ran for, if any.
90 pub pull: Option<u32>,
91 /// `queued`, `in_progress` or `completed`.
92 pub status: String,
93 /// When completed: `success`, `failure`, `cancelled` or `skipped`.
94 pub conclusion: Option<String>,
95 /// Why it could not start, such as a workflow file that does not read.
96 pub error: Option<String>,
97 /// Username of whoever caused it.
98 pub actor: Option<String>,
99 pub created_at: String,
100 pub started_at: Option<String>,
101 pub finished_at: Option<String>,
102}
103
104#[derive(Clone, Debug, Default, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct StepState {
107 /// From 1.
108 pub number: u32,
109 pub name: String,
110 /// `queued`, `in_progress` or `completed`.
111 pub status: String,
112 /// `success`, `failure`, `cancelled` or `skipped`.
113 pub conclusion: Option<String>,
114 pub started_at: Option<String>,
115 pub finished_at: Option<String>,
116}
117
118/// A message a step left with `::error::`, `::warning::` or `::notice::`.
119#[derive(Clone, Debug, Default, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct Annotation {
122 /// `error`, `warning` or `notice`.
123 pub level: String,
124 pub message: String,
125 pub title: Option<String>,
126 pub file: Option<String>,
127 pub line: Option<u32>,
128}
129
130#[derive(Clone, Debug, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct Job {
133 pub id: String,
134 pub run_id: String,
135 /// Its key under `jobs:`.
136 pub key: String,
137 /// With its matrix combination: `test (ubuntu-latest, 20)`.
138 pub name: String,
139 pub needs: Vec<String>,
140 /// `queued`, `waiting` (for the jobs it needs), `in_progress` or `completed`.
141 pub status: String,
142 pub conclusion: Option<String>,
143 pub steps: Vec<StepState>,
144 pub annotations: Vec<Annotation>,
145 /// Why it did not run, what stopped it, or what it waits for.
146 pub reason: Option<String>,
147 pub started_at: Option<String>,
148 pub finished_at: Option<String>,
149 /// The environment it names, once its needs are done (an expression
150 /// read by then). A job held by the environment's protection rules is
151 /// `pending` until they let it through.
152 #[serde(default)]
153 pub environment: Option<String>,
154 /// Its `runs-on` names self-hosted runners (see `runners`).
155 #[serde(default)]
156 pub self_hosted: bool,
157 /// The self-hosted runner that took it, by name.
158 #[serde(default)]
159 pub runner: Option<String>,
160 /// It was cancelled and is running its `if: always()` and `cancelled()`
161 /// steps and its post steps before it ends.
162 #[serde(default)]
163 pub cancelling: bool,
164 /// Where its deployment is (`environment.url`), for a job that deploys
165 /// and says; the current attempt's only.
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub environment_url: Option<String>,
168 /// For a job that calls a reusable workflow: that workflow's file. Its
169 /// jobs' keys start with this job's key and a `/`.
170 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub uses: Option<String>,
172}
173
174#[derive(Clone, Debug, Serialize, Deserialize)]
175#[serde(rename_all = "camelCase")]
176pub struct RunDetail {
177 pub run: WorkflowRun,
178 pub jobs: Vec<Job>,
179 /// The workflow's notes, as of the run's commit.
180 pub notes: Vec<WorkflowNote>,
181 /// For a run of a pull request from outside: whether it waits for, or
182 /// had, someone's approval (`status` is `action_required` while it waits).
183 #[serde(default)]
184 pub approval: Option<RunApproval>,
185 /// The environments whose protection rules hold its jobs, this attempt.
186 #[serde(default)]
187 pub pending_deployments: Vec<PendingDeployment>,
188 /// Every attempt of the run, oldest first, the one shown included.
189 /// `run.attempt` says which one `jobs` belong to.
190 #[serde(default)]
191 pub attempts: Vec<RunAttempt>,
192}
193
194/// One attempt of a run: the first, or a re-run.
195#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct RunAttempt {
198 /// From 1.
199 pub attempt: u64,
200 /// `queued`, `in_progress` or `completed`; earlier attempts are completed.
201 pub status: String,
202 pub conclusion: Option<String>,
203 /// Who started it: whoever caused the run for the first, whoever re-ran
204 /// it for the rest.
205 pub actor: Option<String>,
206 /// It ran with debug logging (`RUNNER_DEBUG=1`).
207 pub debug: bool,
208 pub started_at: Option<String>,
209 pub finished_at: Option<String>,
210}
211
212/// One job's summary: what its steps wrote to `$GITHUB_STEP_SUMMARY`, in
213/// Markdown, masked.
214#[derive(Clone, Debug, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct JobSummary {
217 /// The job's id, as `RunDetail.jobs` gives it for the attempt.
218 pub job_id: String,
219 pub name: String,
220 pub steps: Vec<StepSummary>,
221}
222
223#[derive(Clone, Debug, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct StepSummary {
226 /// The step, from 1 (post steps follow the job's own).
227 pub step: u32,
228 pub markdown: String,
229}
230
231/// A job's whole log, for downloading: its steps, to split the text by.
232#[derive(Clone, Debug, Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234pub struct JobLogText {
235 pub job_id: String,
236 pub name: String,
237 pub steps: Vec<StepState>,
238 pub chunks: Vec<LogChunk>,
239 /// Whether the job has finished.
240 pub done: bool,
241 /// Its log was left out: the run's logs reached `MAX_RUN_LOG_BYTES`.
242 #[serde(default)]
243 pub omitted: bool,
244}
245
246/// A run that needed approval before it started.
247#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct RunApproval {
250 /// `required` while it waits, then `approved`.
251 pub state: String,
252 /// Why it waits, in words.
253 pub reason: String,
254 /// Who approved it.
255 pub approved_by: Option<String>,
256}
257
258/// One person or team who may approve a job's deployment to an
259/// environment.
260#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
261pub struct EnvironmentReviewer {
262 /// `user` or `team`.
263 #[serde(rename = "type")]
264 pub kind: String,
265 /// A username, or a team's slug in the repository's workspace.
266 pub name: String,
267}
268
269/// A branch or tag pattern an environment lets deploy.
270#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
271pub struct BranchPattern {
272 /// fnmatch-style, as branch filters are: `main`, `release/*`, `v*`.
273 pub name: String,
274 /// `branch` or `tag`.
275 #[serde(rename = "type", default = "branch_kind")]
276 pub kind: String,
277}
278
279fn branch_kind() -> String {
280 "branch".to_owned()
281}
282
283/// The most reviewers an environment may have, as on GitHub.
284pub const MAX_ENVIRONMENT_REVIEWERS: usize = 6;
285/// The longest wait timer, in minutes: 30 days.
286pub const MAX_WAIT_MINUTES: u32 = 43_200;
287
288/// An environment and its protection rules. Jobs that name it with
289/// `environment:` wait until the rules let them through; only then does the
290/// job get the environment's secrets.
291#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
292#[serde(rename_all = "camelCase")]
293pub struct Environment {
294 /// Lowercase.
295 pub name: String,
296 /// Who may approve its jobs; none means no review is needed.
297 pub reviewers: Vec<EnvironmentReviewer>,
298 /// Whoever started a run may not approve its jobs, even as a reviewer.
299 pub prevent_self_review: bool,
300 /// Minutes each job waits before it may start.
301 pub wait_minutes: u32,
302 /// Which refs may deploy: `all`, `protected` (branches the rules
303 /// protect, the default branch included) or `selected` (`branch_patterns`).
304 pub branch_policy: String,
305 pub branch_patterns: Vec<BranchPattern>,
306 /// Admins may approve without being reviewers, which also skips the wait.
307 pub admins_bypass: bool,
308 /// Whether it has rules saved; false for one only named by a workflow,
309 /// a secret or a deployment.
310 pub protected: bool,
311 pub updated_at: Option<String>,
312 pub updated_by: Option<String>,
313}
314
315/// An environment holding a run's jobs, and where its rules stand.
316#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
317#[serde(rename_all = "camelCase")]
318pub struct PendingDeployment {
319 pub environment: String,
320 /// `waiting`, `approved` or `rejected`.
321 pub state: String,
322 /// Whether a reviewer must approve it before its jobs start.
323 pub needs_review: bool,
324 /// When its wait timer lets its jobs start, if it has one.
325 pub wait_until: Option<String>,
326 pub reviewers: Vec<EnvironmentReviewer>,
327 /// The jobs it holds, by name.
328 pub jobs: Vec<String>,
329 /// Whether the viewer may approve or reject it now.
330 #[serde(default)]
331 pub can_review: bool,
332 pub reviewed_by: Option<String>,
333 pub comment: Option<String>,
334 pub reviewed_at: Option<String>,
335}
336
337/// A repository's choices for its workflows.
338#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase")]
340pub struct ActionsSettings {
341 /// What a workflow without `permissions:` gets: `read` (contents and
342 /// packages read) or `write` (every permission). Unchosen, a repository
343 /// made before restricted tokens keeps `write`; a newer one takes its
344 /// workspace's default. Never more than the workspace's maximum.
345 pub default_permissions: String,
346 /// Whether the repository chose it, rather than taking it as above.
347 #[serde(default)]
348 pub default_chosen: bool,
349 /// The most the workspace lets a repository's default be.
350 #[serde(default = "write")]
351 pub max_permissions: String,
352 /// Which pull requests' runs wait for approval: `first_time_contributors`,
353 /// `outside_contributors` (the default) or `all_external_contributors`.
354 pub approval_policy: String,
355 /// Whether a job's token may open pull requests and approve them. Off
356 /// unless the repository turns it on, and only where the workspace
357 /// allows it.
358 #[serde(default)]
359 pub can_approve_pull_requests: bool,
360 /// Whether the workspace lets its repositories turn that on.
361 #[serde(default)]
362 pub workspace_allows_pull_requests: bool,
363 /// Who may use this repository's actions and reusable workflows from
364 /// their workflows, when it is private: `none` (only itself, the
365 /// default) or `organization` (private repositories of its workspace).
366 /// A public repository's are anyone's. See [`ACCESS_LEVELS`].
367 #[serde(default = "no_access")]
368 pub access_level: String,
369 /// Whether the repository's workflows run at all. Unchosen, on for a
370 /// repository and off for a fork (a repository with a parent), so a
371 /// fork starts no workflows until someone turns them on. While off, no
372 /// run starts from any event, schedule or dispatch.
373 #[serde(default = "on")]
374 pub enabled: bool,
375 /// Whether the repository chose `enabled`, rather than taking it as
376 /// above.
377 #[serde(default)]
378 pub enabled_chosen: bool,
379}
380
381fn on() -> bool {
382 true
383}
384
385/// Whether a repository's workflows run: as chosen, else on, and off for
386/// a fork.
387pub fn actions_enabled(chosen: Option<bool>, is_fork: bool) -> bool {
388 chosen.unwrap_or(!is_fork)
389}
390
391/// Why a workflow does not start by hand while a repository's Actions are
392/// off.
393pub fn actions_disabled_message(namespace: &str, name: &str) -> String {
394 format!("Actions is turned off for {namespace}/{name}. Turn it on in Settings → Actions.")
395}
396
397fn no_access() -> String {
398 "none".to_owned()
399}
400
401/// The values of `access_level`. `user` is read as `organization`: a
402/// personal account's repositories are its own workspace's.
403pub const ACCESS_LEVELS: [&str; 2] = ["none", "organization"];
404
405/// `access_level` as given, as one of [`ACCESS_LEVELS`]; None when it is
406/// not one.
407pub fn access_level(given: &str) -> Option<&'static str> {
408 match given.trim().to_ascii_lowercase().as_str() {
409 "none" | "" => Some("none"),
410 "organization" | "user" | "workspace" => Some("organization"),
411 _ => None,
412 }
413}
414
415fn write() -> String {
416 "write".to_owned()
417}
418
419/// A workspace's policy for its repositories' tokens.
420#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
421#[serde(rename_all = "camelCase")]
422pub struct WorkspaceActionsSettings {
423 /// What a repository made from now on gets by default: `read` (the
424 /// default) or `write`.
425 pub default_permissions: String,
426 /// The most any repository's default may be: `write` (the default) or
427 /// `read`, which holds every repository to read-only.
428 pub max_permissions: String,
429 /// Whether its repositories may let jobs open and approve pull
430 /// requests. Off by default.
431 pub can_approve_pull_requests: bool,
432}
433
434/// `workspace_actions_settings`: members only. Returns
435/// `Outcome<WorkspaceActionsSettings>`.
436#[derive(Debug, Serialize, Deserialize)]
437pub struct WorkspaceActionsSettingsArgs {
438 pub viewer: Viewer,
439 pub workspace: String,
440}
441
442/// `set_workspace_actions_settings`: owners only. Fields left out stay as
443/// they are. Returns `Outcome<WorkspaceActionsSettings>`.
444#[derive(Debug, Serialize, Deserialize)]
445#[serde(rename_all = "camelCase")]
446pub struct SetWorkspaceActionsSettingsArgs {
447 pub actor: User,
448 pub workspace: String,
449 #[serde(default)]
450 pub default_permissions: Option<String>,
451 #[serde(default)]
452 pub max_permissions: Option<String>,
453 #[serde(default)]
454 pub can_approve_pull_requests: Option<bool>,
455}
456
457/// The approval policies, least strict first.
458pub const APPROVAL_POLICIES: [&str; 3] = ["first_time_contributors", "outside_contributors", "all_external_contributors"];
459
460/// `actions_settings`. Returns `Outcome<ActionsSettings>`; anyone who can
461/// read the repository may see them.
462#[derive(Debug, Serialize, Deserialize)]
463pub struct ActionsSettingsArgs {
464 pub viewer: Viewer,
465 pub repo: RepoPath,
466}
467
468/// `set_actions_settings`: Admins only. Fields left out stay as they are.
469/// Returns `Outcome<ActionsSettings>`.
470#[derive(Debug, Serialize, Deserialize)]
471#[serde(rename_all = "camelCase")]
472pub struct SetActionsSettingsArgs {
473 pub actor: User,
474 pub repo: RepoPath,
475 /// `read` or `write`; `inherit` goes back to the workspace's (or, for a
476 /// repository made before restricted tokens, `write`).
477 #[serde(default)]
478 pub default_permissions: Option<String>,
479 #[serde(default)]
480 pub approval_policy: Option<String>,
481 #[serde(default)]
482 pub can_approve_pull_requests: Option<bool>,
483 /// `none`, or `organization` (`user` reads the same). See
484 /// [`ActionsSettings::access_level`].
485 #[serde(default)]
486 pub access_level: Option<String>,
487 /// Turns the repository's workflows on or off. See
488 /// [`ActionsSettings::enabled`].
489 #[serde(default)]
490 pub enabled: Option<bool>,
491}
492
493/// `environments`: every environment a repository's workflows, secrets,
494/// deployments or rules name, with its rules. `environment`: one, by
495/// `name`. Returns `Outcome<Vec<Environment>>` and `Outcome<Environment>`.
496#[derive(Debug, Serialize, Deserialize)]
497pub struct EnvironmentsArgs {
498 pub viewer: Viewer,
499 pub repo: RepoPath,
500 #[serde(default)]
501 pub name: Option<String>,
502}
503
504/// `set_environment`: create an environment's rules or change them. Fields
505/// left out stay as they are (none, for a new one). Admins only. Returns
506/// `Outcome<Environment>`.
507#[derive(Debug, Serialize, Deserialize)]
508#[serde(rename_all = "camelCase")]
509pub struct SetEnvironmentArgs {
510 pub actor: User,
511 pub repo: RepoPath,
512 pub name: String,
513 #[serde(default)]
514 pub reviewers: Option<Vec<EnvironmentReviewer>>,
515 #[serde(default)]
516 pub prevent_self_review: Option<bool>,
517 #[serde(default)]
518 pub wait_minutes: Option<u32>,
519 #[serde(default)]
520 pub branch_policy: Option<String>,
521 #[serde(default)]
522 pub branch_patterns: Option<Vec<BranchPattern>>,
523 #[serde(default)]
524 pub admins_bypass: Option<bool>,
525}
526
527/// `delete_environment`: its rules go; jobs naming it run without them.
528/// Its secrets' rows stay. Admins only. Returns `Outcome<bool>`.
529#[derive(Debug, Serialize, Deserialize)]
530pub struct DeleteEnvironmentArgs {
531 pub actor: User,
532 pub repo: RepoPath,
533 pub name: String,
534}
535
536/// `pending_deployments`: the environments holding a run's jobs. Returns
537/// `Outcome<Vec<PendingDeployment>>`.
538#[derive(Debug, Serialize, Deserialize)]
539pub struct PendingDeploymentsArgs {
540 pub viewer: Viewer,
541 pub repo: RepoPath,
542 pub id: String,
543}
544
545/// `review_deployments`: approve or reject a run's jobs for `environments`
546/// (every one waiting, if empty). Returns `Outcome<Vec<PendingDeployment>>`.
547#[derive(Debug, Serialize, Deserialize)]
548pub struct ReviewDeploymentsArgs {
549 pub actor: User,
550 pub repo: RepoPath,
551 pub id: String,
552 #[serde(default)]
553 pub environments: Vec<String>,
554 /// `approved` or `rejected`.
555 pub state: String,
556 #[serde(default)]
557 pub comment: Option<String>,
558}
559
560/// `repository_dispatch`: start the default branch's workflows that run
561/// `on: repository_dispatch` for `event_type`. Needs the Write role (a
562/// token's `code:write`). Returns `Outcome<u32>`: how many started.
563#[derive(Debug, Serialize, Deserialize)]
564#[serde(rename_all = "camelCase")]
565pub struct RepositoryDispatchArgs {
566 pub actor: User,
567 pub repo: RepoPath,
568 pub event_type: String,
569 #[serde(default)]
570 pub client_payload: Value,
571}
572
573#[derive(Clone, Debug, Serialize, Deserialize)]
574#[serde(rename_all = "camelCase")]
575pub struct LogChunk {
576 pub seq: u64,
577 /// The step it belongs to, from 1; 0 for the job's setup.
578 pub step: u32,
579 pub text: String,
580}
581
582#[derive(Clone, Debug, Serialize, Deserialize)]
583#[serde(rename_all = "camelCase")]
584pub struct JobLog {
585 pub chunks: Vec<LogChunk>,
586 /// Whether the job has finished, so no more will come.
587 pub done: bool,
588}
589
590/// Who may read a secret or variable: workflows (`secrets.*` and `vars.*`
591/// in GitHub Actions) and deployments (a deploy build's environment and the
592/// running app's bindings). Agents, checks and the merge queue read none.
593pub const CONSUMERS: [&str; 2] = ["workflows", "deployments"];
594
595/// One row of a repository's or workspace's secrets and variables, as
596/// Vercel lists environment variables: a key, its type, the environments
597/// it applies to and who reads it. A key may have one row per environment.
598/// Secrets' values are never returned.
599#[derive(Clone, Debug, Serialize, Deserialize)]
600#[serde(rename_all = "camelCase")]
601pub struct Setting {
602 #[serde(default)]
603 pub id: String,
604 pub name: String,
605 /// `secret`, or `variable` (shown as Config).
606 #[serde(default)]
607 pub kind: String,
608 /// A variable's value; secrets' are never returned.
609 pub value: Option<String>,
610 /// `project` (a repository's, which belong to its project) or
611 /// `workspace`.
612 pub scope: String,
613 pub updated_at: String,
614 /// `workflows` and/or `deployments`.
615 #[serde(default)]
616 pub available_to: Vec<String>,
617 /// The environments it applies to; empty is every environment.
618 #[serde(default)]
619 pub environments: Vec<String>,
620 /// A workspace's row: the projects it reaches, by slug; empty is every
621 /// project.
622 #[serde(default)]
623 pub projects: Vec<String>,
624 #[serde(default)]
625 pub note: Option<String>,
626 #[serde(default)]
627 pub updated_by: Option<String>,
628 /// A brokered secret's hosts: a job gets a stand-in for it, and g1t
629 /// puts the real value on the job's requests to these hosts as they
630 /// leave its sandbox. Empty for a secret that is not brokered, and for
631 /// config.
632 #[serde(default)]
633 pub brokered_hosts: Vec<String>,
634}
635
636/// How a brokered secret's stand-in starts. The rest is 48 random hex
637/// characters, so the whole is shaped like a token and tools take it.
638pub const STAND_IN_PREFIX: &str = "g1tsec_";
639
640/// A stand-in a running job was given for a brokered secret, as the runner
641/// Worker asks for them (`brokered_secrets`) to fill in the real value on
642/// the job's requests to `hosts`.
643#[derive(Clone, Debug, Serialize, Deserialize)]
644#[serde(rename_all = "camelCase")]
645pub struct BrokeredSecret {
646 pub stand_in: String,
647 /// The secret's key, for the job's log.
648 pub name: String,
649 pub value: String,
650 pub hosts: Vec<String>,
651}
652
653// --- Methods ---------------------------------------------------------------
654
655/// `workflows`. Returns `Outcome<Vec<Workflow>>`.
656#[derive(Debug, Serialize, Deserialize)]
657pub struct WorkflowsArgs {
658 pub repo: RepoPath,
659 pub viewer: Viewer,
660}
661
662/// `workflow`: one workflow, named by its id (`wfl_…`), its file name
663/// (`ci.yml`), its path or its name, with its file as of `ref` (the
664/// default branch when absent). Returns `Outcome<WorkflowDetail>`.
665#[derive(Debug, Serialize, Deserialize)]
666pub struct WorkflowArgs {
667 pub repo: RepoPath,
668 pub viewer: Viewer,
669 pub workflow: String,
670 #[serde(default, rename = "ref")]
671 pub git_ref: Option<String>,
672}
673
674/// A workflow with its file.
675#[derive(Clone, Debug, Serialize, Deserialize)]
676#[serde(rename_all = "camelCase")]
677pub struct WorkflowDetail {
678 #[serde(flatten)]
679 pub workflow: Workflow,
680 /// The branch or tag the file was read from.
681 #[serde(rename = "ref")]
682 pub git_ref: String,
683 /// The file, as written on `ref`.
684 pub source: String,
685}
686
687/// `runs`: newest first. Returns `Outcome<Vec<WorkflowRun>>`.
688#[derive(Debug, Serialize, Deserialize)]
689pub struct RunsArgs {
690 pub repo: RepoPath,
691 pub viewer: Viewer,
692 /// A workflow's id or file name.
693 #[serde(default)]
694 pub workflow: Option<String>,
695 #[serde(default)]
696 pub branch: Option<String>,
697 #[serde(default)]
698 pub event: Option<String>,
699 /// The pull request's number.
700 #[serde(default)]
701 pub pull: Option<u32>,
702 #[serde(default)]
703 pub sha: Option<String>,
704 /// GitHub's `status` filter: a status (`queued`, `in_progress`,
705 /// `waiting`, `completed`) or a conclusion (`success`, `failure`,
706 /// `cancelled`, `skipped`, `timed_out`, `neutral`, `action_required`).
707 #[serde(default)]
708 pub status: Option<String>,
709 /// RFC 3339: only runs created at or after it, and before `until`.
710 #[serde(default)]
711 pub since: Option<String>,
712 #[serde(default)]
713 pub until: Option<String>,
714 #[serde(default)]
715 pub limit: Option<u32>,
716}
717
718/// `run`. Returns `Outcome<RunDetail>`.
719#[derive(Debug, Serialize, Deserialize)]
720pub struct RunArgs {
721 pub repo: RepoPath,
722 pub viewer: Viewer,
723 pub id: String,
724 /// An earlier attempt; the latest when absent.
725 #[serde(default)]
726 pub attempt: Option<u64>,
727}
728
729/// `summaries`: the job summaries of a run's attempt (the latest when
730/// `attempt` is absent), jobs in the run's order, those with none left
731/// out. Returns `Outcome<Vec<JobSummary>>`.
732#[derive(Debug, Serialize, Deserialize)]
733pub struct SummariesArgs {
734 pub repo: RepoPath,
735 pub viewer: Viewer,
736 pub id: String,
737 #[serde(default)]
738 pub attempt: Option<u64>,
739}
740
741/// `job_log_text`: one job's whole log, any attempt's (by the id the run
742/// gave the job). Returns `Outcome<JobLogText>`.
743#[derive(Debug, Serialize, Deserialize)]
744pub struct JobLogTextArgs {
745 pub repo: RepoPath,
746 pub viewer: Viewer,
747 pub job: String,
748}
749
750/// `run_logs`: every job's whole log for an attempt of a run (the latest
751/// when `attempt` is absent), until they add up to `MAX_RUN_LOG_BYTES`;
752/// jobs past it come `omitted`, with no chunks. Returns
753/// `Outcome<Vec<JobLogText>>`.
754#[derive(Debug, Serialize, Deserialize)]
755pub struct RunLogsArgs {
756 pub repo: RepoPath,
757 pub viewer: Viewer,
758 pub id: String,
759 #[serde(default)]
760 pub attempt: Option<u64>,
761}
762
763/// The most log `run_logs` returns at once, in bytes.
764pub const MAX_RUN_LOG_BYTES: usize = 24 * 1024 * 1024;
765
766/// `logs`: a job's log after `after`, or with `tail` its last chunks.
767/// Returns `Outcome<JobLog>`.
768#[derive(Debug, Serialize, Deserialize)]
769pub struct LogsArgs {
770 pub repo: RepoPath,
771 pub viewer: Viewer,
772 pub job: String,
773 #[serde(default)]
774 pub after: u64,
775 /// The last this many chunks (at most [`MAX_LOG_PAGE`]) instead: where
776 /// a failed job says why. `after` is ignored with it.
777 #[serde(default, skip_serializing_if = "Option::is_none")]
778 pub tail: Option<u32>,
779}
780
781/// The most chunks of a log one `logs` call returns.
782pub const MAX_LOG_PAGE: u32 = 500;
783
784/// `dispatch`: run a workflow that has `workflow_dispatch`. Members only.
785/// Returns `Outcome<WorkflowRun>`.
786#[derive(Debug, Serialize, Deserialize)]
787pub struct DispatchArgs {
788 pub actor: User,
789 pub repo: RepoPath,
790 /// A workflow's id or file name.
791 pub workflow: String,
792 /// A branch or tag; the default branch when absent.
793 #[serde(default, rename = "ref")]
794 pub git_ref: Option<String>,
795 #[serde(default)]
796 pub inputs: serde_json::Map<String, Value>,
797}
798
799/// `cancel` and `rerun`: every job, with `failed_only` the ones that did
800/// not succeed, or with `job` that one job (by its id in the run's latest
801/// attempt); each with the jobs that need them. `debug` runs the new
802/// attempt with debug logging. Members only. Returns `Outcome<WorkflowRun>`.
803#[derive(Debug, Serialize, Deserialize)]
804pub struct RunActionArgs {
805 pub actor: User,
806 pub repo: RepoPath,
807 pub id: String,
808 #[serde(default)]
809 pub failed_only: bool,
810 #[serde(default)]
811 pub job: Option<String>,
812 #[serde(default)]
813 pub debug: bool,
814 /// `cancel`: stop running jobs outright, without their cleanup steps.
815 #[serde(default)]
816 pub force: bool,
817}
818
819/// `set_workflow_enabled`. Members only. Returns `Outcome<Workflow>`.
820#[derive(Debug, Serialize, Deserialize)]
821pub struct SetWorkflowEnabledArgs {
822 pub actor: User,
823 pub repo: RepoPath,
824 pub workflow: String,
825 pub enabled: bool,
826}
827
828/// Whose secrets or variables: a repository's, or with only `workspace`,
829/// a workspace's.
830#[derive(Clone, Debug, Serialize, Deserialize)]
831pub struct SettingsOwner {
832 #[serde(default)]
833 pub repo: Option<RepoPath>,
834 #[serde(default)]
835 pub workspace: Option<String>,
836}
837
838/// `settings`: the secrets (`kind: secret`) or variables (`kind: variable`)
839/// of a repository, with its workspace's, or of a workspace. Members only.
840/// Returns `Outcome<Vec<Setting>>`.
841#[derive(Debug, Serialize, Deserialize)]
842pub struct SettingsArgs {
843 pub actor: User,
844 #[serde(flatten)]
845 pub owner: SettingsOwner,
846 pub kind: String,
847}
848
849/// `set_setting`: add or replace one. A repository's need a member; a
850/// workspace's an owner. Returns `Outcome<Setting>`.
851#[derive(Debug, Serialize, Deserialize)]
852pub struct SetSettingArgs {
853 pub actor: User,
854 #[serde(flatten)]
855 pub owner: SettingsOwner,
856 /// `secret` or `variable`. Changing a variable's row to `secret` seals
857 /// it; a secret cannot become a variable.
858 pub kind: String,
859 pub name: String,
860 /// The row to change. Left out, the key's row for every environment, as
861 /// GitHub's API addresses a secret by name alone.
862 #[serde(default)]
863 pub id: Option<String>,
864 /// Needed for a new row; left out, an existing row keeps its value.
865 #[serde(default)]
866 pub value: Option<String>,
867 /// `workflows` and/or `deployments`; left out, unchanged (both, for a
868 /// new row).
869 // Named as callers send it: an `alias` is not honoured beside the
870 // flattened owner in the Worker's build.
871 #[serde(default, rename = "availableTo")]
872 pub available_to: Option<Vec<String>>,
873 /// The environments it applies to; empty is every one. Left out,
874 /// unchanged.
875 #[serde(default)]
876 pub environments: Option<Vec<String>>,
877 /// A workspace's row: project slugs; empty for every one.
878 #[serde(default)]
879 pub projects: Option<Vec<String>>,
880 #[serde(default)]
881 pub note: Option<String>,
882 /// A secret's brokered hosts (`Setting::brokered_hosts`); empty stops
883 /// brokering it. Left out, unchanged.
884 #[serde(default, rename = "brokeredHosts")]
885 pub brokered_hosts: Option<Vec<String>>,
886}
887
888/// `set_managed_workflow`: installs, or replaces, a managed workflow in a
889/// repository (`g1t_actions::managed`): one that runs as if it were in
890/// `.g1t/workflows/` without being committed there, under the path
891/// `managed/<name>.yml`. For g1t's own services; never reached from the API.
892/// Refused when `source` is not a workflow g1t can read. Returns `bool`.
893#[derive(Debug, Serialize, Deserialize)]
894#[serde(rename_all = "camelCase")]
895pub struct SetManagedWorkflowArgs {
896 pub repo_id: String,
897 /// Letters, digits and hyphens: the file name its path ends with.
898 pub name: String,
899 pub source: String,
900 /// What keeps it: `deployments`, or a workspace's id.
901 pub origin: String,
902}
903
904/// `set_managed_deploy`: installs, replaces or removes the deploy workflow
905/// a project's deployments run through (`g1t_actions::managed`), named
906/// `deploy-<project>`, from the project's settings. Nothing deploys (both
907/// `previews` and `production` off) removes it. For the deployments
908/// service only. Returns `bool`, whether one is installed now.
909#[derive(Debug, Serialize, Deserialize)]
910#[serde(rename_all = "camelCase")]
911pub struct SetManagedDeployArgs {
912 pub repo_id: String,
913 pub project: String,
914 pub production_branch: String,
915 #[serde(default)]
916 pub root_dir: String,
917 #[serde(default)]
918 pub build_command: String,
919 #[serde(default)]
920 pub output_dir: String,
921 pub previews: bool,
922 pub production: bool,
923}
924
925/// A workflow a workspace's owners added to its repositories
926/// (`g1t_actions::managed::Target`): it runs in each repository it targets
927/// as that repository's own, with its token and secrets, under the path
928/// `managed/<name>.yml`, from a copy of its source taken when it was saved.
929#[derive(Clone, Debug, Serialize, Deserialize)]
930#[serde(rename_all = "camelCase")]
931pub struct WorkspaceManagedWorkflow {
932 /// Lowercase letters, digits and hyphens: its path is
933 /// `managed/<name>.yml`.
934 pub name: String,
935 pub path: String,
936 /// Its `name:`, as repositories list it; a repository's workflow of
937 /// this name takes a default one's place.
938 pub workflow_name: String,
939 /// Where it was copied from: `owner/repo/path@ref`, a repository of the
940 /// same workspace.
941 pub source: String,
942 /// The commit it was copied at.
943 pub source_sha: Option<String>,
944 /// Whether the source repository is private, so the workflow runs in
945 /// private repositories only.
946 pub source_private: bool,
947 /// `required` or `default`.
948 pub mode: String,
949 /// Names and patterns of the repositories it runs in; empty is every one.
950 pub repositories: Vec<String>,
951 /// `all`, `public` or `private`.
952 pub visibility: String,
953 /// The copy that runs.
954 pub content: String,
955 pub created_at: String,
956 pub updated_at: String,
957 pub updated_by: String,
958}
959
960/// `workspace_managed_workflows`: owners only. Returns
961/// `Outcome<Vec<WorkspaceManagedWorkflow>>`.
962#[derive(Debug, Serialize, Deserialize)]
963pub struct WorkspaceManagedWorkflowsArgs {
964 pub viewer: Viewer,
965 pub workspace: String,
966}
967
968/// `set_workspace_managed_workflow`: owners only. Adds one (`create`, which
969/// needs `source` and `mode`) or changes one; fields left out stay as they
970/// are. The source is copied again when it is given or `refresh` is set.
971/// Refused when the source is not a workflow file in a repository of the
972/// workspace, or is private while the workflow could run in a public
973/// repository. Returns `Outcome<WorkspaceManagedWorkflow>`.
974#[derive(Debug, Serialize, Deserialize)]
975#[serde(rename_all = "camelCase")]
976pub struct SetWorkspaceManagedWorkflowArgs {
977 pub actor: User,
978 pub workspace: String,
979 pub name: String,
980 #[serde(default)]
981 pub create: bool,
982 /// `owner/repo/.g1t/workflows/file.yml@ref`.
983 #[serde(default)]
984 pub source: Option<String>,
985 #[serde(default)]
986 pub mode: Option<String>,
987 #[serde(default)]
988 pub repositories: Option<Vec<String>>,
989 #[serde(default)]
990 pub visibility: Option<String>,
991 #[serde(default)]
992 pub refresh: bool,
993}
994
995/// `remove_workspace_managed_workflow`: owners only. Its runs stay.
996/// Returns `Outcome<bool>`, whether there was one.
997#[derive(Debug, Serialize, Deserialize)]
998pub struct RemoveWorkspaceManagedWorkflowArgs {
999 pub actor: User,
1000 pub workspace: String,
1001 pub name: String,
1002}
1003
1004/// `remove_managed_workflow`: takes a managed workflow out of a repository.
1005/// Its runs stay. Returns `bool`, whether there was one.
1006#[derive(Debug, Serialize, Deserialize)]
1007#[serde(rename_all = "camelCase")]
1008pub struct RemoveManagedWorkflowArgs {
1009 pub repo_id: String,
1010 pub name: String,
1011}
1012
1013/// `resolve_settings`: the secrets and variables one reader gets, for the
1014/// services that hand them out (the deployments service). Returns
1015/// `ResolvedSettings`.
1016#[derive(Debug, Serialize, Deserialize)]
1017#[serde(rename_all = "camelCase")]
1018pub struct ResolveSettingsArgs {
1019 pub repo_id: String,
1020 pub repo: RepoPath,
1021 /// The project being read for; its repository's primary project if left
1022 /// out.
1023 #[serde(default)]
1024 pub project_id: Option<String>,
1025 #[serde(default)]
1026 pub project_slug: Option<String>,
1027 /// `workflows` or `deployments`.
1028 pub consumer: String,
1029 /// The environment being read for, such as `production` or `preview`.
1030 #[serde(default)]
1031 pub environment: Option<String>,
1032 /// Whether the run is trusted; an untrusted one gets no secrets.
1033 pub trusted: bool,
1034}
1035
1036#[derive(Debug, Default, Serialize, Deserialize)]
1037pub struct ResolvedSettings {
1038 pub secrets: serde_json::Map<String, serde_json::Value>,
1039 pub variables: serde_json::Map<String, serde_json::Value>,
1040}
1041
1042/// `delete_setting`. Returns `Outcome<bool>`.
1043#[derive(Debug, Serialize, Deserialize)]
1044pub struct DeleteSettingArgs {
1045 pub actor: User,
1046 #[serde(flatten)]
1047 pub owner: SettingsOwner,
1048 pub kind: String,
1049 pub name: String,
1050 /// One row; left out, every row of the key.
1051 #[serde(default)]
1052 pub id: Option<String>,
1053}
1054
1055/// `job_spec` and `job_report`: the sandbox running a job, with the job's
1056/// own token. `report` is one of:
1057/// `{"kind": "step", "number", "status", "conclusion"}`,
1058/// `{"kind": "log", "step", "text"}`,
1059/// `{"kind": "annotation", "level", "message", "title", "file", "line"}`,
1060/// `{"kind": "done", "conclusion", "outputs", "reason"}`.
1061#[derive(Debug, Serialize, Deserialize)]
1062pub struct JobCallArgs {
1063 pub job: String,
1064 pub token: String,
1065 #[serde(default)]
1066 pub report: Value,
1067 /// `job_spec` only: the harness takes a new token with the spec, and
1068 /// the one it asked with stops working, so a copy of it left on the
1069 /// runner's computer cannot fetch the job's secrets again. Harnesses
1070 /// that predate it never ask, and keep their token.
1071 #[serde(default)]
1072 pub rotate: bool,
1073 /// `job_spec` only: the harness on a self-hosted runner can pass the
1074 /// job's requests to a brokered secret's hosts through g1t
1075 /// (`POST /actions/jobs/{job}/broker`), so its spec lists each
1076 /// stand-in's hosts, never a value. Harnesses that predate it never
1077 /// ask, and their job's log says the stand-ins will not be filled in.
1078 #[serde(default)]
1079 pub broker: bool,
1080}
1081
1082/// What the runner needs to start a job's sandbox.
1083#[derive(Debug, Serialize, Deserialize)]
1084#[serde(rename_all = "camelCase")]
1085pub struct StartJobArgs {
1086 pub job: String,
1087 pub token: String,
1088 pub repo: RepoPath,
1089 /// Minutes before the job is stopped.
1090 pub timeout_minutes: u32,
1091 /// The machine its `runs-on` asked for, by label (`instance_for`):
1092 /// `g1t-2core` or `g1t-4core`, which is also what `ubuntu-latest`
1093 /// gets in a public repository; absent, the standard one.
1094 #[serde(default)]
1095 pub instance: Option<String>,
1096 /// Whether the job may be given brokered secrets, so the runner sends
1097 /// its HTTP and HTTPS through the Worker, which fills them in.
1098 #[serde(default)]
1099 pub brokered: bool,
1100}
1101
1102// ── The machine catalogue ─────────────────────────────────────────────────
1103//
1104// `INSTANCE_TYPES` is the one table of g1t's hosted machines: each row is a
1105// `runs-on` label, the machine it bills as, the runner's Container class
1106// that runs it, the Cloudflare Containers instance type behind that class,
1107// its size, and its price a minute. Its TypeScript mirror is
1108// packages/contracts/src/instances.ts, and services/runner/wrangler.jsonc
1109// gives each class its instance type; tests hold the three together.
1110// Adding a size, renaming a label or pointing a label at another instance
1111// type is a row here and in the mirror, and a class in wrangler.jsonc
1112// (CONTRIBUTING.md, "Adding a machine size").
1113
1114/// A label a workflow job can ask for in `runs-on`, and the machine g1t
1115/// runs it on. Each machine is a named Cloudflare Containers instance type
1116/// (no custom types): the standard machine is standard-3 (2 vCPU, 8 GiB,
1117/// 16 GB of disk), `g1t-2core` is kept as a name of it so workflows that
1118/// ask for it keep working, and `g1t-4core` is standard-4 (4 vCPU, 12 GiB,
1119/// 20 GB).
1120///
1121/// `ubuntu-latest` and every other label not named here follow GitHub's
1122/// hosted runners ([`instance_for`]): in a public repository the 4-core
1123/// machine (GitHub's is 4 vCPU and 16 GB; 12 GiB is the most a Containers
1124/// instance has without a raised limit), in a private one the standard
1125/// machine (GitHub's is 2 vCPU and 8 GB).
1126#[derive(Clone, Copy, Debug, PartialEq)]
1127pub struct InstanceType {
1128 /// The `runs-on` label, or `standard` for the default.
1129 pub label: &'static str,
1130 /// The machine, as it is billed and reported: runner minutes are
1131 /// counted by machine (`2-core`), so a label renamed or pointed at
1132 /// another machine leaves the history where it was. Never reused for a
1133 /// different machine.
1134 pub machine: &'static str,
1135 /// The runner's Durable Object class that runs it, one per label (a
1136 /// class has one instance type and its own `max_instances`).
1137 pub class: &'static str,
1138 /// The named Containers instance type its class runs on.
1139 pub container: &'static str,
1140 pub vcpu: f64,
1141 pub memory_gib: f64,
1142 pub disk_gb: f64,
1143 /// What a minute of it is charged, in millionths of a dollar: a job is
1144 /// charged its minutes, rounded up, at this rate (`runner_minutes`).
1145 /// The machine's cost a minute at Cloudflare's list prices with half
1146 /// its vCPUs busy, plus the 20% margin, rounded up to a tenth of a cent,
1147 /// and never under its cost with every vCPU busy (Cloudflare bills
1148 /// memory, disk and the Durable Object for every second, CPU as used).
1149 pub micros_per_minute: i64,
1150}
1151
1152impl InstanceType {
1153 /// Its price as a multiple of the standard machine's.
1154 pub fn price_scale(&self) -> f64 {
1155 self.micros_per_minute as f64 / STANDARD_INSTANCE.micros_per_minute as f64
1156 }
1157
1158 /// `2 vCPU, 8 GiB, 16 GB`.
1159 pub fn size(&self) -> String {
1160 format!("{} vCPU, {} GiB, {} GB", self.vcpu, self.memory_gib, self.disk_gb)
1161 }
1162}
1163
1164/// The default in a private repository: what `ubuntu-latest` and every
1165/// other hosted label get there, as on GitHub. Checks and the merge queue
1166/// run on it too.
1167pub const STANDARD_INSTANCE: InstanceType = InstanceType {
1168 label: "standard",
1169 machine: "2-core",
1170 class: "AttemptSandbox",
1171 container: "standard-3",
1172 vcpu: 2.0,
1173 memory_gib: 8.0,
1174 disk_gb: 16.0,
1175 micros_per_minute: 4_000,
1176};
1177
1178/// Every label a workflow job can ask for, the default first.
1179pub const INSTANCE_TYPES: [InstanceType; 3] = [
1180 STANDARD_INSTANCE,
1181 // The standard machine under the name it had when it was larger.
1182 InstanceType { label: "g1t-2core", class: "Sandbox2Core", ..STANDARD_INSTANCE },
1183 InstanceType {
1184 label: "g1t-4core",
1185 machine: "4-core",
1186 class: "Sandbox4Core",
1187 container: "standard-4",
1188 vcpu: 4.0,
1189 memory_gib: 12.0,
1190 disk_gb: 20.0,
1191 micros_per_minute: 7_000,
1192 },
1193];
1194
1195/// The machines, once each, in the catalogue's order: the first row of
1196/// each.
1197pub fn machines() -> Vec<InstanceType> {
1198 let mut seen: Vec<InstanceType> = Vec::new();
1199 for instance in INSTANCE_TYPES {
1200 if !seen.iter().any(|m| m.machine == instance.machine) {
1201 seen.push(instance);
1202 }
1203 }
1204 seen
1205}
1206
1207/// A machine by its key (`2-core`), if it is one.
1208pub fn machine_named(machine: &str) -> Option<InstanceType> {
1209 INSTANCE_TYPES.iter().find(|instance| instance.machine == machine).copied()
1210}
1211
1212/// The minutes a job on g1t's machines is charged for: its time rounded up
1213/// to the whole minute, so 61 seconds is 2 minutes and any job that ran is
1214/// at least 1. Nothing for a job that never ran.
1215pub fn runner_minutes(seconds: i64) -> i64 {
1216 if seconds <= 0 { 0 } else { (seconds + 59) / 60 }
1217}
1218
1219/// The label of the machine `ubuntu-latest` gets in a public repository:
1220/// GitHub gives public repositories 4 vCPU.
1221pub const PUBLIC_DEFAULT_LABEL: &str = "g1t-4core";
1222
1223/// The machine a job's `runs-on` labels ask for: the dearest named (the
1224/// later row when two cost the same). With none named (`ubuntu-latest`,
1225/// say), what GitHub gives: the 4-core machine in a public repository,
1226/// the standard one in a private one. Labels compare without regard to
1227/// case.
1228pub fn instance_for(labels: &[String], public: bool) -> InstanceType {
1229 INSTANCE_TYPES
1230 .iter()
1231 .filter(|instance| instance.label != STANDARD_INSTANCE.label && labels.iter().any(|label| label.trim().eq_ignore_ascii_case(instance.label)))
1232 .max_by_key(|instance| instance.micros_per_minute)
1233 .copied()
1234 .unwrap_or_else(|| if public { instance_named(PUBLIC_DEFAULT_LABEL).unwrap_or(STANDARD_INSTANCE) } else { STANDARD_INSTANCE })
1235}
1236
1237/// An instance type by its label, if it is one.
1238pub fn instance_named(label: &str) -> Option<InstanceType> {
1239 INSTANCE_TYPES.iter().find(|instance| instance.label.eq_ignore_ascii_case(label.trim())).copied()
1240}
1241
1242// ── The cache (actions/cache) ─────────────────────────────────────────────
1243//
1244// Entries are kept in R2 by the API (the ACTIONS_CACHE bucket) and listed
1245// here, by the actions service, which decides what is found, what fits and
1246// what is evicted. A sandbox reaches these through the API with its job's
1247// token: `/actions/jobs/{job}/cache` (see apps/api/src/blobs.rs).
1248
1249/// The largest one cache entry may be, compressed.
1250pub const CACHE_MAX_ENTRY_BYTES: u64 = 2 * 1024 * 1024 * 1024;
1251/// What one repository's entries may hold together. Saving past it evicts
1252/// the entries restored longest ago.
1253pub const CACHE_REPO_QUOTA_BYTES: u64 = 10 * 1024 * 1024 * 1024;
1254/// An entry not restored for this long is deleted.
1255pub const CACHE_UNUSED_DAYS: u64 = 7;
1256/// An entry is deleted this long after it was saved, however often it is
1257/// restored (the bucket's own lifecycle rule deletes objects at 30 days).
1258pub const CACHE_MAX_AGE_DAYS: u64 = 28;
1259/// An upload is sent in parts of this size (the last may be smaller).
1260pub const CACHE_PART_BYTES: u64 = 32 * 1024 * 1024;
1261/// What R2 charges g1t to store a GB for a month, in millionths of a
1262/// dollar ($0.015): what the cache's storage is charged at, plus the margin.
1263pub const CACHE_MICROS_PER_GB_MONTH: i64 = 15_000;
1264
1265/// `cache_lookup`: the entry a job restores: its key exactly, else the
1266/// newest whose key starts with one of `restore`, in order.
1267/// Returns `Outcome<Option<CacheHit>>`.
1268#[derive(Debug, Serialize, Deserialize)]
1269pub struct CacheLookupArgs {
1270 pub job: String,
1271 pub token: String,
1272 pub key: String,
1273 #[serde(default)]
1274 pub restore: Vec<String>,
1275 /// The entry's version, a hash of its paths and compression, as the
1276 /// toolkit's client and g1t's runner both send it: only an entry of the
1277 /// same version is found. `None` from runners that send none, whose
1278 /// entries have none.
1279 #[serde(default)]
1280 pub version: Option<String>,
1281}
1282
1283#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1284pub struct CacheHit {
1285 pub key: String,
1286 pub object: String,
1287 pub size: u64,
1288 /// When it was saved, RFC 3339.
1289 #[serde(default)]
1290 pub created_at: String,
1291 /// A signed token for downloading it through the toolkit's blob
1292 /// endpoint, when the lookup came with a version.
1293 #[serde(default)]
1294 pub blob: Option<String>,
1295}
1296
1297/// `cache_reserve`: a job about to save `size` bytes under `key`. Refused
1298/// when the key is taken (`conflict`: keys are written once) or the entry
1299/// is too large. Returns `Outcome<CacheReservation>`.
1300#[derive(Debug, Serialize, Deserialize)]
1301pub struct CacheReserveArgs {
1302 pub job: String,
1303 pub token: String,
1304 pub key: String,
1305 /// Its size, when known before it is sent (the toolkit's newer client
1306 /// says only when it finishes: 0 then).
1307 pub size: u64,
1308 /// As in `CacheLookupArgs`.
1309 #[serde(default)]
1310 pub version: Option<String>,
1311}
1312
1313#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1314pub struct CacheReservation {
1315 pub id: String,
1316 /// Where the API puts it in R2.
1317 pub object: String,
1318 /// The entry's number, which the toolkit's older protocol names it by.
1319 #[serde(default)]
1320 pub number: u64,
1321 /// Its R2 upload, once one is started.
1322 #[serde(default)]
1323 pub upload: Option<String>,
1324 /// A signed token for sending its parts through the toolkit's blob
1325 /// endpoint, once its upload is started.
1326 #[serde(default)]
1327 pub blob: Option<String>,
1328}
1329
1330/// `cache_upload`: an entry a job is still uploading, by its number or by
1331/// key and version. Returns `Outcome<CacheReservation>`, with `upload` and
1332/// `blob` set once its upload has been started.
1333#[derive(Debug, Serialize, Deserialize)]
1334pub struct CacheUploadArgs {
1335 pub job: String,
1336 pub token: String,
1337 #[serde(default)]
1338 pub number: Option<u64>,
1339 #[serde(default)]
1340 pub key: Option<String>,
1341 #[serde(default)]
1342 pub version: Option<String>,
1343}
1344
1345/// `cache_commit`: the upload of `id` is complete, at `size` bytes. Returns
1346/// `Outcome<CacheCommitted>`: the objects of entries it evicted, which the
1347/// API deletes from R2.
1348#[derive(Debug, Serialize, Deserialize)]
1349pub struct CacheCommitArgs {
1350 pub job: String,
1351 pub token: String,
1352 pub id: String,
1353 pub size: u64,
1354}
1355
1356#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1357pub struct CacheCommitted {
1358 pub evicted: Vec<String>,
1359}
1360
1361/// `cache_abort`: an upload that will not finish; its reservation goes.
1362/// Returns `Outcome<bool>`.
1363#[derive(Debug, Serialize, Deserialize)]
1364pub struct CacheAbortArgs {
1365 pub job: String,
1366 pub token: String,
1367 pub id: String,
1368}
1369
1370/// A cache entry as people see it, in GitHub's shape: `id` is a number,
1371/// `git_ref` the ref whose run saved it (sent as `ref`; a pull request from
1372/// outside the repository saves under `untrusted:<ref>`), `version` the
1373/// hash of its paths and compression (empty for one g1t's own
1374/// `actions/cache` saved).
1375#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1376pub struct CacheEntry {
1377 pub id: u64,
1378 pub key: String,
1379 #[serde(rename = "ref")]
1380 pub git_ref: String,
1381 pub version: String,
1382 pub size_in_bytes: u64,
1383 pub created_at: String,
1384 pub last_accessed_at: String,
1385}
1386
1387/// A page of cache entries, in GitHub's shape.
1388#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1389pub struct CacheList {
1390 pub total_count: u64,
1391 pub actions_caches: Vec<CacheEntry>,
1392}
1393
1394/// How a list of cache entries is ordered: GitHub's `sort` names.
1395pub const CACHE_SORTS: [&str; 3] = ["last_accessed_at", "created_at", "size_in_bytes"];
1396
1397/// `caches`: a repository's saved cache entries, narrowed to keys starting
1398/// with `key` and to one `git_ref`, ordered by `sort` (one of
1399/// `CACHE_SORTS`, `last_accessed_at` by default) in `direction` (`asc` or
1400/// `desc`, the default), a page at a time (30 by default, at most 100).
1401/// Anyone who can see the repository. Returns `Outcome<CacheList>`.
1402#[derive(Debug, Serialize, Deserialize)]
1403pub struct CachesArgs {
1404 pub repo: RepoPath,
1405 pub viewer: Viewer,
1406 #[serde(default)]
1407 pub key: Option<String>,
1408 #[serde(default)]
1409 pub git_ref: Option<String>,
1410 #[serde(default)]
1411 pub sort: Option<String>,
1412 #[serde(default)]
1413 pub direction: Option<String>,
1414 #[serde(default)]
1415 pub page: Option<u32>,
1416 #[serde(default)]
1417 pub per_page: Option<u32>,
1418}
1419
1420/// `cache_usage`: what a repository's saved entries hold together, against
1421/// `CACHE_REPO_QUOTA_BYTES`. Anyone who can see the repository. Returns
1422/// `Outcome<CacheUsage>`.
1423#[derive(Debug, Serialize, Deserialize)]
1424pub struct CacheUsageArgs {
1425 pub repo: RepoPath,
1426 pub viewer: Viewer,
1427}
1428
1429/// GitHub's shape, with the most the repository may hold.
1430#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1431pub struct CacheUsage {
1432 pub active_caches_size_in_bytes: u64,
1433 pub active_caches_count: u64,
1434 pub quota_bytes: u64,
1435}
1436
1437/// `delete_caches`: deletes one entry by `id`, or every entry whose key is
1438/// exactly `key` (only `git_ref`'s, when given). Needs the Write role.
1439/// Returns the entries deleted: `Outcome<CacheList>`.
1440#[derive(Debug, Serialize, Deserialize)]
1441pub struct DeleteCachesArgs {
1442 pub actor: User,
1443 pub repo: RepoPath,
1444 #[serde(default)]
1445 pub id: Option<u64>,
1446 #[serde(default)]
1447 pub key: Option<String>,
1448 #[serde(default)]
1449 pub git_ref: Option<String>,
1450}
1451
1452// ── Artifacts (actions/upload-artifact) ───────────────────────────────────
1453//
1454// Kept in R2 by the API (the ACTIONS_CACHE bucket, under `a/`) and listed
1455// here, by the actions service, which decides names, sizes and how long
1456// each is kept. A sandbox reaches them with its job's token
1457// (`/actions/jobs/{job}/artifacts…`) or, through the toolkit's protocol,
1458// with its runtime token (`ACTIONS_RUNTIME_TOKEN`); people through the
1459// REST API and the run's page.
1460
1461/// The largest one artifact may be.
1462pub const ARTIFACT_MAX_BYTES: u64 = 5 * 1024 * 1024 * 1024;
1463/// What one run's artifacts may hold together.
1464pub const RUN_ARTIFACTS_MAX_BYTES: u64 = 10 * 1024 * 1024 * 1024;
1465/// How long artifacts are kept unless a repository says otherwise.
1466pub const ARTIFACT_RETENTION_DEFAULT_DAYS: u32 = 14;
1467/// The longest a repository may keep them.
1468pub const ARTIFACT_RETENTION_MAX_DAYS: u32 = 90;
1469/// A native upload is sent in parts of this size (the last may be smaller).
1470pub const ARTIFACT_PART_BYTES: u64 = 32 * 1024 * 1024;
1471
1472/// An artifact, as the API and the site show it.
1473#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1474pub struct Artifact {
1475 pub id: u64,
1476 pub name: String,
1477 pub size: u64,
1478 /// `sha256:<hex>`, when the uploader said.
1479 pub digest: Option<String>,
1480 /// `zip`, or `tgz` for one an older runner sent.
1481 pub format: String,
1482 pub run_id: String,
1483 pub job_id: String,
1484 pub repo_id: String,
1485 /// Whether it has expired or been deleted (its bytes are gone).
1486 pub expired: bool,
1487 pub created_at: String,
1488 pub updated_at: String,
1489 pub expires_at: String,
1490 /// The run's branch and commit, for the REST shape.
1491 #[serde(default)]
1492 pub head_branch: Option<String>,
1493 #[serde(default)]
1494 pub head_sha: Option<String>,
1495}
1496
1497/// An artifact with where its bytes are, and a signed token for them.
1498#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1499pub struct ArtifactBlob {
1500 pub artifact: Artifact,
1501 pub object: String,
1502 /// For the toolkit's blob endpoint (`/actions/toolkit/blobs/{blob}`).
1503 pub blob: String,
1504}
1505
1506/// A page of artifacts, in GitHub's shape.
1507#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1508pub struct ArtifactList {
1509 pub total_count: u64,
1510 pub artifacts: Vec<Artifact>,
1511}
1512
1513/// `artifact_reserve`: a job about to upload an artifact. Refused when its
1514/// run has one of that name and `overwrite` is not set (`conflict`), or it
1515/// is too large. Returns `Outcome<ArtifactReservation>`.
1516#[derive(Debug, Default, Serialize, Deserialize)]
1517pub struct ArtifactReserveArgs {
1518 pub job: String,
1519 /// The job's token, or its runtime token.
1520 pub token: String,
1521 pub name: String,
1522 /// Its size, when known before it is sent (0 otherwise).
1523 #[serde(default)]
1524 pub size: u64,
1525 /// Days to keep it: 0 for the repository's default; at most the
1526 /// repository's setting.
1527 #[serde(default)]
1528 pub retention_days: u32,
1529 /// When to expire it, RFC 3339, as the toolkit says it (in place of
1530 /// `retention_days`).
1531 #[serde(default)]
1532 pub expires_at: Option<String>,
1533 #[serde(default)]
1534 pub overwrite: bool,
1535 /// `zip` (the default) or `tgz`.
1536 #[serde(default)]
1537 pub format: Option<String>,
1538}
1539
1540#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1541pub struct ArtifactReservation {
1542 pub id: u64,
1543 /// Where the API puts it in R2.
1544 pub object: String,
1545 /// The days it will be kept, and until when.
1546 pub retention_days: u32,
1547 pub expires_at: String,
1548}
1549
1550/// `artifact_commit`: its upload is complete, at `size` bytes. The artifact
1551/// is named by `id`, or by `name` in the job's run (the toolkit's way).
1552/// Returns `Outcome<Artifact>`.
1553#[derive(Debug, Default, Serialize, Deserialize)]
1554pub struct ArtifactCommitArgs {
1555 pub job: String,
1556 pub token: String,
1557 #[serde(default)]
1558 pub id: Option<u64>,
1559 #[serde(default)]
1560 pub name: Option<String>,
1561 pub size: u64,
1562 #[serde(default)]
1563 pub digest: Option<String>,
1564}
1565
1566/// `job_artifacts`: a running job listing the artifacts of its own run, or
1567/// of another run of its repository (`run_id`), narrowed by `name` or
1568/// `id`: `Outcome<Vec<Artifact>>`. `job_artifact` gives the one named, with
1569/// a token to download it: `Outcome<ArtifactBlob>`. `job_delete_artifact`
1570/// deletes one of its own run's: `Outcome<Artifact>`. `artifact_abort`
1571/// gives up an upload by `id`: `Outcome<bool>`.
1572#[derive(Debug, Default, Serialize, Deserialize)]
1573pub struct JobArtifactsArgs {
1574 pub job: String,
1575 pub token: String,
1576 #[serde(default)]
1577 pub run_id: Option<String>,
1578 #[serde(default)]
1579 pub name: Option<String>,
1580 #[serde(default)]
1581 pub id: Option<u64>,
1582}
1583
1584/// `artifacts`: a repository's artifacts, newest first, or one run's.
1585/// Anyone who can see the repository. Returns `Outcome<ArtifactList>`.
1586#[derive(Debug, Serialize, Deserialize)]
1587pub struct ArtifactsArgs {
1588 pub repo: RepoPath,
1589 pub viewer: Viewer,
1590 #[serde(default)]
1591 pub run: Option<String>,
1592 #[serde(default)]
1593 pub name: Option<String>,
1594 #[serde(default)]
1595 pub page: Option<u32>,
1596 #[serde(default)]
1597 pub per_page: Option<u32>,
1598}
1599
1600/// `artifact` (`Outcome<Artifact>`) and `artifact_download`
1601/// (`Outcome<ArtifactBlob>`, with a token good for a few minutes): one
1602/// artifact by `id`, or by `name` within `run`. Anyone who can see the
1603/// repository.
1604#[derive(Debug, Serialize, Deserialize)]
1605pub struct ArtifactArgs {
1606 pub repo: RepoPath,
1607 pub viewer: Viewer,
1608 #[serde(default)]
1609 pub id: Option<u64>,
1610 #[serde(default)]
1611 pub run: Option<String>,
1612 #[serde(default)]
1613 pub name: Option<String>,
1614}
1615
1616/// `delete_artifact`: needs the Write role. Returns `Outcome<Artifact>`.
1617#[derive(Debug, Serialize, Deserialize)]
1618pub struct DeleteArtifactArgs {
1619 pub actor: User,
1620 pub repo: RepoPath,
1621 pub id: u64,
1622}
1623
1624/// `artifact_retention`: anyone who can see the repository. With `days`,
1625/// sets it, which needs the Maintain role. Returns
1626/// `Outcome<ArtifactRetention>`.
1627#[derive(Debug, Serialize, Deserialize)]
1628pub struct ArtifactRetentionArgs {
1629 pub repo: RepoPath,
1630 pub viewer: Viewer,
1631 #[serde(default)]
1632 pub days: Option<u32>,
1633}
1634
1635/// GitHub's shape: the days artifacts are kept by default, and the most a
1636/// repository may choose.
1637#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1638pub struct ArtifactRetention {
1639 pub days: u32,
1640 pub maximum_allowed_days: u32,
1641}
1642
1643// ── The toolkit's protocols ───────────────────────────────────────────────
1644//
1645// Actions built on GitHub's toolkit (`@actions/cache`, `@actions/artifact`,
1646// `@actions/core`'s `getIDToken`) reach g1t with the job's runtime token,
1647// `ACTIONS_RUNTIME_TOKEN`: a JSON Web Token whose `scp` names the run and
1648// job, signed with a key derived from the job's own token, so the actions
1649// service checks it without keeping another secret. Cache and artifact
1650// operations above take it in place of the job's token.
1651
1652/// `runtime_auth`: which job a runtime token is, while it runs:
1653/// `Outcome<RuntimeJob>`. `oidc_claims` takes the same and returns
1654/// `Outcome<Value>`: the claims of the job's OIDC token, less `iss`, `aud`,
1655/// `jti` and the times, or `forbidden` when the job's `permissions` do not
1656/// give it `id-token: write`.
1657#[derive(Debug, Serialize, Deserialize)]
1658pub struct RuntimeAuthArgs {
1659 pub job: String,
1660 pub token: String,
1661}
1662
1663#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1664pub struct RuntimeJob {
1665 pub job: String,
1666 pub run: String,
1667 pub repo_id: String,
1668 pub namespace: String,
1669 /// `owner/name`.
1670 pub repository: String,
1671}
1672
1673/// What a signed blob token lets its holder do.
1674#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1675pub struct BlobGrant {
1676 /// `cache` or `artifact`.
1677 pub kind: String,
1678 /// The entry's id: a cache entry's `cache_…`, an artifact's number.
1679 pub id: String,
1680 pub object: String,
1681 /// The R2 upload it sends parts to; `None` for a download.
1682 pub upload: Option<String>,
1683 /// For a download: what to call the file, and its type.
1684 #[serde(default)]
1685 pub filename: Option<String>,
1686 #[serde(default)]
1687 pub content_type: Option<String>,
1688}
1689
1690/// `blob_sign`: a token for uploading an entry the job reserved, to the R2
1691/// upload the API started for it. Returns `Outcome<String>`.
1692#[derive(Debug, Serialize, Deserialize)]
1693pub struct BlobSignArgs {
1694 pub job: String,
1695 pub token: String,
1696 /// `cache` or `artifact`.
1697 pub kind: String,
1698 pub id: String,
1699 pub upload: String,
1700}
1701
1702/// `blob_open`: what a signed token grants, while it is good and its entry
1703/// is there: `Outcome<BlobGrant>`. `blob_part` records a part sent with an
1704/// upload token (`part`, `etag`, `size`): `Outcome<bool>`. `blob_parts`
1705/// gives the parts recorded, in order: `Outcome<Vec<BlobPart>>`, and
1706/// `blob_done` forgets them: `Outcome<bool>`.
1707#[derive(Debug, Default, Serialize, Deserialize)]
1708pub struct BlobArgs {
1709 pub blob: String,
1710 #[serde(default)]
1711 pub part: u32,
1712 #[serde(default)]
1713 pub etag: String,
1714 #[serde(default)]
1715 pub size: u64,
1716}
1717
1718#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1719pub struct BlobPart {
1720 pub part: u32,
1721 pub etag: String,
1722 pub size: u64,
1723}
1724
1725#[cfg(test)]
1726mod instance_tests {
1727 use super::*;
1728
1729 fn labels(given: &[&str]) -> Vec<String> {
1730 given.iter().map(|l| (*l).to_owned()).collect()
1731 }
1732
1733 #[test]
1734 fn runs_on_picks_the_machine() {
1735 // As on GitHub: ubuntu-latest is 2 vCPU in a private repository, 4 in a public one.
1736 assert_eq!(instance_for(&labels(&["ubuntu-latest"]), false), STANDARD_INSTANCE);
1737 assert_eq!(instance_for(&labels(&["ubuntu-latest"]), true).machine, "4-core");
1738 assert_eq!(instance_for(&labels(&["ubuntu-24.04"]), true).vcpu, 4.0);
1739 assert_eq!(instance_for(&labels(&[]), false).label, "standard");
1740 assert_eq!(instance_for(&labels(&[]), true).label, PUBLIC_DEFAULT_LABEL);
1741 // A machine asked for by name is that machine, public or not.
1742 assert_eq!(instance_for(&labels(&["g1t-4core"]), false).label, "g1t-4core");
1743 assert_eq!(instance_for(&labels(&["ubuntu-latest", "G1T-2Core"]), false).label, "g1t-2core");
1744 assert_eq!(instance_for(&labels(&["ubuntu-latest", "g1t-2core"]), true).machine, "2-core");
1745 // Both named: the later one.
1746 assert_eq!(instance_for(&labels(&["g1t-2core", "g1t-4core"]), false).label, "g1t-4core");
1747 assert_eq!(instance_named("g1t-2core").map(|i| i.vcpu), Some(2.0));
1748 assert_eq!(instance_named("g1t-4core").map(|i| i.vcpu), Some(4.0));
1749 assert_eq!(instance_named("standard"), Some(STANDARD_INSTANCE));
1750 assert_eq!(instance_named("g1t-64core"), None);
1751 }
1752
1753 #[test]
1754 fn every_machine_is_a_named_instance_type_at_its_size() {
1755 // Containers' named instance types, at the sizes Cloudflare gives
1756 // them; no custom types.
1757 let named = |container: &str| match container {
1758 "standard-3" => Some((2.0, 8.0, 16.0)),
1759 "standard-4" => Some((4.0, 12.0, 20.0)),
1760 _ => None,
1761 };
1762 for instance in INSTANCE_TYPES {
1763 assert_eq!(named(instance.container), Some((instance.vcpu, instance.memory_gib, instance.disk_gb)), "{}", instance.label);
1764 }
1765 // g1t-2core is a name of the standard machine, in a class of its own.
1766 let two = instance_named("g1t-2core").unwrap();
1767 assert_eq!(InstanceType { label: "standard", class: "AttemptSandbox", ..two }, STANDARD_INSTANCE);
1768 }
1769
1770 #[test]
1771 fn the_catalogue_is_one_row_a_label_and_one_size_a_machine() {
1772 let mut labels: Vec<_> = INSTANCE_TYPES.iter().map(|i| i.label).collect();
1773 let mut classes: Vec<_> = INSTANCE_TYPES.iter().map(|i| i.class).collect();
1774 labels.sort_unstable();
1775 labels.dedup();
1776 classes.sort_unstable();
1777 classes.dedup();
1778 assert_eq!((labels.len(), classes.len()), (INSTANCE_TYPES.len(), INSTANCE_TYPES.len()));
1779 // Every row of a machine says the same size and price as its first.
1780 for instance in INSTANCE_TYPES {
1781 let first = machine_named(instance.machine).unwrap();
1782 assert_eq!(
1783 (instance.container, instance.vcpu, instance.memory_gib, instance.disk_gb, instance.micros_per_minute),
1784 (first.container, first.vcpu, first.memory_gib, first.disk_gb, first.micros_per_minute),
1785 "{}",
1786 instance.label
1787 );
1788 }
1789 assert_eq!(machines().iter().map(|m| m.machine).collect::<Vec<_>>(), ["2-core", "4-core"]);
1790 assert_eq!(STANDARD_INSTANCE.size(), "2 vCPU, 8 GiB, 16 GB");
1791 assert!((INSTANCE_TYPES[2].price_scale() - 1.75).abs() < 1e-12);
1792 }
1793
1794 #[test]
1795 fn rates_cover_the_machine_at_full_cpu_and_its_usual_cost_plus_the_margin() {
1796 // Memory, disk and the Durable Object behind the container (128 MB)
1797 // for every second, and vCPU as used, at Cloudflare's list prices,
1798 // in micros a minute.
1799 let per_minute = |i: &InstanceType, busy_vcpus: f64| 60.0 * (i.memory_gib * 2.5 + i.disk_gb * 0.07 + 0.125 * 12.5 + busy_vcpus * 20.0);
1800 let tenth_of_a_cent = |micros: f64| ((micros / 1_000.0).ceil() * 1_000.0) as i64;
1801 for instance in machines() {
1802 let usual = per_minute(&instance, instance.vcpu / 2.0) * 1.2;
1803 let full = per_minute(&instance, instance.vcpu);
1804 assert_eq!(instance.micros_per_minute, tenth_of_a_cent(usual).max(tenth_of_a_cent(full)), "{}: {usual} {full}", instance.machine);
1805 assert!(instance.micros_per_minute as f64 >= full, "{}", instance.machine);
1806 }
1807 assert_eq!(STANDARD_INSTANCE.micros_per_minute, 4_000);
1808 assert_eq!(machine_named("4-core").map(|m| m.micros_per_minute), Some(7_000));
1809 }
1810
1811 #[test]
1812 fn the_typescript_mirror_has_every_row() {
1813 let mirror = include_str!("../../../packages/contracts/src/instances.ts");
1814 let rows = mirror.lines().filter(|line| line.trim_start().starts_with("{ label: ")).count();
1815 assert_eq!(rows, INSTANCE_TYPES.len());
1816 for i in INSTANCE_TYPES {
1817 let row = format!(
1818 r#"{{ label: "{}", machine: "{}", sandboxClass: "{}", container: "{}", vcpu: {}, memoryGib: {}, diskGb: {}, microsPerMinute: {} }}"#,
1819 i.label, i.machine, i.class, i.container, i.vcpu, i.memory_gib, i.disk_gb, i.micros_per_minute
1820 );
1821 assert!(mirror.contains(&row), "instances.ts has no row\n{row}");
1822 }
1823 }
1824
1825 #[test]
1826 fn a_job_is_charged_its_minutes_rounded_up() {
1827 assert_eq!(runner_minutes(0), 0);
1828 assert_eq!(runner_minutes(-5), 0);
1829 assert_eq!(runner_minutes(1), 1);
1830 assert_eq!(runner_minutes(60), 1);
1831 assert_eq!(runner_minutes(61), 2);
1832 assert_eq!(runner_minutes(3_600), 60);
1833 }
1834
1835 #[test]
1836 fn start_args_from_older_callers_read() {
1837 let args: StartJobArgs = serde_json::from_value(serde_json::json!({
1838 "job": "job_1", "token": "t", "repo": { "namespace": "acme", "name": "web" }, "timeoutMinutes": 30
1839 }))
1840 .unwrap();
1841 assert!(args.instance.is_none() && !args.brokered);
1842 // Callers from when workflow-only domains existed still read.
1843 let older: StartJobArgs = serde_json::from_value(serde_json::json!({
1844 "job": "job_1", "token": "t", "repo": { "namespace": "acme", "name": "web" }, "timeoutMinutes": 30,
1845 "workflow": ".g1t/workflows/deploy.yml", "environment": "production", "trusted": true
1846 }))
1847 .unwrap();
1848 assert_eq!(older.timeout_minutes, 30);
1849 }
1850}
1851
1852#[cfg(test)]
1853mod setting_args_tests {
1854 use super::*;
1855
1856 #[test]
1857 fn who_reads_a_row_is_read_as_the_site_and_api_send_it() {
1858 let args: SetSettingArgs = serde_json::from_value(serde_json::json!({
1859 "actor": { "id": "usr_1", "username": "a" },
1860 "repo": { "namespace": "acme", "name": "web" },
1861 "kind": "secret",
1862 "name": "STRIPE_KEY",
1863 "availableTo": ["deployments"],
1864 "environments": ["production"],
1865 }))
1866 .unwrap();
1867 assert_eq!(args.available_to, Some(vec!["deployments".to_owned()]));
1868 }
1869}