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 put | 1 | //! 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 | ||
| 5 | use g1t_actions::workflow::{self, FOLDER}; | |
| 6 | use g1t_contracts::new_id; | |
| 7 | use g1t_contracts::repos::{BlobArgs, BlobView, EntryKind, Repo, RepoPath, TreeArgs, TreeView}; | |
| 8 | use g1t_contracts::time::rfc3339; | |
| 9 | use g1t_contracts::{Outcome, User}; | |
| 10 | use g1t_kit::now_ms; | |
| 11 | use serde::Deserialize; | |
| 12 | use worker::Result; | |
| 13 | ||
| 14 | use crate::{Actions, Count, MAX_WORKFLOWS, optional}; | |
| 15 | ||
| 16 | /// One workflow file as read at a commit. | |
| 17 | pub 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 put | 23 | } |
| 24 | ||
| 25 | /// The files at a commit, and the commit the ref resolved to. | |
| 26 | pub struct Read { | |
| 27 | pub files: Vec<WorkflowFile>, | |
| 28 | pub head: Option<String>, | |
| 29 | } | |
| 30 | ||
| 31 | #[derive(Deserialize)] | |
| 32 | pub 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 for | 46 | /// What a workflow's row says once its file has left the default branch. |
| 47 | pub 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. | |
| 52 | pub 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 put | 59 | impl 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-mode | 63 | 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-mode | 71 | 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 put | 75 | }; |
| 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-mode | 88 | } |
| 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 put | 92 | let mut files = Vec::new(); |
| Merge branch 'mirroring' into artifacts-mode | 93 | 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 put | 96 | &self.repos, |
| Merge branch 'mirroring' into artifacts-mode | 97 | "tree", |
| 98 | &TreeArgs { | |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 99 | path: path.clone(), |
| 100 | viewer: viewer.clone(), | |
| Merge branch 'mirroring' into artifacts-mode | 101 | 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 put | 103 | }, |
| 104 | ) | |
| 105 | .await?; | |
| Merge branch 'mirroring' into artifacts-mode | 106 | 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-mode | 133 | } |
| Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put | 134 | } |
| 135 | } | |
| Merge branch 'mirroring' into artifacts-mode | 136 | 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 put | 137 | } |
| 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-mode | 141 | 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 put | 143 | 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 for | 194 | .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 put | 196 | ); |
| 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 put | 208 | 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 for | 217 | /// 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 put | 238 | 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 for | 277 | |
| 278 | #[cfg(test)] | |
| 279 | mod 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.