Skip to content
647 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.

Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.1//! Managed workflows (`g1t_actions::managed`): workflows installed into a
2//! repository without committing them. g1t's services install them in one
3//! repository (`managed_workflows`, such as a project's deploy workflow);
4//! a workspace's owners add them to many (`workspace_managed_workflows`),
5//! each with a target and a mode. Both are read beside the repository's own
6//! files wherever its workflows are read (`sync.rs` `read_for`, and a fork's
7//! pull request in trigger.rs), and so listed, started, run and re-run as
8//! any other workflow is, as the repository's own, with its token and its
9//! secrets.
10//!
11//! A workspace's workflow runs from a copy of its source file taken when an
12//! owner saves it, not from the file as it is at each run: whoever can push
13//! to the source repository cannot change what runs with every targeted
14//! repository's secrets. An owner copies it again to take up a change.
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.15
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.16use g1t_actions::managed::{self, Arranged, Mode, Target, VISIBILITIES};
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.17use g1t_actions::workflow;
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.18use g1t_contracts::actions::{
19 ManagedBy, RemoveManagedWorkflowArgs, RemoveWorkspaceManagedWorkflowArgs, SetManagedDeployArgs, SetManagedWorkflowArgs,
20 SetWorkspaceManagedWorkflowArgs, WorkspaceManagedWorkflow, WorkspaceManagedWorkflowsArgs,
21};
22use g1t_contracts::repos::{BlobArgs, BlobView, ListArgs, Repo, RepoPath, TreeArgs, TreeView};
23use g1t_contracts::audit::{AuditActor, AuditOutcome, AuditTarget, NewAuditEntry, Surface};
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.24use g1t_contracts::time::rfc3339;
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.25use g1t_contracts::{FailureCode, Outcome, PrincipalKind, Role, User, new_id};
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.26use g1t_kit::now_ms;
27use serde::Deserialize;
28use worker::Result;
29
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.30use crate::reach::{UsesRef, is_workflow_path, parse_uses};
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.31use crate::sync::WorkflowFile;
32use crate::{Actions, fail};
33
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.34/// Repositories synced at once when a workspace's workflow changes; the
35/// rest are synced when their workflows are next listed or their default
36/// branch is pushed to. Their runs read the workflows as they are anyway.
37const SYNCED_AT_ONCE: usize = 20;
38
39/// The most repositories the repos service's `list` answers with.
40const LISTED: usize = 50;
41
42/// Repositories [`Actions::catch_up_syncs`] syncs each minute.
43const CAUGHT_UP_PER_MINUTE: u32 = 5;
44
45/// The most repository names and patterns a target takes.
46const MAX_PATTERNS: usize = 100;
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.47
48#[derive(Deserialize)]
49struct Row {
50 name: String,
51 source: String,
52}
53
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.54/// One row of `workspace_managed_workflows`.
55#[derive(Clone, Deserialize)]
56struct WorkspaceRow {
57 name: String,
58 source_ref: String,
59 source_repo_id: String,
60 source_sha: Option<String>,
61 source_private: u32,
62 source: String,
63 workflow_name: String,
64 mode: String,
65 repositories: String,
66 visibility: String,
67 created_at: String,
68 updated_at: String,
69 updated_by: String,
70}
71
72impl WorkspaceRow {
73 fn mode(&self) -> Mode {
74 Mode::parse(&self.mode).unwrap_or(Mode::Default)
75 }
76
77 fn target(&self) -> Target {
78 Target {
79 repositories: serde_json::from_str(&self.repositories).unwrap_or_default(),
80 visibility: self.visibility.clone(),
81 }
82 }
83
84 /// Whether it runs in `repo`.
85 fn targets(&self, repo: &Repo) -> bool {
86 self.target().includes(&repo.name, repo.is_private, self.source_private != 0)
87 }
88
89 fn view(&self) -> WorkspaceManagedWorkflow {
90 WorkspaceManagedWorkflow {
91 name: self.name.clone(),
92 path: managed::path(&self.name),
93 workflow_name: self.workflow_name.clone(),
94 source: self.source_ref.clone(),
95 source_sha: self.source_sha.clone(),
96 source_private: self.source_private != 0,
97 mode: self.mode().as_str().to_owned(),
98 repositories: self.target().repositories,
99 visibility: self.visibility.clone(),
100 content: self.source.clone(),
101 created_at: self.created_at.clone(),
102 updated_at: self.updated_at.clone(),
103 updated_by: self.updated_by.clone(),
104 }
105 }
106}
107
108/// The workspace's managed workflow's source as given:
109/// `owner/repo/.g1t/workflows/file.yml@ref`, a workflow file in a
110/// repository of `workspace`.
111fn parse_source(text: &str, workspace: &str) -> std::result::Result<UsesRef, String> {
112 let shape = "Give the source as owner/repo/.g1t/workflows/file.yml@ref, a workflow file in one of the workspace's repositories.";
113 let source = parse_uses(text).ok_or_else(|| shape.to_owned())?;
114 if !is_workflow_path(&source.path) {
115 return Err(shape.to_owned());
116 }
117 if !source.owner.eq_ignore_ascii_case(workspace) {
118 return Err(format!("The source must be in one of {workspace}'s own repositories."));
119 }
120 Ok(source)
121}
122
123/// Whether a workflow copied from a repository (private or not) may target
124/// repositories of `visibility`: a private repository's workflow never runs
125/// in a public one, whose workflow page and logs anyone can read, as a
126/// public repository's workflows may not use a private one's (reach.rs).
127fn may_reach(source: &str, source_private: bool, visibility: &str) -> std::result::Result<(), String> {
128 if source_private && visibility != "private" {
129 return Err(format!(
130 "{source} is in a private repository, so it can run in private repositories only: choose Private repositories, or a source in a public repository."
131 ));
132 }
133 Ok(())
134}
135
136/// Names and patterns as given: trimmed, without empties or repeats.
137fn clean_patterns(given: &[String]) -> std::result::Result<Vec<String>, String> {
138 let mut out: Vec<String> = Vec::new();
139 for pattern in given.iter().map(|pattern| pattern.trim()).filter(|pattern| !pattern.is_empty()) {
140 if pattern.len() > 100 || pattern.contains('/') {
141 return Err(format!("\"{pattern}\" is not a repository name or pattern."));
142 }
143 if !out.iter().any(|kept| kept.eq_ignore_ascii_case(pattern)) {
144 out.push(pattern.to_owned());
145 }
146 }
147 if out.len() > MAX_PATTERNS {
148 return Err(format!("A workflow targets at most {MAX_PATTERNS} repository names and patterns."));
149 }
150 Ok(out)
151}
152
153/// An entry for the workspace's audit log: who changed which of its
154/// managed workflows, and how.
155fn audit_entry(actor: &User, action: &str, workspace: &str, name: &str, message: String) -> NewAuditEntry {
156 NewAuditEntry {
157 actor: AuditActor::of(actor),
158 action: action.to_owned(),
159 surface: Surface::Rest,
160 target: AuditTarget { workspace: workspace.to_owned(), path: Some(managed::path(name)), ..AuditTarget::default() },
161 outcome: AuditOutcome::Allowed,
162 rule: "managed_workflow".to_owned(),
163 result: Some("ok".to_owned()),
164 message: Some(message),
165 request_id: new_id("req", now_ms()),
166 }
167}
168
169/// Owners only: Err is the refusal.
170fn owner_of<T>(actor: &User, workspace: &str) -> std::result::Result<(), Outcome<T>> {
171 if actor.token.as_ref().is_some_and(|token| token.job.is_some()) {
172 return Err(fail(FailureCode::Forbidden, "A workflow job's token cannot change the workspace's workflows."));
173 }
174 if actor.kind != PrincipalKind::User || actor.role_in(workspace) != Some(Role::Owner) {
175 return Err(fail(FailureCode::Forbidden, format!("Only an owner of {workspace} can change the workflows it manages.")));
176 }
177 Ok(())
178}
179
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.180impl Actions {
181 /// `set_managed_workflow`.
182 pub async fn set_managed_workflow(&self, a: SetManagedWorkflowArgs) -> Result<Outcome<bool>> {
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.183 if !managed::valid_name(&a.name) {
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.184 return Ok(fail(FailureCode::Invalid, "A managed workflow's name is lowercase letters, digits and hyphens."));
185 }
186 if let Err(problem) = workflow::parse(&a.source) {
187 return Ok(fail(FailureCode::Invalid, format!("The managed workflow does not read as a workflow: {problem}")));
188 }
189 let Some((repo, ws)) = self.repo_by_id(&a.repo_id).await? else {
190 return Ok(fail(FailureCode::NotFound, "No such repository."));
191 };
192 self.db
193 .prepare(
194 "INSERT INTO managed_workflows (repo_id, name, source, origin, updated_at) VALUES (?, ?, ?, ?, ?)
195 ON CONFLICT (repo_id, name) DO UPDATE SET source = excluded.source, origin = excluded.origin, updated_at = excluded.updated_at",
196 )
197 .bind(&[a.repo_id.as_str().into(), a.name.as_str().into(), a.source.as_str().into(), a.origin.as_str().into(), rfc3339(now_ms()).into()])?
198 .run()
199 .await?;
200 // Listed, and on schedule, at once.
201 self.sync(&repo, &ws).await?;
202 Ok(Outcome::Ok(true))
203 }
204
205 /// `remove_managed_workflow`.
206 pub async fn remove_managed_workflow(&self, a: RemoveManagedWorkflowArgs) -> Result<Outcome<bool>> {
207 let removed = self
208 .db
209 .prepare("DELETE FROM managed_workflows WHERE repo_id = ? AND name = ? RETURNING name")
210 .bind(&[a.repo_id.as_str().into(), a.name.as_str().into()])?
211 .first::<serde_json::Value>(None)
212 .await?
213 .is_some();
214 if removed && let Some((repo, ws)) = self.repo_by_id(&a.repo_id).await? {
215 self.sync(&repo, &ws).await?;
216 }
217 Ok(Outcome::Ok(removed))
218 }
219
A project can build with a workflow. Turning on **Build with a workflow** under Settings, Deployments installs a managed workflow named `g1t` that calls g1t's reusable deploy workflow (`g1t/workflows/.g1t/workflows/deploy.yml@v1`, published from `workflows/` by the actions release) with the project's settings, and pushes and pull requests then start it rather than a build of Deployments' own: the same previews and production, at the same addresses, reported as `g1t / deploy`, now the check of the workflow's job, listed on the Actions page with its runs and logs. A pull request from a fork runs it with a read-only token, as on GitHub, and still gets its preview without the project's build secrets: a read-only job token may open a preview build only from a run of a managed workflow. Fork pull requests run the repository's managed workflows too. Its jobs are Actions time, at cost plus 20% on g1t's machines and free on a self-hosted runner. A workflow named `g1t` in `.g1t/workflows/` takes its place. The deployments and Actions guides say so.220 /// `set_managed_deploy`.
221 pub async fn set_managed_deploy(&self, a: SetManagedDeployArgs) -> Result<Outcome<bool>> {
222 let name = format!("deploy-{}", a.project);
223 let settings = managed::DeploySettings {
224 project: a.project,
225 production_branch: a.production_branch,
226 root_dir: a.root_dir,
227 build_command: a.build_command,
228 output_dir: a.output_dir,
229 previews: a.previews,
230 production: a.production,
231 };
232 match managed::deploy_workflow(&settings) {
233 Some(source) => self.set_managed_workflow(SetManagedWorkflowArgs { repo_id: a.repo_id, name, source, origin: "deployments".to_owned() }).await,
234 None => {
235 self.remove_managed_workflow(RemoveManagedWorkflowArgs { repo_id: a.repo_id, name }).await?;
236 Ok(Outcome::Ok(false))
237 }
238 }
239 }
240
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.241 // --- A workspace's ------------------------------------------------------------
242
243 async fn workspace_rows(&self, workspace: &str) -> Result<Vec<WorkspaceRow>> {
244 self.db
245 .prepare("SELECT * FROM workspace_managed_workflows WHERE namespace = ? ORDER BY name")
246 .bind(&[workspace.to_lowercase().into()])?
247 .all()
248 .await?
249 .results::<WorkspaceRow>()
250 }
251
252 async fn workspace_row(&self, workspace: &str, name: &str) -> Result<Option<WorkspaceRow>> {
253 self.db
254 .prepare("SELECT * FROM workspace_managed_workflows WHERE namespace = ? AND name = ?")
255 .bind(&[workspace.to_lowercase().into(), name.into()])?
256 .first::<WorkspaceRow>(None)
257 .await
258 }
259
260 /// `workspace_managed_workflows`.
261 pub async fn workspace_managed_workflows(&self, a: WorkspaceManagedWorkflowsArgs) -> Result<Outcome<Vec<WorkspaceManagedWorkflow>>> {
262 let workspace = a.workspace.trim().to_lowercase();
263 let Some(viewer) = a.viewer.as_ref().filter(|viewer| viewer.is_member(&workspace)) else {
264 return Ok(fail(FailureCode::NotFound, "There is no such workspace."));
265 };
266 if viewer.role_in(&workspace) != Some(Role::Owner) {
267 return Ok(fail(FailureCode::Forbidden, format!("Only an owner of {workspace} can see the workflows it manages.")));
268 }
269 Ok(Outcome::Ok(self.workspace_rows(&workspace).await?.iter().map(WorkspaceRow::view).collect()))
270 }
271
272 /// Copies a workspace's managed workflow's source, as `actor` reads it:
273 /// the file at the commit its ref names. Err is the refusal.
274 async fn copy_source(&self, actor: &User, workspace: &str, given: &str) -> Result<std::result::Result<(Repo, String, Option<String>), String>> {
275 let source = match parse_source(given, workspace) {
276 Ok(source) => source,
277 Err(problem) => return Ok(Err(problem)),
278 };
279 let path = RepoPath { namespace: source.owner.clone(), name: source.repo.clone() };
280 let Some(repo) = self.visible_repo(&path, &Some(actor.clone())).await? else {
281 return Ok(Err(format!("There is no repository {} in {workspace}.", source.full_name())));
282 };
283 let folder = source.path.rsplit_once('/').map_or("", |(folder, _)| folder).to_owned();
284 let tree: Outcome<TreeView> = g1t_kit::call(
285 &self.repos,
286 "tree",
287 &TreeArgs { path: path.clone(), viewer: Some(actor.clone()), git_ref: Some(source.git_ref.clone()), tree_path: folder },
288 )
289 .await?;
290 let Outcome::Ok(tree) = tree else {
291 return Ok(Err(format!("{} has no {} at {}.", source.full_name(), source.path, source.git_ref)));
292 };
293 let sha = tree.head.map(|commit| commit.hash);
294 let blob: Outcome<BlobView> = g1t_kit::call(
295 &self.repos,
296 "blob",
297 &BlobArgs {
298 path,
299 viewer: Some(actor.clone()),
300 git_ref: sha.clone().unwrap_or_else(|| source.git_ref.clone()),
301 file_path: source.path.clone(),
302 },
303 )
304 .await?;
305 let Outcome::Ok(BlobView { text: Some(text), .. }) = blob else {
306 return Ok(Err(format!("{} has no {} at {}.", source.full_name(), source.path, source.git_ref)));
307 };
308 if let Err(problem) = workflow::parse(&text) {
309 return Ok(Err(format!("{} does not read as a workflow: {problem}", source.path)));
310 }
311 Ok(Ok((repo, text, sha)))
312 }
313
314 /// `set_workspace_managed_workflow`.
315 pub async fn set_workspace_managed_workflow(&self, a: SetWorkspaceManagedWorkflowArgs) -> Result<Outcome<WorkspaceManagedWorkflow>> {
316 let workspace = a.workspace.trim().to_lowercase();
317 if let Err(refused) = owner_of(&a.actor, &workspace) {
318 return Ok(refused);
319 }
320 let name = a.name.trim().to_owned();
321 if !managed::valid_name(&name) {
322 return Ok(fail(FailureCode::Invalid, "A managed workflow's name is lowercase letters, digits and hyphens, at most 64."));
323 }
324 let before = self.workspace_row(&workspace, &name).await?;
325 match (&before, a.create) {
326 (Some(_), true) => return Ok(fail(FailureCode::Conflict, format!("{workspace} already manages a workflow called {name}."))),
327 (None, false) => return Ok(fail(FailureCode::NotFound, format!("{workspace} manages no workflow called {name}."))),
328 _ => {}
329 }
330 let mode = match a.mode.as_deref().map(Mode::parse) {
331 Some(Some(mode)) => mode,
332 Some(None) => return Ok(fail(FailureCode::Invalid, "mode is required or default.")),
333 None => match &before {
334 Some(row) => row.mode(),
335 None => return Ok(fail(FailureCode::Invalid, "Choose a mode: required or default.")),
336 },
337 };
338 let visibility = match a.visibility.as_deref().map(|text| text.trim().to_ascii_lowercase()) {
339 Some(text) if VISIBILITIES.contains(&text.as_str()) => text,
340 Some(_) => return Ok(fail(FailureCode::Invalid, "visibility is all, public or private.")),
341 None => before.as_ref().map_or_else(|| "all".to_owned(), |row| row.visibility.clone()),
342 };
343 let repositories = match &a.repositories {
344 Some(given) => match clean_patterns(given) {
345 Ok(patterns) => patterns,
346 Err(problem) => return Ok(fail(FailureCode::Invalid, problem)),
347 },
348 None => before.as_ref().map(|row| row.target().repositories).unwrap_or_default(),
349 };
350 let given_source = a.source.as_deref().map(str::trim).filter(|text| !text.is_empty()).map(str::to_owned);
351 let mut row = match (&before, given_source) {
352 (Some(row), None) if !a.refresh => row.clone(),
353 (before, given) => {
354 let Some(given) = given.or_else(|| before.as_ref().map(|row| row.source_ref.clone())) else {
355 return Ok(fail(FailureCode::Invalid, "Give the source as owner/repo/.g1t/workflows/file.yml@ref."));
356 };
357 let (source_repo, text, sha) = match self.copy_source(&a.actor, &workspace, &given).await? {
358 Ok(copied) => copied,
359 Err(problem) => return Ok(fail(FailureCode::Invalid, problem)),
360 };
361 let workflow_name = workflow::parse(&text).map(|parsed| parsed.display_name(&managed::path(&name))).unwrap_or_default();
362 WorkspaceRow {
363 name: name.clone(),
364 source_ref: given,
365 source_repo_id: source_repo.id,
366 source_sha: sha,
367 source_private: u32::from(source_repo.is_private),
368 source: text,
369 workflow_name,
370 mode: String::new(),
371 repositories: String::new(),
372 visibility: String::new(),
373 created_at: before.as_ref().map_or_else(|| rfc3339(now_ms()), |row| row.created_at.clone()),
374 updated_at: String::new(),
375 updated_by: String::new(),
376 }
377 }
378 };
379 if let Err(problem) = may_reach(&row.source_ref, row.source_private != 0, &visibility) {
380 return Ok(fail(FailureCode::Invalid, problem));
381 }
382 row.mode = mode.as_str().to_owned();
383 row.repositories = serde_json::to_string(&repositories)?;
384 row.visibility = visibility;
385 row.updated_at = rfc3339(now_ms());
386 row.updated_by = a.actor.username.clone();
387 self.db
388 .prepare(
389 "INSERT INTO workspace_managed_workflows
390 (namespace, name, source_ref, source_repo_id, source_sha, source_private, source, workflow_name, mode, repositories, visibility, created_at, updated_at, updated_by)
391 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)
392 ON CONFLICT (namespace, name) DO UPDATE SET
393 source_ref = ?3, source_repo_id = ?4, source_sha = ?5, source_private = ?6, source = ?7, workflow_name = ?8,
394 mode = ?9, repositories = ?10, visibility = ?11, updated_at = ?13, updated_by = ?14",
395 )
396 .bind(&[
397 workspace.as_str().into(),
398 row.name.as_str().into(),
399 row.source_ref.as_str().into(),
400 row.source_repo_id.as_str().into(),
401 crate::optional(row.source_sha.as_deref()),
402 row.source_private.into(),
403 row.source.as_str().into(),
404 row.workflow_name.as_str().into(),
405 row.mode.as_str().into(),
406 row.repositories.as_str().into(),
407 row.visibility.as_str().into(),
408 row.created_at.as_str().into(),
409 row.updated_at.as_str().into(),
410 row.updated_by.as_str().into(),
411 ])?
412 .run()
413 .await?;
414 let message = format!(
415 "{} the {} workflow {} from {}{}.",
416 if before.is_some() { "Changed" } else { "Added" },
417 row.mode,
418 row.name,
419 row.source_ref,
420 row.source_sha.as_deref().map(|sha| format!(" at {}", &sha[..sha.len().min(12)])).unwrap_or_default(),
421 );
422 let action = if before.is_some() { "update_workspace_managed_workflow" } else { "create_workspace_managed_workflow" };
423 self.audit(vec![audit_entry(&a.actor, action, &workspace, &row.name, message)]).await;
424 self.resync_workspace(&workspace, before.as_ref(), Some(&row)).await?;
425 Ok(Outcome::Ok(row.view()))
426 }
427
428 /// `remove_workspace_managed_workflow`.
429 pub async fn remove_workspace_managed_workflow(&self, a: RemoveWorkspaceManagedWorkflowArgs) -> Result<Outcome<bool>> {
430 let workspace = a.workspace.trim().to_lowercase();
431 if let Err(refused) = owner_of(&a.actor, &workspace) {
432 return Ok(refused);
433 }
434 let removed = self
435 .db
436 .prepare("DELETE FROM workspace_managed_workflows WHERE namespace = ? AND name = ? RETURNING *")
437 .bind(&[workspace.as_str().into(), a.name.trim().into()])?
438 .first::<WorkspaceRow>(None)
439 .await?;
440 if let Some(removed) = &removed {
441 let message = format!("Removed the {} workflow {}.", removed.mode, removed.name);
442 self.audit(vec![audit_entry(&a.actor, "delete_workspace_managed_workflow", &workspace, &removed.name, message)]).await;
443 self.resync_workspace(&workspace, Some(removed), None).await?;
444 }
445 Ok(Outcome::Ok(removed.is_some()))
446 }
447
448 /// Brings the workflows of the repositories a change reached (those the
449 /// workflow targeted `before`, or does `after`) up to date. Up to
450 /// [`SYNCED_AT_ONCE`] are synced now. In a larger workspace every other
451 /// repository's `synced` row goes, so it is synced when its workflows
452 /// are next listed, or by the minute's sweep ([`Self::catch_up_syncs`]);
453 /// until then its runs still read the workflows as they are, and its
454 /// managed workflows' schedules wait.
455 async fn resync_workspace(&self, workspace: &str, before: Option<&WorkspaceRow>, after: Option<&WorkspaceRow>) -> Result<()> {
456 let Some(ws) = self.workspace_actor(workspace).await? else { return Ok(()) };
457 // The workspace's newest repositories (the listing's first page).
458 let repos: Vec<Repo> = g1t_kit::call(
459 &self.repos,
460 "list",
461 &ListArgs { viewer: Some(ws.clone()), query: None, namespace: Some(workspace.to_owned()), member_only: false },
462 )
463 .await?;
464 let listed_all = repos.len() < LISTED;
465 let reached: Vec<Repo> = repos
466 .into_iter()
467 .filter(|repo| repo.fork_of.is_none() && repo.namespace.eq_ignore_ascii_case(workspace))
468 .filter(|repo| before.is_some_and(|row| row.targets(repo)) || after.is_some_and(|row| row.targets(repo)))
469 .collect();
470 let now = &reached[..reached.len().min(SYNCED_AT_ONCE)];
471 for repo in now {
472 self.sync(repo, &ws).await?;
473 }
474 if !listed_all || reached.len() > now.len() {
475 let done: Vec<&str> = now.iter().map(|repo| repo.id.as_str()).collect();
476 self.db
477 .prepare(
478 "DELETE FROM synced WHERE repo_id IN (SELECT DISTINCT repo_id FROM workflows WHERE substr(repo, 1, length(?1) + 1) = ?1 || '/')
479 AND repo_id NOT IN (SELECT value FROM json_each(?2))",
480 )
481 .bind(&[workspace.into(), serde_json::to_string(&done)?.into()])?
482 .run()
483 .await?;
484 }
485 Ok(())
486 }
487
488 /// Every minute: syncs a few repositories whose workflows are listed
489 /// but not synced since a workspace's managed workflows changed.
490 pub(crate) async fn catch_up_syncs(&self) -> Result<()> {
491 #[derive(Deserialize)]
492 struct Id {
493 repo_id: String,
494 }
495 let ids = self
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.496 .db
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.497 .prepare("SELECT DISTINCT repo_id FROM workflows WHERE repo_id NOT IN (SELECT repo_id FROM synced) LIMIT ?")
498 .bind(&[CAUGHT_UP_PER_MINUTE.into()])?
499 .all()
500 .await?
501 .results::<Id>()?;
502 for Id { repo_id } in ids {
503 match self.repo_by_id(&repo_id).await? {
504 Some((repo, ws)) => self.sync(&repo, &ws).await?,
505 // Gone: nothing to sync, so it is not looked for again.
506 None => {
507 self.db
508 .prepare("INSERT OR REPLACE INTO synced (repo_id, at) VALUES (?, ?)")
509 .bind(&[repo_id.as_str().into(), rfc3339(now_ms()).into()])?
510 .run()
511 .await?;
512 }
513 }
514 }
515 Ok(())
516 }
517
518 /// The workflows `repo` runs: its own `files` and the managed workflows
519 /// installed in it or its workspace's that target it, as
520 /// `g1t_actions::managed::arrange` puts them together. A required
521 /// workflow of the workspace's sets aside a file of its name.
522 pub(crate) async fn with_managed(&self, repo: &Repo, files: Vec<WorkflowFile>) -> Result<Vec<WorkflowFile>> {
523 let installed = self
524 .db
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.525 .prepare("SELECT name, source FROM managed_workflows WHERE repo_id = ? ORDER BY name")
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.526 .bind(&[repo.id.as_str().into()])?
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.527 .all()
528 .await?
529 .results::<Row>()?;
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.530 let workspace: Vec<WorkspaceRow> = self.workspace_rows(&repo.namespace).await?.into_iter().filter(|row| row.targets(repo)).collect();
531 if installed.is_empty() && workspace.is_empty() {
532 return Ok(files);
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.533 }
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.534 Ok(managed::arrange(
535 files.into_iter().map(|file| (file.path, file.source)).collect(),
536 installed.into_iter().map(|row| (managed::path(&row.name), row.source)).collect(),
537 workspace.into_iter().map(|row| (managed::path(&row.name), row.source.clone(), row.mode())).collect(),
538 )
539 .into_iter()
540 .map(|Arranged { path, source, required }| WorkflowFile { path, source, required })
541 .collect())
542 }
543
544 /// Whether `path` in `repo` is a required workflow of its workspace's,
545 /// which no one in the repository can turn off.
546 pub(crate) async fn required_here(&self, repo: &Repo, path: &str) -> Result<bool> {
547 let Some(name) = path.strip_prefix("managed/").and_then(|rest| rest.strip_suffix(".yml")) else {
548 return Ok(false);
549 };
550 Ok(self.workspace_row(&repo.namespace, name).await?.is_some_and(|row| row.mode() == Mode::Required && row.targets(repo)))
551 }
552
553 /// What installed the managed workflow at `path` in a repository of
554 /// `workspace`, whose synced copy is `source`: the workspace, when the
555 /// copy is its; else g1t.
556 pub(crate) async fn managed_by(&self, workspace: &str, path: &str, source: &str) -> Result<Option<ManagedBy>> {
557 let Some(name) = path.strip_prefix("managed/").and_then(|rest| rest.strip_suffix(".yml")) else {
558 return Ok(None);
559 };
560 Ok(Some(match self.workspace_row(workspace, name).await?.filter(|row| row.source == source) {
561 Some(row) => ManagedBy { origin: workspace.to_lowercase(), mode: row.mode().as_str().to_owned(), source: Some(row.source_ref) },
562 None => ManagedBy { origin: "g1t".to_owned(), mode: String::new(), source: None },
563 }))
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.564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 #[test]
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.572 fn a_source_is_a_workflow_file_in_the_workspace() {
573 let source = parse_source("acme/policies/.g1t/workflows/scan.yml@main", "acme").unwrap();
574 assert_eq!((source.repo.as_str(), source.path.as_str(), source.git_ref.as_str()), ("policies", ".g1t/workflows/scan.yml", "main"));
575 assert!(parse_source("ACME/policies/.github/workflows/scan.yaml@v1", "acme").is_ok());
576 assert!(parse_source("other/policies/.g1t/workflows/scan.yml@main", "acme").unwrap_err().contains("acme's own"));
577 for bad in ["acme/policies@main", "acme/policies/README.md@main", "acme/policies/.g1t/workflows/scan.yml", "./.g1t/workflows/scan.yml"] {
578 assert!(parse_source(bad, "acme").is_err(), "{bad}");
579 }
580 }
581
582 #[test]
583 fn a_private_source_runs_in_private_repositories_only() {
584 assert!(may_reach("acme/p/.g1t/workflows/a.yml@main", false, "all").is_ok());
585 assert!(may_reach("acme/p/.g1t/workflows/a.yml@main", true, "private").is_ok());
586 assert!(may_reach("acme/p/.g1t/workflows/a.yml@main", true, "all").is_err());
587 assert!(may_reach("acme/p/.g1t/workflows/a.yml@main", true, "public").is_err());
588 }
589
590 #[test]
591 fn patterns_are_names_without_repeats() {
592 assert_eq!(clean_patterns(&[" web ".into(), "".into(), "WEB".into(), "api-*".into()]).unwrap(), ["web", "api-*"]);
593 assert!(clean_patterns(&["acme/web".into()]).is_err());
594 assert!(clean_patterns(&(0..=MAX_PATTERNS).map(|n| format!("r{n}")).collect::<Vec<_>>()).is_err());
595 }
596
597 fn row(mode: &str, repositories: &str, visibility: &str, private: bool) -> WorkspaceRow {
598 WorkspaceRow {
599 name: "scan".into(),
600 source_ref: "acme/policies/.g1t/workflows/scan.yml@main".into(),
601 source_repo_id: "rep_1".into(),
602 source_sha: None,
603 source_private: u32::from(private),
604 source: String::new(),
605 workflow_name: "Scan".into(),
606 mode: mode.into(),
607 repositories: repositories.into(),
608 visibility: visibility.into(),
609 created_at: String::new(),
610 updated_at: String::new(),
611 updated_by: String::new(),
612 }
613 }
614
615 fn repo(name: &str, private: bool) -> Repo {
616 Repo {
617 id: "rep_2".into(),
618 namespace: "acme".into(),
619 name: name.into(),
620 description: None,
621 is_private: private,
622 owner_id: String::new(),
623 default_branch: "main".into(),
624 fork_of: None,
625 protected: false,
626 created_at: String::new(),
627 topics: Vec::new(),
628 website: None,
629 archived_at: None,
630 mirror: None,
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.631 }
632 }
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.633
634 #[test]
635 fn a_row_targets_by_its_patterns_and_visibility() {
636 let all = row("required", "[]", "all", false);
637 assert!(all.targets(&repo("web", false)) && all.targets(&repo("api", true)));
638 assert_eq!(all.mode(), Mode::Required);
639 let some = row("default", r#"["web","api-*"]"#, "all", false);
640 assert!(some.targets(&repo("api-1", false)) && !some.targets(&repo("docs", false)));
641 assert_eq!(some.view().repositories, ["web", "api-*"]);
642 // From a private repository: never a public one.
643 let private = row("required", "[]", "all", true);
644 assert!(private.targets(&repo("web", true)) && !private.targets(&repo("site", false)));
645 assert_eq!(row("whatever", "[]", "all", false).mode(), Mode::Default);
646 }
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.647}

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