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.
| 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. | 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 | ||
| 7 | use g1t_actions::managed; | |
| 8 | use g1t_actions::workflow; | |
| 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. | 9 | use g1t_contracts::actions::{RemoveManagedWorkflowArgs, SetManagedDeployArgs, SetManagedWorkflowArgs}; |
| 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. | 10 | use g1t_contracts::time::rfc3339; |
| 11 | use g1t_contracts::{FailureCode, Outcome}; | |
| 12 | use g1t_kit::now_ms; | |
| 13 | use serde::Deserialize; | |
| 14 | use worker::Result; | |
| 15 | ||
| 16 | use crate::sync::WorkflowFile; | |
| 17 | use crate::{Actions, fail}; | |
| 18 | ||
| 19 | /// Whether `name` can end a managed workflow's path. | |
| 20 | fn 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)] | |
| 25 | struct Row { | |
| 26 | name: String, | |
| 27 | source: String, | |
| 28 | } | |
| 29 | ||
| 30 | impl 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 | ||
| 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. | 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 | ||
| 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. | 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)] | |
| 113 | mod 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 | } |