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.

g1t.sh
291 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.

Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put1//! Reading workflow files: at any commit for a run, and from the default
2//! branch into the `workflows` table, which lists them, holds their
3//! schedules and remembers which are turned off.
4
5use g1t_actions::workflow::{self, FOLDER};
6use g1t_contracts::new_id;
7use g1t_contracts::repos::{BlobArgs, BlobView, EntryKind, Repo, RepoPath, TreeArgs, TreeView};
8use g1t_contracts::time::rfc3339;
9use g1t_contracts::{Outcome, User};
10use g1t_kit::now_ms;
11use serde::Deserialize;
12use worker::Result;
13
14use crate::{Actions, Count, MAX_WORKFLOWS, optional};
15
16/// One workflow file as read at a commit.
17pub struct WorkflowFile {
18 pub path: String,
19 pub source: String,
A workspace can manage workflows in its repositories, as GitHub's required workflows do, through g1t's managed mechanism. An owner adds a workflow file from one of the workspace's own repositories (`owner/repo/.g1t/workflows/file.yml@ref`) with a name, a mode and a target, kept in the actions service's new `workspace_managed_workflows` table (migration 0017): the file is copied at the commit its ref names when it is saved, so only an owner changes what runs with every repository's secrets, and copying it again takes up a change. The target is repository names and patterns (`*`, `!` to leave one out, the last match deciding; empty is every repository) and a visibility, and a source in a private repository runs in private repositories only, as reach.rs keeps a public repository from using a private one's workflows. Wherever a repository's workflows are read, a fork's pull request and the merge queue included, `g1t_actions::managed::arrange` puts its files and the managed workflows together: a required workflow runs as `managed/<name>.yml` and sets aside a file of its `name:`, so nothing else reports its check and a ruleset can require it, and can't be turned off in the repository; a default one gives way to a file of its name, as the workflows g1t's services install do, and a repository admin can turn it off. It runs as the repository's own workflow, with its token and secrets. A change syncs the first 20 repositories it reaches at once and leaves the rest to be synced when their workflows are listed or by the minute's sweep, a few a minute, their managed schedules waiting until then; it is recorded in the audit log. A workflow's view says what manages it (`managedBy`), and the Actions list now includes managed workflows, which it had left out. The API has `GET` and `POST /workspaces/{workspace}/actions/managed-workflows` and `PATCH` and `DELETE /workspaces/{workspace}/actions/managed-workflows/{workflow}`, owners only, and the `workflow` MCP tool `list_managed_workflows`, `add_managed_workflow`, `update_managed_workflow` and `remove_managed_workflow`; the reference and the OpenAPI copy say so.20 /// A workspace's required workflow (managed.rs): no one in the
21 /// repository can turn it off.
22 pub required: bool,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put23}
24
25/// The files at a commit, and the commit the ref resolved to.
26pub struct Read {
27 pub files: Vec<WorkflowFile>,
28 pub head: Option<String>,
29}
30
31#[derive(Deserialize)]
32pub struct WorkflowRow {
33 pub id: String,
34 pub repo_id: String,
35 pub repo: String,
36 pub path: String,
37 pub name: String,
38 pub source: String,
39 pub events: String,
40 pub crons: String,
41 pub error: Option<String>,
42 pub state: String,
43 pub updated_at: String,
44}
45
Fewer Artifacts reads: the store is asked for a handle only when needed, objects are kept in the isolate, and issue events read no workflows nobody listens for46/// What a workflow's row says once its file has left the default branch.
47pub const GONE: &str = "Its file is no longer on the default branch.";
48
49/// Whether a synced workflow could start on `event`: it lists the event,
50/// or it did not parse (and is still on the branch), so reading it again
51/// reports why.
52pub fn could_start(events: &str, error: Option<&str>, event: &str) -> bool {
53 match error {
54 Some(error) => error != GONE,
55 None => serde_json::from_str::<Vec<String>>(events).is_ok_and(|events| events.iter().any(|e| e == event)),
56 }
57}
58
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put59impl Actions {
60 /// The workflow files of `path` as of `git_ref` (the default branch
61 /// when absent).
62 pub async fn read_workflows(&self, path: &RepoPath, actor: &User, git_ref: Option<&str>) -> Result<Read> {
Merge branch 'mirroring' into artifacts-mode63 self.read_folders(path, actor, git_ref, &[FOLDER]).await
64 }
65
66 /// The workflow files a repository runs: `.g1t/workflows`, and on a
67 /// mirror in CI failover or taken over, `.github/workflows` too (see
A repository can run a managed workflow: one installed by g1t's own services without a file in the repository. It is listed, started, run and re-run like any workflow in `.g1t/workflows/`, under the path `managed/<name>.yml`, and a workflow of the same `name:` in the repository runs in its place, so moving one into the repository is committing its source. The Actions page says a managed workflow is managed by g1t rather than linking to a file. Nothing installs one yet; the deploy workflow deployments will install, which calls g1t's reusable deploy workflow with a project's settings as its inputs and reports as `g1t / deploy`, is written and tested but not installed.68 /// mirrored.rs), where g1t's own stands in for one of the same name;
A workspace can manage workflows in its repositories, as GitHub's required workflows do, through g1t's managed mechanism. An owner adds a workflow file from one of the workspace's own repositories (`owner/repo/.g1t/workflows/file.yml@ref`) with a name, a mode and a target, kept in the actions service's new `workspace_managed_workflows` table (migration 0017): the file is copied at the commit its ref names when it is saved, so only an owner changes what runs with every repository's secrets, and copying it again takes up a change. The target is repository names and patterns (`*`, `!` to leave one out, the last match deciding; empty is every repository) and a visibility, and a source in a private repository runs in private repositories only, as reach.rs keeps a public repository from using a private one's workflows. Wherever a repository's workflows are read, a fork's pull request and the merge queue included, `g1t_actions::managed::arrange` puts its files and the managed workflows together: a required workflow runs as `managed/<name>.yml` and sets aside a file of its `name:`, so nothing else reports its check and a ruleset can require it, and can't be turned off in the repository; a default one gives way to a file of its name, as the workflows g1t's services install do, and a repository admin can turn it off. It runs as the repository's own workflow, with its token and secrets. A change syncs the first 20 repositories it reaches at once and leaves the rest to be synced when their workflows are listed or by the minute's sweep, a few a minute, their managed schedules waiting until then; it is recorded in the audit log. A workflow's view says what manages it (`managedBy`), and the Actions list now includes managed workflows, which it had left out. The API has `GET` and `POST /workspaces/{workspace}/actions/managed-workflows` and `PATCH` and `DELETE /workspaces/{workspace}/actions/managed-workflows/{workflow}`, owners only, and the `workflow` MCP tool `list_managed_workflows`, `add_managed_workflow`, `update_managed_workflow` and `remove_managed_workflow`; the reference and the OpenAPI copy say so.69 /// with the managed workflows installed in it and its workspace's that
70 /// target it, as managed.rs puts them together.
Merge branch 'mirroring' into artifacts-mode71 pub async fn read_for(&self, repo: &Repo, actor: &User, git_ref: Option<&str>, github: bool) -> Result<Read> {
72 let path = RepoPath {
73 namespace: repo.namespace.clone(),
74 name: repo.name.clone(),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put75 };
A repository can run a managed workflow: one installed by g1t's own services without a file in the repository. It is listed, started, run and re-run like any workflow in `.g1t/workflows/`, under the path `managed/<name>.yml`, and a workflow of the same `name:` in the repository runs in its place, so moving one into the repository is committing its source. The Actions page says a managed workflow is managed by g1t rather than linking to a file. Nothing installs one yet; the deploy workflow deployments will install, which calls g1t's reusable deploy workflow with a project's settings as its inputs and reports as `g1t / deploy`, is written and tested but not installed.76 let mut read = if github {
77 let read = self.read_folders(&path, actor, git_ref, &[FOLDER, crate::mirrored::GITHUB_FOLDER]).await?;
78 Read {
79 files: crate::mirrored::prefer_g1t(read.files),
80 head: read.head,
81 }
82 } else {
83 self.read_workflows(&path, actor, git_ref).await?
84 };
A workspace can manage workflows in its repositories, as GitHub's required workflows do, through g1t's managed mechanism. An owner adds a workflow file from one of the workspace's own repositories (`owner/repo/.g1t/workflows/file.yml@ref`) with a name, a mode and a target, kept in the actions service's new `workspace_managed_workflows` table (migration 0017): the file is copied at the commit its ref names when it is saved, so only an owner changes what runs with every repository's secrets, and copying it again takes up a change. The target is repository names and patterns (`*`, `!` to leave one out, the last match deciding; empty is every repository) and a visibility, and a source in a private repository runs in private repositories only, as reach.rs keeps a public repository from using a private one's workflows. Wherever a repository's workflows are read, a fork's pull request and the merge queue included, `g1t_actions::managed::arrange` puts its files and the managed workflows together: a required workflow runs as `managed/<name>.yml` and sets aside a file of its `name:`, so nothing else reports its check and a ruleset can require it, and can't be turned off in the repository; a default one gives way to a file of its name, as the workflows g1t's services install do, and a repository admin can turn it off. It runs as the repository's own workflow, with its token and secrets. A change syncs the first 20 repositories it reaches at once and leaves the rest to be synced when their workflows are listed or by the minute's sweep, a few a minute, their managed schedules waiting until then; it is recorded in the audit log. A workflow's view says what manages it (`managedBy`), and the Actions list now includes managed workflows, which it had left out. The API has `GET` and `POST /workspaces/{workspace}/actions/managed-workflows` and `PATCH` and `DELETE /workspaces/{workspace}/actions/managed-workflows/{workflow}`, owners only, and the `workflow` MCP tool `list_managed_workflows`, `add_managed_workflow`, `update_managed_workflow` and `remove_managed_workflow`; the reference and the OpenAPI copy say so.85 // And the managed workflows beside them (managed.rs).
86 read.files = self.with_managed(repo, read.files).await?;
A repository can run a managed workflow: one installed by g1t's own services without a file in the repository. It is listed, started, run and re-run like any workflow in `.g1t/workflows/`, under the path `managed/<name>.yml`, and a workflow of the same `name:` in the repository runs in its place, so moving one into the repository is committing its source. The Actions page says a managed workflow is managed by g1t rather than linking to a file. Nothing installs one yet; the deploy workflow deployments will install, which calls g1t's reusable deploy workflow with a project's settings as its inputs and reports as `g1t / deploy`, is written and tested but not installed.87 Ok(read)
Merge branch 'mirroring' into artifacts-mode88 }
89
90 async fn read_folders(&self, path: &RepoPath, actor: &User, git_ref: Option<&str>, folders: &[&str]) -> Result<Read> {
91 let viewer = Some(actor.clone());
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put92 let mut files = Vec::new();
Merge branch 'mirroring' into artifacts-mode93 let mut found_head = None;
94 for folder in folders {
95 let tree: Outcome<TreeView> = g1t_kit::call(
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put96 &self.repos,
Merge branch 'mirroring' into artifacts-mode97 "tree",
98 &TreeArgs {
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put99 path: path.clone(),
100 viewer: viewer.clone(),
Merge branch 'mirroring' into artifacts-mode101 git_ref: git_ref.map(str::to_owned),
102 tree_path: (*folder).to_owned(),
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put103 },
104 )
105 .await?;
Merge branch 'mirroring' into artifacts-mode106 let (entries, head, resolved) = match tree {
107 Outcome::Ok(tree) => (tree.entries, tree.head.map(|commit| commit.hash), tree.git_ref),
108 // No folder: no workflows from it.
109 Outcome::Fail(_) => continue,
110 };
111 let at = head.clone().unwrap_or(resolved);
112 found_head = found_head.or(head);
113 for entry in entries
114 .into_iter()
115 .filter(|entry| matches!(entry.kind, EntryKind::Blob | EntryKind::Exec))
116 .filter(|entry| entry.name.ends_with(".yml") || entry.name.ends_with(".yaml"))
117 .take(MAX_WORKFLOWS.saturating_sub(files.len()))
118 {
119 let file_path = format!("{folder}/{}", entry.name);
120 let blob: Outcome<BlobView> = g1t_kit::call(
121 &self.repos,
122 "blob",
123 &BlobArgs {
124 path: path.clone(),
125 viewer: viewer.clone(),
126 git_ref: at.clone(),
127 file_path: file_path.clone(),
128 },
129 )
130 .await?;
131 if let Outcome::Ok(BlobView { text: Some(source), .. }) = blob {
A workspace can manage workflows in its repositories, as GitHub's required workflows do, through g1t's managed mechanism. An owner adds a workflow file from one of the workspace's own repositories (`owner/repo/.g1t/workflows/file.yml@ref`) with a name, a mode and a target, kept in the actions service's new `workspace_managed_workflows` table (migration 0017): the file is copied at the commit its ref names when it is saved, so only an owner changes what runs with every repository's secrets, and copying it again takes up a change. The target is repository names and patterns (`*`, `!` to leave one out, the last match deciding; empty is every repository) and a visibility, and a source in a private repository runs in private repositories only, as reach.rs keeps a public repository from using a private one's workflows. Wherever a repository's workflows are read, a fork's pull request and the merge queue included, `g1t_actions::managed::arrange` puts its files and the managed workflows together: a required workflow runs as `managed/<name>.yml` and sets aside a file of its `name:`, so nothing else reports its check and a ruleset can require it, and can't be turned off in the repository; a default one gives way to a file of its name, as the workflows g1t's services install do, and a repository admin can turn it off. It runs as the repository's own workflow, with its token and secrets. A change syncs the first 20 repositories it reaches at once and leaves the rest to be synced when their workflows are listed or by the minute's sweep, a few a minute, their managed schedules waiting until then; it is recorded in the audit log. A workflow's view says what manages it (`managedBy`), and the Actions list now includes managed workflows, which it had left out. The API has `GET` and `POST /workspaces/{workspace}/actions/managed-workflows` and `PATCH` and `DELETE /workspaces/{workspace}/actions/managed-workflows/{workflow}`, owners only, and the `workflow` MCP tool `list_managed_workflows`, `add_managed_workflow`, `update_managed_workflow` and `remove_managed_workflow`; the reference and the OpenAPI copy say so.132 files.push(WorkflowFile { path: file_path, source, required: false });
Merge branch 'mirroring' into artifacts-mode133 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put134 }
135 }
Merge branch 'mirroring' into artifacts-mode136 Ok(Read { files, head: found_head })
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put137 }
138
139 /// Keeps the `workflows` table in step with the default branch.
140 pub async fn sync(&self, repo: &Repo, actor: &User) -> Result<()> {
Merge branch 'mirroring' into artifacts-mode141 let github = crate::mirrored::policy(repo.mirror.as_ref(), false).github;
142 let read = self.read_for(repo, actor, None, github).await?;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put143 let full_name = format!("{}/{}", repo.namespace, repo.name);
144 let now = rfc3339(now_ms());
145 let mut statements = Vec::new();
146 for file in &read.files {
147 let parsed = workflow::parse(&file.source);
148 let (name, error, events, crons) = match &parsed {
149 Ok(parsed) => (
150 parsed.display_name(&file.path),
151 None,
152 parsed.triggers.iter().map(|t| t.event.clone()).collect::<Vec<_>>(),
153 parsed.trigger("schedule").map(|t| t.crons.clone()).unwrap_or_default(),
154 ),
155 Err(problem) => (file.path.clone(), Some(problem.clone()), Vec::new(), Vec::new()),
156 };
157 statements.push(
158 self.db
159 .prepare(
160 "INSERT INTO workflows (id, repo_id, repo, path, name, source, events, crons, error, updated_at)
161 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
162 ON CONFLICT (repo_id, path) DO UPDATE SET
163 repo = excluded.repo, name = excluded.name, source = excluded.source,
164 events = excluded.events, crons = excluded.crons, error = excluded.error,
165 updated_at = excluded.updated_at",
166 )
167 .bind(&[
168 new_id("wfl", now_ms()).into(),
169 repo.id.as_str().into(),
170 full_name.as_str().into(),
171 file.path.as_str().into(),
172 name.into(),
173 file.source.as_str().into(),
174 serde_json::to_string(&events)?.into(),
175 serde_json::to_string(&crons)?.into(),
176 optional(error.as_deref()),
177 now.as_str().into(),
178 ])?,
179 );
180 }
181 // A workflow whose file is gone keeps its runs, but no longer runs
182 // on schedule or by hand: it is listed only while it has runs.
183 let kept: Vec<String> = read.files.iter().map(|file| file.path.clone()).collect();
184 let existing = self
185 .db
186 .prepare("SELECT * FROM workflows WHERE repo_id = ?")
187 .bind(&[repo.id.as_str().into()])?
188 .all()
189 .await?
190 .results::<WorkflowRow>()?;
191 for row in existing.iter().filter(|row| !kept.contains(&row.path)) {
192 statements.push(
193 self.db
Fewer Artifacts reads: the store is asked for a handle only when needed, objects are kept in the isolate, and issue events read no workflows nobody listens for194 .prepare("UPDATE workflows SET crons = '[]', error = ? WHERE id = ?")
195 .bind(&[GONE.into(), row.id.as_str().into()])?,
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put196 );
197 }
A workspace can manage workflows in its repositories, as GitHub's required workflows do, through g1t's managed mechanism. An owner adds a workflow file from one of the workspace's own repositories (`owner/repo/.g1t/workflows/file.yml@ref`) with a name, a mode and a target, kept in the actions service's new `workspace_managed_workflows` table (migration 0017): the file is copied at the commit its ref names when it is saved, so only an owner changes what runs with every repository's secrets, and copying it again takes up a change. The target is repository names and patterns (`*`, `!` to leave one out, the last match deciding; empty is every repository) and a visibility, and a source in a private repository runs in private repositories only, as reach.rs keeps a public repository from using a private one's workflows. Wherever a repository's workflows are read, a fork's pull request and the merge queue included, `g1t_actions::managed::arrange` puts its files and the managed workflows together: a required workflow runs as `managed/<name>.yml` and sets aside a file of its `name:`, so nothing else reports its check and a ruleset can require it, and can't be turned off in the repository; a default one gives way to a file of its name, as the workflows g1t's services install do, and a repository admin can turn it off. It runs as the repository's own workflow, with its token and secrets. A change syncs the first 20 repositories it reaches at once and leaves the rest to be synced when their workflows are listed or by the minute's sweep, a few a minute, their managed schedules waiting until then; it is recorded in the audit log. A workflow's view says what manages it (`managedBy`), and the Actions list now includes managed workflows, which it had left out. The API has `GET` and `POST /workspaces/{workspace}/actions/managed-workflows` and `PATCH` and `DELETE /workspaces/{workspace}/actions/managed-workflows/{workflow}`, owners only, and the `workflow` MCP tool `list_managed_workflows`, `add_managed_workflow`, `update_managed_workflow` and `remove_managed_workflow`; the reference and the OpenAPI copy say so.198 // A workspace's required workflow cannot stay turned off, even one
199 // turned off while it was a default.
200 let required: Vec<&str> = read.files.iter().filter(|file| file.required).map(|file| file.path.as_str()).collect();
201 if !required.is_empty() {
202 statements.push(
203 self.db
204 .prepare("UPDATE workflows SET state = 'active' WHERE repo_id = ? AND path IN (SELECT value FROM json_each(?))")
205 .bind(&[repo.id.as_str().into(), serde_json::to_string(&required)?.into()])?,
206 );
207 }
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put208 statements.push(
209 self.db
210 .prepare("INSERT OR REPLACE INTO synced (repo_id, at) VALUES (?, ?)")
211 .bind(&[repo.id.as_str().into(), now.into()])?,
212 );
213 self.db.batch(statements).await?;
214 Ok(())
215 }
216
Fewer Artifacts reads: the store is asked for a handle only when needed, objects are kept in the isolate, and issue events read no workflows nobody listens for217 /// Whether any of the repository's default-branch workflows could
218 /// start on `event`, from the synced table; None before the first sync.
219 pub async fn listens(&self, repo_id: &str, event: &str) -> Result<Option<bool>> {
220 #[derive(Deserialize)]
221 struct Row {
222 events: String,
223 error: Option<String>,
224 }
225 if !self.synced(repo_id).await? {
226 return Ok(None);
227 }
228 let rows = self
229 .db
230 .prepare("SELECT events, error FROM workflows WHERE repo_id = ? AND state = 'active'")
231 .bind(&[repo_id.into()])?
232 .all()
233 .await?
234 .results::<Row>()?;
235 Ok(Some(rows.iter().any(|row| could_start(&row.events, row.error.as_deref(), event))))
236 }
237
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put238 pub async fn synced(&self, repo_id: &str) -> Result<bool> {
239 Ok(self
240 .db
241 .prepare("SELECT COUNT(*) AS n FROM synced WHERE repo_id = ?")
242 .bind(&[repo_id.into()])?
243 .first::<Count>(None)
244 .await?
245 .is_some_and(|count| count.n > 0))
246 }
247
248 /// The row for a workflow file, made if it is new (a file that exists
249 /// only on a branch still gets its runs counted and listed).
250 pub async fn workflow_row(&self, repo: &Repo, path: &str, name: &str, source: &str) -> Result<WorkflowRow> {
251 let now = rfc3339(now_ms());
252 self.db
253 .prepare(
254 "INSERT INTO workflows (id, repo_id, repo, path, name, source, events, error, updated_at)
255 VALUES (?, ?, ?, ?, ?, ?, '[]', 'Its file is not on the default branch.', ?)
256 ON CONFLICT (repo_id, path) DO NOTHING",
257 )
258 .bind(&[
259 new_id("wfl", now_ms()).into(),
260 repo.id.as_str().into(),
261 format!("{}/{}", repo.namespace, repo.name).into(),
262 path.into(),
263 name.into(),
264 source.into(),
265 now.into(),
266 ])?
267 .run()
268 .await?;
269 self.db
270 .prepare("SELECT * FROM workflows WHERE repo_id = ? AND path = ?")
271 .bind(&[repo.id.as_str().into(), path.into()])?
272 .first::<WorkflowRow>(None)
273 .await?
274 .ok_or_else(|| worker::Error::RustError("the workflow was not recorded".into()))
275 }
276}
Fewer Artifacts reads: the store is asked for a handle only when needed, objects are kept in the isolate, and issue events read no workflows nobody listens for277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn a_synced_workflow_starts_on_the_events_it_lists_or_when_broken() {
284 assert!(could_start(r#"["push","issues"]"#, None, "issues"));
285 assert!(!could_start(r#"["push","pull_request"]"#, None, "issue_comment"));
286 // A file that does not parse is read again, so its error shows.
287 assert!(could_start("[]", Some("bad yaml"), "issues"));
288 // A file gone from the branch starts nothing.
289 assert!(!could_start("[]", Some(GONE), "issues"));
290 }
291}

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