Skip to content
373 linesCodeBlameRaw

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

The statement is a month at a time, a line per kind of charge1//! The actions service: a repository's GitHub Actions workflows, run on
2//! g1t as they are. See `g1t_contracts::actions` for the methods and
3//! `g1t_actions` for how workflows, expressions and filters are read.
4//!
5//! - [`sync`] reads workflow files, at the default branch for the list and
6//! at an event's own commit for its runs.
7//! - [`trigger`] turns events, schedules and manual runs into runs.
8//! - [`plan`] moves a run's jobs along: each waits for the jobs it needs,
9//! is skipped or expanded into its matrix, queued, started in a sandbox
10//! when the workspace has room, and finished by the sandbox's report.
11//! - [`payload`] builds the webhook-shaped `github.event`.
12//! - [`settings`] keeps secrets and variables.
Merge branch 'worktree-agent-a3abfcce648e87dca'13//! - [`protection`] keeps a repository's workflow settings and its
14//! environments' protection rules, holds jobs at those rules until they
15//! pass, and lets pull requests' runs from outside wait for approval.
Fast pages, required checks on the branch, self-hosted runners, honest incidents16//! - [`cache`] lists `actions/cache` entries, which the API keeps in R2.
Merge remote-tracking branch 'origin/claude/peaceful-franklin-7qtkwx'17//! - [`reach`] finds the actions and reusable workflows of other
18//! repositories on g1t, and [`registry`] resolves those from GitHub to
19//! commits and keeps their archives, for jobs to download from g1t.
Fast pages, required checks on the branch, self-hosted runners, honest incidents20//! - [`runners`] keeps self-hosted runners, hands them jobs (and agent
21//! work from the runner service) when they ask, and hears back.
The statement is a month at a time, a line per kind of charge22//!
23//! The service acts as the repository's workspace: it reads what the
Merge branch 'worktree-agent-a3abfcce648e87dca'24//! workspace can read, and a job's `GITHUB_TOKEN` is a token of the
25//! workspace's that reaches the job's repository only, with the scopes its
26//! `permissions:` give it, revoked when the job ends.
The statement is a month at a time, a line per kind of charge27
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.28mod run_artifacts;
Fast pages, required checks on the branch, self-hosted runners, honest incidents29mod cache;
Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.30mod managed;
Merge branch 'mirroring' into artifacts-mode31mod mirrored;
The statement is a month at a time, a line per kind of charge32mod payload;
33mod plan;
Merge branch 'worktree-agent-a3abfcce648e87dca'34mod protection;
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts35mod reach;
Merge remote-tracking branch 'origin/claude/peaceful-franklin-7qtkwx'36mod registry;
The statement is a month at a time, a line per kind of charge37mod rename;
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R238pub mod runtime;
Fast pages, required checks on the branch, self-hosted runners, honest incidents39mod runners;
The statement is a month at a time, a line per kind of charge40mod settings;
41mod sync;
42mod trigger;
43mod views;
44
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look45use g1t_contracts::access::{self, Capability};
The statement is a month at a time, a line per kind of charge46use g1t_contracts::events::Event;
47use g1t_contracts::identity::{SlugArgs, Workspace};
48use g1t_contracts::repos::{GetArgs, GetByIdArgs, Repo, RepoPath};
49use g1t_contracts::{FailureCode, Membership, Outcome, PrincipalKind, User, Viewer};
50use g1t_kit::{args, reply, rpc_method};
51use g1t_secrets::Sealer;
52use serde::Deserialize;
53use serde_json::Value;
54use worker::wasm_bindgen::JsValue;
55use worker::{Context, D1Database, Env, Fetcher, MessageBatch, MessageExt, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
56
57/// The most workflow files read from a repository.
58pub const MAX_WORKFLOWS: usize = 50;
59/// Jobs one workspace may have running at once; the rest wait their turn.
60pub const RUNNING_PER_WORKSPACE: u32 = 4;
Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.61/// The longest a job on g1t's own machines may run, whatever its
62/// `timeout-minutes`.
63pub const MAX_TIMEOUT_MINUTES: u32 = g1t_actions::workflow::HOSTED_MAX_TIMEOUT_MINUTES;
Fast pages, required checks on the branch, self-hosted runners, honest incidents64/// The longest a job on a self-hosted runner may run: the machine is the
65/// workspace's own, and its time costs nothing.
66pub const SELF_HOSTED_MAX_TIMEOUT_MINUTES: u32 = 24 * 60;
The statement is a month at a time, a line per kind of charge67/// A running job that has said nothing for this long is taken as lost.
68pub const SILENT_MS: u64 = 10 * 60 * 1000;
Merge branch 'worktree-agent-ab9543c492a7ed481' into spend-guardrails69/// Schedules pause in a repository that has had no push for this long; the
70/// next push resumes them.
71pub const SCHEDULE_IDLE_MS: u64 = 60 * 24 * 60 * 60 * 1000;
72/// How long a workflow's schedule waits after billing refused to start a
73/// job of one of its scheduled runs, before it tries again.
74pub const SCHEDULE_REFUSED_MS: u64 = 60 * 60 * 1000;
The statement is a month at a time, a line per kind of charge75pub const SITE: &str = "https://g1t.sh";
76pub const API: &str = "https://api.g1t.sh";
77
78#[derive(Deserialize)]
79struct Count {
80 n: u32,
81}
82
83pub fn optional(value: Option<&str>) -> JsValue {
84 value.map_or(JsValue::NULL, JsValue::from)
85}
86
87pub fn fail<T>(code: FailureCode, message: impl Into<String>) -> Outcome<T> {
88 Outcome::fail(code, message)
89}
90
91/// `owner/name` as a path.
92pub fn repo_path(full_name: &str) -> RepoPath {
93 let (namespace, name) = full_name.split_once('/').unwrap_or((full_name, ""));
94 RepoPath {
95 namespace: namespace.to_owned(),
96 name: name.to_owned(),
97 }
98}
99
100pub struct Actions {
101 db: D1Database,
102 repos: Fetcher,
103 work: Fetcher,
104 identity: Fetcher,
105 runner: Fetcher,
106 events: Fetcher,
107 /// Projects: a repository's secrets and variables belong to its project.
108 projects: Fetcher,
Fast pages, required checks on the branch, self-hosted runners, honest incidents109 /// Billing: self-hosted runners' time, recorded at $0, and the
110 /// cache's storage.
111 billing: Fetcher,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97112 /// Deployments: a job with an `environment:` deploys to it, and its
113 /// run's deployment is recorded there (plan.rs `report_deployment`).
114 deployments: Fetcher,
Fast pages, required checks on the branch, self-hosted runners, honest incidents115 /// Where the API keeps cache entries, for deleting evicted ones.
116 cache: Option<worker::Bucket>,
The statement is a month at a time, a line per kind of charge117 /// Seals secrets; absent until `ACTIONS_KEY` is set, when secrets
118 /// cannot be saved.
119 sealer: Option<Sealer>,
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2120 /// Signs the toolkit's blob links (runtime.rs), from `ACTIONS_KEY`.
121 blob_key: Option<String>,
The statement is a month at a time, a line per kind of charge122}
123
124impl Actions {
125 fn new(env: &Env) -> Result<Self> {
126 Ok(Actions {
127 db: env.d1("DB")?,
128 repos: env.service("REPOS")?,
129 work: env.service("WORK")?,
130 identity: env.service("IDENTITY")?,
131 runner: env.service("RUNNER")?,
132 events: env.service("EVENTS")?,
133 projects: env.service("PROJECTS")?,
Fast pages, required checks on the branch, self-hosted runners, honest incidents134 billing: env.service("BILLING")?,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97135 deployments: env.service("DEPLOYMENTS")?,
Fast pages, required checks on the branch, self-hosted runners, honest incidents136 cache: env.bucket("ACTIONS_CACHE").ok(),
The statement is a month at a time, a line per kind of charge137 sealer: env.secret("ACTIONS_KEY").ok().and_then(|key| Sealer::new(&key.to_string())),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2138 blob_key: env.secret("ACTIONS_KEY").ok().map(|key| runtime::blob_key(&key.to_string())),
The statement is a month at a time, a line per kind of charge139 })
140 }
141
142 /// The workspace itself, as the service acts.
143 async fn workspace_actor(&self, slug: &str) -> Result<Option<User>> {
144 let workspace: Option<Workspace> = g1t_kit::call(&self.identity, "get_workspace", &SlugArgs { slug: slug.to_owned() }).await?;
145 Ok(workspace.map(|workspace| User {
146 id: workspace.id,
147 username: workspace.slug.clone(),
148 kind: PrincipalKind::Workspace,
149 verified: true,
150 workspaces: vec![Membership::member(workspace.slug)],
151 ..User::default()
152 }))
153 }
154
155 /// The repository, if the viewer may see it and it is not a pull
156 /// request's working copy.
157 async fn visible_repo(&self, path: &RepoPath, viewer: &Viewer) -> Result<Option<Repo>> {
158 let found: Outcome<Repo> = g1t_kit::call(
159 &self.repos,
160 "get",
161 &GetArgs {
162 path: path.clone(),
163 viewer: viewer.clone(),
164 },
165 )
166 .await?;
167 Ok(found.into_result().ok().filter(|repo| repo.fork_of.is_none()))
168 }
169
170 /// A repository by id, as its workspace sees it.
171 async fn repo_by_id(&self, id: &str) -> Result<Option<(Repo, User)>> {
172 let path: Option<RepoPath> = g1t_kit::call(&self.repos, "path_by_id", &g1t_contracts::repos::PathByIdArgs { id: id.to_owned() }).await?;
173 let Some(path) = path else { return Ok(None) };
174 let Some(actor) = self.workspace_actor(&path.namespace).await? else {
175 return Ok(None);
176 };
177 let found: Outcome<Repo> = g1t_kit::call(
178 &self.repos,
179 "get_by_id",
180 &GetByIdArgs {
181 id: id.to_owned(),
182 viewer: Some(actor.clone()),
183 },
184 )
185 .await?;
186 Ok(found.into_result().ok().filter(|repo| repo.fork_of.is_none()).map(|repo| (repo, actor)))
187 }
188
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look189 /// The repository at `path`, when `actor` may do `capability` in it:
190 /// not found when they cannot read it, forbidden when their role falls
191 /// short. Agents never may: people and tokens run and change workflows.
192 async fn may(&self, actor: &User, path: &RepoPath, capability: Capability) -> Result<Outcome<Repo>> {
193 if actor.kind == PrincipalKind::Agent {
194 return Ok(fail(FailureCode::Forbidden, "An agent cannot do that. Ask a person."));
195 }
196 let Some(repo) = self.visible_repo(path, &Some(actor.clone())).await? else {
197 return Ok(fail(FailureCode::NotFound, "There is no such repository."));
198 };
199 if !access::can(Some(actor), &repo, capability) {
200 return Ok(fail(
201 FailureCode::Forbidden,
202 access::needs(capability, &format!("{}/{}", repo.namespace, repo.name)),
203 ));
204 }
205 Ok(Outcome::Ok(repo))
The statement is a month at a time, a line per kind of charge206 }
207}
208
209/// Unwraps an `Outcome`, or returns its failure from the enclosing method.
210#[macro_export]
211macro_rules! check {
212 ($outcome:expr) => {
213 match $outcome {
214 g1t_contracts::Outcome::Ok(value) => value,
215 g1t_contracts::Outcome::Fail(refused) => return Ok(g1t_contracts::Outcome::Fail(refused)),
216 }
217 };
218}
219
220#[event(fetch)]
221async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
222 let Some(method) = rpc_method(&request) else {
223 return Response::error("Not found", 404);
224 };
225 let body: Value = request.json().await?;
226 let service = Actions::new(&env)?;
227 match method.as_str() {
228 "workflows" => reply(&service.workflows(args(body)?).await?),
229 "runs" => reply(&service.runs(args(body)?).await?),
230 "run" => reply(&service.run(args(body)?).await?),
231 "logs" => reply(&service.logs(args(body)?).await?),
Merge Actions runs: summaries, attempts and re-runs, graceful cancel, log downloads, badges (actions 0009)232 "job_log_text" => reply(&service.job_log_text(args(body)?).await?),
233 "run_logs" => reply(&service.run_logs(args(body)?).await?),
234 "summaries" => reply(&service.summaries(args(body)?).await?),
The statement is a month at a time, a line per kind of charge235 "dispatch" => reply(&service.dispatch(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'236 "repository_dispatch" => reply(&service.repository_dispatch(args(body)?).await?),
237 "approve_run" => reply(&service.approve_run(args(body)?).await?),
238 "pending_deployments" => reply(&service.pending_deployments(args(body)?).await?),
239 "review_deployments" => reply(&service.review_deployments(args(body)?).await?),
240 "actions_settings" => reply(&service.actions_settings(args(body)?).await?),
241 "set_actions_settings" => reply(&service.set_actions_settings(args(body)?).await?),
242 "workspace_actions_settings" => reply(&service.workspace_actions_settings(args(body)?).await?),
243 "set_workspace_actions_settings" => reply(&service.set_workspace_actions_settings(args(body)?).await?),
244 "environments" => reply(&service.environments(args(body)?).await?),
245 "set_environment" => reply(&service.set_environment(args(body)?).await?),
246 "delete_environment" => reply(&service.delete_environment(args(body)?).await?),
The statement is a month at a time, a line per kind of charge247 "merge_group" => reply(&service.merge_group(args(body)?).await?),
248 "cancel" => reply(&service.cancel(args(body)?).await?),
249 "rerun" => reply(&service.rerun(args(body)?).await?),
250 "set_workflow_enabled" => reply(&service.set_workflow_enabled(args(body)?).await?),
251 "settings" => reply(&service.settings(args(body)?).await?),
252 "set_setting" => reply(&service.set_setting(args(body)?).await?),
253 "delete_setting" => reply(&service.delete_setting(args(body)?).await?),
254 "resolve_settings" => reply(&service.resolve_settings(args(body)?).await?),
Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.255 // Managed workflows: g1t's services' side only.
256 "set_managed_workflow" => reply(&service.set_managed_workflow(args(body)?).await?),
257 "remove_managed_workflow" => reply(&service.remove_managed_workflow(args(body)?).await?),
258 "set_managed_deploy" => reply(&service.set_managed_deploy(args(body)?).await?),
The statement is a month at a time, a line per kind of charge259 "job_spec" => reply(&service.job_spec(args(body)?).await?),
260 "job_auth" => reply(&service.job_auth(args(body)?).await?),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts261 "job_action" => reply(&service.job_action(args(body)?).await?),
Merge remote-tracking branch 'origin/claude/peaceful-franklin-7qtkwx'262 "job_action_archive" => reply(&service.job_action_archive(args(body)?).await?),
Merge checks: statuses and check runs on every commit263 "check_runs" => reply(&service.check_runs(args(body)?).await?),
The statement is a month at a time, a line per kind of charge264 "job_report" => reply(&service.job_report(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents265 // actions/cache, through the API with the job's token.
266 "cache_lookup" => reply(&service.cache_lookup(args(body)?).await?),
267 "cache_reserve" => reply(&service.cache_reserve(args(body)?).await?),
268 "cache_commit" => reply(&service.cache_commit(args(body)?).await?),
269 "cache_abort" => reply(&service.cache_abort(args(body)?).await?),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2270 "cache_upload" => reply(&service.cache_upload(args(body)?).await?),
271 // Artifacts: a job's side, with its token or its runtime token.
272 "artifact_reserve" => reply(&service.artifact_reserve(args(body)?).await?),
273 "artifact_commit" => reply(&service.artifact_commit(args(body)?).await?),
274 "artifact_abort" => reply(&service.artifact_abort(args(body)?).await?),
275 "job_artifacts" => reply(&service.job_artifacts(args(body)?).await?),
276 "job_artifact" => reply(&service.job_artifact(args(body)?).await?),
277 "job_delete_artifact" => reply(&service.job_delete_artifact(args(body)?).await?),
278 // Artifacts: people's side.
279 "artifacts" => reply(&service.artifacts(args(body)?).await?),
280 "artifact" => reply(&service.artifact(args(body)?).await?),
281 "artifact_download" => reply(&service.artifact_download(args(body)?).await?),
282 "delete_artifact" => reply(&service.delete_artifact(args(body)?).await?),
283 "artifact_retention" => reply(&service.artifact_retention(args(body)?).await?),
284 // The toolkit's protocols: the runtime token, OIDC, blob links.
285 "runtime_auth" => reply(&service.runtime_auth(args(body)?).await?),
286 "oidc_claims" => reply(&service.oidc_claims(args(body)?).await?),
287 "blob_sign" => reply(&service.blob_sign(args(body)?).await?),
288 "blob_open" => reply(&service.blob_open(args(body)?).await?),
289 "blob_part" => reply(&service.blob_part(args(body)?).await?),
290 "blob_parts" => reply(&service.blob_parts(args(body)?).await?),
291 "blob_done" => reply(&service.blob_done(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents292 // Self-hosted runners: people's side.
293 "runners" => reply(&service.runners(args(body)?).await?),
294 "create_registration_token" => reply(&service.create_registration_token(args(body)?).await?),
295 "remove_runner" => reply(&service.remove_runner(args(body)?).await?),
296 "runner_groups" => reply(&service.runner_groups(args(body)?).await?),
297 "set_runner_group" => reply(&service.set_runner_group(args(body)?).await?),
298 "delete_runner_group" => reply(&service.delete_runner_group(args(body)?).await?),
299 "runner_settings" => reply(&service.runner_settings(args(body)?).await?),
300 "set_runner_settings" => reply(&service.set_runner_settings(args(body)?).await?),
301 // The runner's own side, through the API with its credential.
302 "runner_register" => reply(&service.runner_register(args(body)?).await?),
303 "runner_poll" => reply(&service.runner_poll(args(body)?).await?),
304 "runner_finished" => reply(&service.runner_finished(args(body)?).await?),
305 "runner_remove_self" => reply(&service.runner_remove_self(args(body)?).await?),
306 // Agent work, from the runner service.
307 "runner_route" => reply(&service.runner_route(args(body)?).await?),
308 "stuck_jobs" => reply(&service.stuck_jobs(args(body)?).await?),
The Runners page shows the machines a workspace's agents and workflow jobs run on, in Workspace under Compute: g1t's cloud beside your own runners, what each is running now with a link to it, what's waiting, this month's machine time and its cost (your own runners' free), where agent work and workflow jobs run, adding a runner, and groups; runner_activity in Actions says what runs where, work handed to your runners keeps its agent run, and the self-hosted runners guide says how.309 "runner_activity" => reply(&service.runner_activity(args(body)?).await?),
Fast pages, required checks on the branch, self-hosted runners, honest incidents310 "enqueue_task" => reply(&service.enqueue_task(args(body)?).await?),
311 "cancel_task" => reply(&service.cancel_task(args(body)?).await?),
The statement is a month at a time, a line per kind of charge312 _ => Response::error("Unknown method", 404),
313 }
314}
315
316/// Events from the bus, on this service's own queue.
317#[event(queue)]
318async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> {
319 let service = Actions::new(&env)?;
320 for message in batch.messages()? {
321 // A workspace renamed: its rows move to the slug it has now.
322 if g1t_kit::rename::on_event(&env, &env.d1("DB")?, message.body(), rename::STATEMENTS).await? {
323 message.ack();
324 continue;
325 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look326 // A repository renamed or transferred: its rows follow its new path.
327 if g1t_kit::transfer::on_event(&env, &env.d1("DB")?, message.body(), rename::TRANSFERRED).await? {
328 message.ack();
329 continue;
330 }
331 // A workspace deleted: what it kept for itself goes.
332 if g1t_kit::deleted::on_event(&env.d1("DB")?, message.body(), rename::DELETED).await? {
333 message.ack();
334 continue;
335 }
336 // A repository purged: every row kept for it goes.
337 if g1t_kit::lifecycle::on_purged(&env.d1("DB")?, message.body(), rename::PURGED).await? {
338 message.ack();
339 continue;
340 }
341 // A repository deleted or archived: its runs stop.
342 if let Some(repo_id) = plan::stops_runs(message.body()) {
343 if let Err(error) = service.stop_runs(&repo_id).await {
344 worker::console_error!("actions: event {} failed: {error}", message.body().id);
345 message.retry();
346 continue;
347 }
348 message.ack();
349 continue;
350 }
The statement is a month at a time, a line per kind of charge351 if let Err(error) = service.on_event(message.body()).await {
352 worker::console_error!("actions: event {} failed: {error}", message.body().id);
353 message.retry();
354 continue;
355 }
356 message.ack();
357 }
358 Ok(())
359}
360
361/// Every minute: schedules that fire, jobs waiting for room, and jobs
362/// whose sandbox went quiet.
363#[event(scheduled)]
364async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
365 match Actions::new(&env) {
366 Ok(service) => {
367 if let Err(error) = service.on_minute(g1t_kit::now_ms()).await {
368 worker::console_error!("actions: the sweep failed: {error}");
369 }
370 }
371 Err(error) => worker::console_error!("actions: could not start: {error}"),
372 }
373}

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