Skip to content
123 linesCodeBlameRaw
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}