Skip to content
123 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 branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.1//! Managed workflows (`g1t_actions::managed`): workflows g1t's services
2//! install into a repository without committing them. They are kept here,
3//! read beside the repository's own files wherever its workflows are read
4//! (`sync.rs` `read_for`), and so listed, started, run and re-run as any
5//! other workflow is.
6
7use g1t_actions::managed;
8use g1t_actions::workflow;
9use g1t_contracts::actions::{RemoveManagedWorkflowArgs, SetManagedDeployArgs, SetManagedWorkflowArgs};
10use g1t_contracts::time::rfc3339;
11use g1t_contracts::{FailureCode, Outcome};
12use g1t_kit::now_ms;
13use serde::Deserialize;
14use worker::Result;
15
16use crate::sync::WorkflowFile;
17use crate::{Actions, fail};
18
19/// Whether `name` can end a managed workflow's path.
20fn valid_name(name: &str) -> bool {
21 !name.is_empty() && name.len() <= 64 && name.bytes().all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
22}
23
24#[derive(Deserialize)]
25struct Row {
26 name: String,
27 source: String,
28}
29
30impl Actions {
31 /// `set_managed_workflow`.
32 pub async fn set_managed_workflow(&self, a: SetManagedWorkflowArgs) -> Result<Outcome<bool>> {
33 if !valid_name(&a.name) {
34 return Ok(fail(FailureCode::Invalid, "A managed workflow's name is lowercase letters, digits and hyphens."));
35 }
36 if let Err(problem) = workflow::parse(&a.source) {
37 return Ok(fail(FailureCode::Invalid, format!("The managed workflow does not read as a workflow: {problem}")));
38 }
39 let Some((repo, ws)) = self.repo_by_id(&a.repo_id).await? else {
40 return Ok(fail(FailureCode::NotFound, "No such repository."));
41 };
42 self.db
43 .prepare(
44 "INSERT INTO managed_workflows (repo_id, name, source, origin, updated_at) VALUES (?, ?, ?, ?, ?)
45 ON CONFLICT (repo_id, name) DO UPDATE SET source = excluded.source, origin = excluded.origin, updated_at = excluded.updated_at",
46 )
47 .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()])?
48 .run()
49 .await?;
50 // Listed, and on schedule, at once.
51 self.sync(&repo, &ws).await?;
52 Ok(Outcome::Ok(true))
53 }
54
55 /// `remove_managed_workflow`.
56 pub async fn remove_managed_workflow(&self, a: RemoveManagedWorkflowArgs) -> Result<Outcome<bool>> {
57 let removed = self
58 .db
59 .prepare("DELETE FROM managed_workflows WHERE repo_id = ? AND name = ? RETURNING name")
60 .bind(&[a.repo_id.as_str().into(), a.name.as_str().into()])?
61 .first::<serde_json::Value>(None)
62 .await?
63 .is_some();
64 if removed && let Some((repo, ws)) = self.repo_by_id(&a.repo_id).await? {
65 self.sync(&repo, &ws).await?;
66 }
67 Ok(Outcome::Ok(removed))
68 }
69
70 /// `set_managed_deploy`.
71 pub async fn set_managed_deploy(&self, a: SetManagedDeployArgs) -> Result<Outcome<bool>> {
72 let name = format!("deploy-{}", a.project);
73 let settings = managed::DeploySettings {
74 project: a.project,
75 production_branch: a.production_branch,
76 root_dir: a.root_dir,
77 build_command: a.build_command,
78 output_dir: a.output_dir,
79 previews: a.previews,
80 production: a.production,
81 };
82 match managed::deploy_workflow(&settings) {
83 Some(source) => self.set_managed_workflow(SetManagedWorkflowArgs { repo_id: a.repo_id, name, source, origin: "deployments".to_owned() }).await,
84 None => {
85 self.remove_managed_workflow(RemoveManagedWorkflowArgs { repo_id: a.repo_id, name }).await?;
86 Ok(Outcome::Ok(false))
87 }
88 }
89 }
90
91 /// The managed workflows that run beside `files`, a repository's own:
92 /// each one no file of the same `name:` takes the place of.
93 pub(crate) async fn managed_beside(&self, repo_id: &str, files: &[WorkflowFile]) -> Result<Vec<WorkflowFile>> {
94 let rows = self
95 .db
96 .prepare("SELECT name, source FROM managed_workflows WHERE repo_id = ? ORDER BY name")
97 .bind(&[repo_id.into()])?
98 .all()
99 .await?
100 .results::<Row>()?;
101 if rows.is_empty() {
102 return Ok(Vec::new());
103 }
104 let installed = rows.into_iter().map(|row| (managed::path(&row.name), row.source)).collect();
105 Ok(managed::prefer_repository(files.iter().map(|file| (file.path.as_str(), file.source.as_str())), installed)
106 .into_iter()
107 .map(|(path, source)| WorkflowFile { path, source })
108 .collect())
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn managed_workflows_are_named_as_files() {
118 assert!(valid_name("deploy") && valid_name("code-scanning-2"));
119 for bad in ["", "Deploy", "a/b", "a.yml", "../x", &"a".repeat(65)] {
120 assert!(!valid_name(bad), "{bad}");
121 }
122 }
123}

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