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: workflows g1t (or a workspace) installs into a |
| 2 | //! repository without committing them. One runs like any workflow in | |
| 3 | //! `.g1t/workflows/` (the same runs, logs, re-runs and checks), from a | |
| 4 | //! source kept beside the repository rather than in it, under a path in | |
| 5 | //! [`FOLDER`] that no file can have. | |
| 6 | //! | |
| 7 | //! A repository takes one over by having a workflow of the same `name:` in | |
| 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. | 8 | //! `.g1t/workflows/`: that file runs instead ([`arrange`]). So moving a |
| 9 | //! managed workflow into the repository is committing its source. A | |
| 10 | //! workspace's managed workflows ([`Target`], [`Mode`]) are the exception | |
| 11 | //! when they are required: then the repository's file of that name is set | |
| 12 | //! aside instead, and no repository can turn the workflow off. | |
| 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. | 13 | //! |
| 14 | //! [`deploy_workflow`] writes the one deployments install: it calls g1t's | |
| 15 | //! own reusable deploy workflow with the project's settings as its inputs. | |
| 16 | ||
| 17 | use crate::workflow; | |
| 18 | ||
| 19 | /// Where managed workflows' paths start. Not a folder in any repository: | |
| 20 | /// workflow files are read from `.g1t/workflows/` and `.github/workflows/`. | |
| 21 | pub const FOLDER: &str = "managed"; | |
| 22 | ||
| 23 | /// The `name:` of the workflow deployments install, and of its calling | |
| 24 | /// job, so the called job's check is `g1t / deploy`, the name a g1t.page | |
| 25 | /// build's status has always had. | |
| 26 | pub const DEPLOY_NAME: &str = "g1t"; | |
| 27 | ||
| 28 | /// The reusable workflow the deploy workflow calls. | |
| 29 | pub const DEPLOY_CALLS: &str = "g1t/workflows/.g1t/workflows/deploy.yml@v1"; | |
| 30 | ||
| 31 | /// The path a managed workflow called `name` runs under. | |
| 32 | pub fn path(name: &str) -> String { | |
| 33 | format!("{FOLDER}/{name}.yml") | |
| 34 | } | |
| 35 | ||
| 36 | /// Whether `path` is a managed workflow's. | |
| 37 | pub fn is_managed(path: &str) -> bool { | |
| 38 | path.strip_prefix(FOLDER).is_some_and(|rest| rest.starts_with('/')) | |
| 39 | } | |
| 40 | ||
| 41 | /// A project's deployment settings, as the deploy workflow's triggers and | |
| 42 | /// inputs. | |
| 43 | #[derive(Clone, Debug, Default, PartialEq, Eq)] | |
| 44 | pub struct DeploySettings { | |
| 45 | /// The project's slug, which names its app. | |
| 46 | pub project: String, | |
| 47 | /// The branch production builds from: the repository's default branch. | |
| 48 | pub production_branch: String, | |
| 49 | /// The project's folder in the repository; empty or `.` for the root. | |
| 50 | pub root_dir: String, | |
| 51 | /// Empty: what deployments detect. | |
| 52 | pub build_command: String, | |
| 53 | /// Empty: what deployments detect. | |
| 54 | pub output_dir: String, | |
| 55 | /// Whether pull requests get previews. | |
| 56 | pub previews: bool, | |
| 57 | /// Whether pushes to the production branch deploy. | |
| 58 | pub production: bool, | |
| 59 | } | |
| 60 | ||
| 61 | /// A string as YAML reads it back unchanged: JSON's quoting is YAML's. | |
| 62 | fn quoted(text: &str) -> String { | |
| 63 | serde_json::to_string(text).unwrap_or_else(|_| "\"\"".to_owned()) | |
| 64 | } | |
| 65 | ||
| 66 | /// The workflow deployments install for a project, or None when it neither | |
| 67 | /// previews nor deploys production, so nothing would start it. | |
| 68 | pub fn deploy_workflow(settings: &DeploySettings) -> Option<String> { | |
| 69 | if !settings.previews && !settings.production { | |
| 70 | return None; | |
| 71 | } | |
| 72 | let mut on = String::new(); | |
| 73 | if settings.production { | |
| 74 | on.push_str(&format!(" push:\n branches: [{}]\n", quoted(&settings.production_branch))); | |
| 75 | } | |
| 76 | if settings.previews { | |
| 77 | // Previews come down on the server when their pull request closes. | |
| 78 | on.push_str(" pull_request:\n types: [opened, synchronize, reopened, ready_for_review]\n"); | |
| 79 | } | |
| 80 | let root_dir = if settings.root_dir.trim().is_empty() { "." } else { settings.root_dir.trim() }; | |
| 81 | Some(format!( | |
| 82 | "# Managed by g1t, from the deployment settings of the project {project}.\n\ | |
| 83 | # It is not in the repository. A workflow named \"{name}\" in .g1t/workflows/\n\ | |
| 84 | # runs in its place.\n\ | |
| 85 | name: {name}\n\ | |
| 86 | on:\n\ | |
| 87 | {on}\ | |
| 88 | # A newer push replaces a build still running for the same branch or\n\ | |
| 89 | # pull request.\n\ | |
| 90 | concurrency:\n \ | |
| 91 | group: ${{{{ github.workflow }}}}-{project_key}-${{{{ github.event.pull_request.number || github.ref }}}}\n \ | |
| 92 | cancel-in-progress: true\n\ | |
| 93 | permissions:\n \ | |
| 94 | contents: read\n \ | |
| 95 | deployments: write\n \ | |
| 96 | id-token: write\n\ | |
| 97 | jobs:\n \ | |
| 98 | {name}:\n \ | |
| 99 | name: {name}\n \ | |
| 100 | uses: {calls}\n \ | |
| 101 | with:\n \ | |
| 102 | project: {project}\n \ | |
| 103 | root-dir: {root_dir}\n \ | |
| 104 | build-command: {build_command}\n \ | |
| 105 | output-dir: {output_dir}\n \ | |
| 106 | secrets: inherit\n", | |
| 107 | name = DEPLOY_NAME, | |
| 108 | calls = DEPLOY_CALLS, | |
| 109 | on = on, | |
| 110 | project = quoted(&settings.project), | |
| 111 | project_key = settings.project.chars().filter(|c| c.is_ascii_alphanumeric() || *c == '-').collect::<String>(), | |
| 112 | root_dir = quoted(root_dir), | |
| 113 | build_command = quoted(settings.build_command.trim()), | |
| 114 | output_dir = quoted(settings.output_dir.trim()), | |
| 115 | )) | |
| 116 | } | |
| 117 | ||
| 118 | /// A workflow's name, as it would be listed: its `name:`, else its path. | |
| 119 | fn listed_name(path: &str, source: &str) -> String { | |
| 120 | workflow::parse(source).map(|parsed| parsed.display_name(path)).unwrap_or_else(|_| path.to_owned()) | |
| 121 | } | |
| 122 | ||
| 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. | 123 | /// Whether `name` can end a managed workflow's path: lowercase letters, |
| 124 | /// digits and hyphens, at most 64. | |
| 125 | pub fn valid_name(name: &str) -> bool { | |
| 126 | !name.is_empty() && name.len() <= 64 && name.bytes().all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') | |
| 127 | } | |
| 128 | ||
| 129 | /// How a workspace's managed workflow holds in its repositories. | |
| 130 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 131 | pub enum Mode { | |
| 132 | /// It runs in every repository it targets: a workflow of the same | |
| 133 | /// `name:` in the repository is set aside, and no repository can turn | |
| 134 | /// it off, so its check can be required. | |
| 135 | Required, | |
| 136 | /// It runs where the repository has no workflow of the same `name:`, | |
| 137 | /// and the repository's admins can turn it off. | |
| 138 | Default, | |
| 139 | } | |
| 140 | ||
| 141 | impl Mode { | |
| 142 | pub fn parse(text: &str) -> Option<Mode> { | |
| 143 | match text.trim().to_ascii_lowercase().as_str() { | |
| 144 | "required" => Some(Mode::Required), | |
| 145 | "default" => Some(Mode::Default), | |
| 146 | _ => None, | |
| 147 | } | |
| 148 | } | |
| 149 | ||
| 150 | pub fn as_str(self) -> &'static str { | |
| 151 | match self { | |
| 152 | Mode::Required => "required", | |
| 153 | Mode::Default => "default", | |
| 154 | } | |
| 155 | } | |
| 156 | } | |
| 157 | ||
| 158 | /// The visibilities a target can name. | |
| 159 | pub const VISIBILITIES: [&str; 3] = ["all", "public", "private"]; | |
| 160 | ||
| 161 | /// Which of a workspace's repositories one of its managed workflows runs in. | |
| 162 | #[derive(Clone, Debug, Default, PartialEq, Eq)] | |
| 163 | pub struct Target { | |
| 164 | /// Repository names and patterns (`*`, `[a-z]`; `!` excludes what an | |
| 165 | /// earlier one included), read in order, the last match deciding, | |
| 166 | /// without regard to case. Empty is every repository; only exclusions | |
| 167 | /// is every repository but those. | |
| 168 | pub repositories: Vec<String>, | |
| 169 | /// One of [`VISIBILITIES`]. | |
| 170 | pub visibility: String, | |
| 171 | } | |
| 172 | ||
| 173 | impl Target { | |
| 174 | /// Whether it takes in the repository `name`, private or not. A | |
| 175 | /// workflow read from a private repository (`source_private`) never | |
| 176 | /// runs in a public one, whose workflow page and logs anyone can read. | |
| 177 | pub fn includes(&self, name: &str, private: bool, source_private: bool) -> bool { | |
| 178 | if source_private && !private { | |
| 179 | return false; | |
| 180 | } | |
| 181 | match self.visibility.as_str() { | |
| 182 | "public" if private => return false, | |
| 183 | "private" if !private => return false, | |
| 184 | _ => {} | |
| 185 | } | |
| 186 | if self.repositories.is_empty() { | |
| 187 | return true; | |
| 188 | } | |
| 189 | let lowered: Vec<String> = self.repositories.iter().map(|pattern| pattern.trim().to_ascii_lowercase()).collect(); | |
| 190 | let mut patterns = crate::filter::Patterns::new(&lowered); | |
| 191 | if patterns.0.iter().all(|pattern| pattern.negated) { | |
| 192 | patterns.0.insert(0, crate::filter::Pattern::parse("*")); | |
| 193 | } | |
| 194 | patterns.includes(&name.to_ascii_lowercase()) | |
| 195 | } | |
| 196 | } | |
| 197 | ||
| 198 | /// A workflow a repository runs, as [`arrange`] decides. | |
| 199 | #[derive(Clone, Debug, PartialEq, Eq)] | |
| 200 | pub struct Arranged { | |
| 201 | pub path: String, | |
| 202 | pub source: String, | |
| 203 | /// A workspace's required workflow: nothing turns it off. | |
| 204 | pub required: bool, | |
| 205 | } | |
| 206 | ||
| 207 | /// The workflows a repository runs: its own `files`, and the managed | |
| 208 | /// workflows offered to it, those `installed` in it by g1t's services (such | |
| 209 | /// as a project's deploy workflow) and its workspace's (`workspace`, each | |
| 210 | /// with its [`Mode`]), all as `(path, source)`. | |
| 211 | /// | |
| 212 | /// - The workspace's required workflows run, and a file of the same | |
| 213 | /// `name:` is set aside, so nothing else reports their checks. | |
| 214 | /// - Then the repository's own files. | |
| 215 | /// - Then each other managed workflow that no workflow before it of the | |
| 216 | /// same `name:` or path takes the place of: those installed in the | |
| 217 | /// repository first, then the workspace's defaults. | |
| 218 | pub fn arrange(files: Vec<(String, String)>, installed: Vec<(String, String)>, workspace: Vec<(String, String, Mode)>) -> Vec<Arranged> { | |
| 219 | let mut names: Vec<String> = Vec::new(); | |
| 220 | let mut paths: Vec<String> = Vec::new(); | |
| 221 | let mut out: Vec<Arranged> = Vec::new(); | |
| 222 | let (required, defaults): (Vec<_>, Vec<_>) = workspace.into_iter().partition(|(_, _, mode)| *mode == Mode::Required); | |
| 223 | let offer = |path: String, source: String, required: bool, out: &mut Vec<Arranged>, names: &mut Vec<String>, paths: &mut Vec<String>| { | |
| 224 | let name = listed_name(&path, &source); | |
| 225 | if names.contains(&name) || paths.contains(&path) { | |
| 226 | return; | |
| 227 | } | |
| 228 | names.push(name); | |
| 229 | paths.push(path.clone()); | |
| 230 | out.push(Arranged { path, source, required }); | |
| 231 | }; | |
| 232 | for (path, source, _) in required { | |
| 233 | offer(path, source, true, &mut out, &mut names, &mut paths); | |
| 234 | } | |
| 235 | // A repository's files are set aside only for a required workflow's | |
| 236 | // name, never for each other's. | |
| 237 | let required_names = names.clone(); | |
| 238 | for (path, source) in files { | |
| 239 | let name = listed_name(&path, &source); | |
| 240 | if required_names.contains(&name) { | |
| 241 | continue; | |
| 242 | } | |
| 243 | names.push(name); | |
| 244 | paths.push(path.clone()); | |
| 245 | out.push(Arranged { path, source, required: false }); | |
| 246 | } | |
| 247 | for (path, source) in installed.into_iter().chain(defaults.into_iter().map(|(path, source, _)| (path, source))) { | |
| 248 | offer(path, source, false, &mut out, &mut names, &mut paths); | |
| 249 | } | |
| 250 | out | |
| 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. | 251 | } |
| 252 | ||
| 253 | #[cfg(test)] | |
| 254 | mod tests { | |
| 255 | use super::*; | |
| 256 | use crate::workflow::Severity; | |
| 257 | ||
| 258 | fn settings() -> DeploySettings { | |
| 259 | DeploySettings { | |
| 260 | project: "web".into(), | |
| 261 | production_branch: "main".into(), | |
| 262 | root_dir: "apps/web".into(), | |
| 263 | build_command: String::new(), | |
| 264 | output_dir: "dist".into(), | |
| 265 | previews: true, | |
| 266 | production: true, | |
| 267 | } | |
| 268 | } | |
| 269 | ||
| 270 | #[test] | |
| 271 | fn the_deploy_workflow_is_a_workflow_g1t_runs_as_it_is() { | |
| 272 | let source = deploy_workflow(&settings()).unwrap(); | |
| 273 | let parsed = workflow::parse(&source).unwrap(); | |
| 274 | assert_eq!(parsed.name.as_deref(), Some("g1t")); | |
| 275 | assert!(parsed.trigger("push").is_some() && parsed.trigger("pull_request").is_some()); | |
| 276 | assert!(!parsed.notes.iter().any(|note| note.severity != Severity::Info), "{:?}", parsed.notes); | |
| 277 | let job = &parsed.jobs[0]; | |
| 278 | assert_eq!(job.id, "g1t"); | |
| 279 | assert!(source.contains("uses: g1t/workflows/.g1t/workflows/deploy.yml@v1")); | |
| 280 | assert!(source.contains("root-dir: \"apps/web\"") && source.contains("output-dir: \"dist\"") && source.contains("build-command: \"\"")); | |
| 281 | assert!(source.contains("branches: [\"main\"]")); | |
| 282 | } | |
| 283 | ||
| 284 | #[test] | |
| 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. | 285 | fn the_called_workflow_takes_every_input_the_deploy_workflow_gives() { |
| 286 | let called = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../../workflows/.g1t/workflows/deploy.yml")).unwrap(); | |
| 287 | let parsed = workflow::parse(&called).unwrap(); | |
| 288 | assert!(!parsed.notes.iter().any(|note| note.severity != Severity::Info), "{:?}", parsed.notes); | |
| 289 | let raw: serde_json::Value = serde_yaml::from_str(&called).unwrap(); | |
| 290 | let inputs = &raw["on"]["workflow_call"]["inputs"]; | |
| 291 | let caller = workflow::parse(&deploy_workflow(&settings()).unwrap()).unwrap(); | |
| 292 | for (key, _) in caller.jobs[0].raw["with"].as_object().unwrap() { | |
| 293 | assert!(inputs.get(key).is_some(), "the called workflow takes no `{key}`"); | |
| 294 | } | |
| 295 | // Its job is `deploy`, so the caller's `g1t` reports `g1t / deploy`. | |
| 296 | assert_eq!(parsed.jobs[0].raw["name"], "deploy"); | |
| 297 | assert!(DEPLOY_CALLS.starts_with("g1t/workflows/.g1t/workflows/deploy.yml@")); | |
| 298 | } | |
| 299 | ||
| 300 | #[test] | |
| 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. | 301 | fn what_a_project_turns_off_starts_nothing() { |
| 302 | let previews_only = deploy_workflow(&DeploySettings { production: false, ..settings() }).unwrap(); | |
| 303 | let parsed = workflow::parse(&previews_only).unwrap(); | |
| 304 | assert!(parsed.trigger("push").is_none() && parsed.trigger("pull_request").is_some()); | |
| 305 | let production_only = deploy_workflow(&DeploySettings { previews: false, ..settings() }).unwrap(); | |
| 306 | assert!(workflow::parse(&production_only).unwrap().trigger("pull_request").is_none()); | |
| 307 | assert_eq!(deploy_workflow(&DeploySettings { previews: false, production: false, ..settings() }), None); | |
| 308 | } | |
| 309 | ||
| 310 | #[test] | |
| 311 | fn settings_are_quoted_so_they_stay_text() { | |
| 312 | let tricky = DeploySettings { build_command: "npm run build -- --base \"/x\": yes".into(), root_dir: String::new(), ..settings() }; | |
| 313 | let source = deploy_workflow(&tricky).unwrap(); | |
| 314 | let parsed = workflow::parse(&source).unwrap(); | |
| 315 | let with = &parsed.jobs[0].raw["with"]; | |
| 316 | assert_eq!(with["build-command"], "npm run build -- --base \"/x\": yes"); | |
| 317 | assert_eq!(with["root-dir"], "."); | |
| 318 | } | |
| 319 | ||
| 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. | 320 | fn own(path: &str, name: &str) -> (String, String) { |
| 321 | (path.to_owned(), format!("name: {name}\non: push\njobs:\n a:\n runs-on: ubuntu-latest\n steps: [{{ run: 'true' }}]\n")) | |
| 322 | } | |
| 323 | ||
| 324 | fn paths(arranged: &[Arranged]) -> Vec<&str> { | |
| 325 | arranged.iter().map(|kept| kept.path.as_str()).collect() | |
| 326 | } | |
| 327 | ||
| 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. | 328 | #[test] |
| 329 | fn a_repository_workflow_of_the_same_name_takes_its_place() { | |
| 330 | let deploy = (path("deploy"), deploy_workflow(&settings()).unwrap()); | |
| 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. | 331 | let other = own(&path("scan"), "Scan"); |
| 332 | let kept = arrange(vec![own(".g1t/workflows/g1t.yml", "g1t")], vec![deploy.clone(), other.clone()], vec![]); | |
| 333 | assert_eq!(paths(&kept), [".g1t/workflows/g1t.yml", "managed/scan.yml"]); | |
| 334 | let kept = arrange(vec![own(".g1t/workflows/ci.yml", "CI")], vec![deploy.clone(), other], vec![]); | |
| 335 | assert_eq!(kept.len(), 3); | |
| 336 | assert!(kept.iter().all(|kept| !kept.required)); | |
| 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. | 337 | assert!(is_managed(&deploy.0) && !is_managed(".g1t/workflows/g1t.yml") && !is_managed("managedx/a.yml")); |
| 338 | } | |
| 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. | 339 | |
| 340 | #[test] | |
| 341 | fn a_required_workflow_sets_aside_a_file_of_its_name() { | |
| 342 | let files = vec![own(".g1t/workflows/scan.yml", "Scan"), own(".g1t/workflows/ci.yml", "CI")]; | |
| 343 | let kept = arrange(files, vec![], vec![(path("scan"), own("", "Scan").1, Mode::Required)]); | |
| 344 | assert_eq!(paths(&kept), ["managed/scan.yml", ".g1t/workflows/ci.yml"]); | |
| 345 | assert!(kept[0].required && !kept[1].required); | |
| 346 | } | |
| 347 | ||
| 348 | #[test] | |
| 349 | fn a_default_workflow_gives_way_to_a_file_of_its_name() { | |
| 350 | let files = vec![own(".g1t/workflows/lint.yml", "Lint")]; | |
| 351 | let workspace = vec![(path("lint"), own("", "Lint").1, Mode::Default), (path("audit"), own("", "Audit").1, Mode::Default)]; | |
| 352 | let kept = arrange(files, vec![], workspace); | |
| 353 | assert_eq!(paths(&kept), [".g1t/workflows/lint.yml", "managed/audit.yml"]); | |
| 354 | assert!(kept.iter().all(|kept| !kept.required)); | |
| 355 | } | |
| 356 | ||
| 357 | #[test] | |
| 358 | fn a_repository_keeps_two_files_of_one_name() { | |
| 359 | let files = vec![own(".g1t/workflows/a.yml", "Same"), own(".g1t/workflows/b.yml", "Same")]; | |
| 360 | assert_eq!(arrange(files, vec![], vec![]).len(), 2); | |
| 361 | } | |
| 362 | ||
| 363 | #[test] | |
| 364 | fn what_is_installed_in_the_repository_comes_between_required_and_default() { | |
| 365 | // The same path: required beats installed, installed beats default. | |
| 366 | let installed = vec![own(&path("deploy-web"), "Deploy")]; | |
| 367 | let kept = arrange(vec![], installed.clone(), vec![(path("deploy-web"), own("", "Policy").1, Mode::Required)]); | |
| 368 | assert_eq!(kept.len(), 1); | |
| 369 | assert!(kept[0].source.contains("Policy") && kept[0].required); | |
| 370 | let kept = arrange(vec![], installed, vec![(path("deploy-web"), own("", "Policy").1, Mode::Default)]); | |
| 371 | assert_eq!(kept.len(), 1); | |
| 372 | assert!(kept[0].source.contains("Deploy") && !kept[0].required); | |
| 373 | // The same name at another path: the first offered stays. | |
| 374 | let kept = arrange(vec![], vec![own(&path("a"), "Same")], vec![(path("b"), own("", "Same").1, Mode::Default)]); | |
| 375 | assert_eq!(paths(&kept), ["managed/a.yml"]); | |
| 376 | } | |
| 377 | ||
| 378 | #[test] | |
| 379 | fn targets_take_names_patterns_and_visibility() { | |
| 380 | let all = Target { repositories: vec![], visibility: "all".into() }; | |
| 381 | assert!(all.includes("web", true, false) && all.includes("web", false, false)); | |
| 382 | let some = Target { repositories: vec!["web".into(), "api-*".into()], visibility: "all".into() }; | |
| 383 | assert!(some.includes("web", false, false) && some.includes("API-gateway", false, false)); | |
| 384 | assert!(!some.includes("docs", false, false) && !some.includes("webx", false, false)); | |
| 385 | let but = Target { repositories: vec!["!sandbox-*".into()], visibility: "all".into() }; | |
| 386 | assert!(but.includes("web", false, false) && !but.includes("sandbox-1", false, false)); | |
| 387 | let then_back = Target { repositories: vec!["*".into(), "!sandbox-*".into(), "sandbox-keep".into()], visibility: "all".into() }; | |
| 388 | assert!(then_back.includes("sandbox-keep", false, false) && !then_back.includes("sandbox-2", false, false)); | |
| 389 | let private = Target { repositories: vec![], visibility: "private".into() }; | |
| 390 | assert!(private.includes("web", true, false) && !private.includes("web", false, false)); | |
| 391 | let public = Target { repositories: vec![], visibility: "public".into() }; | |
| 392 | assert!(!public.includes("web", true, false) && public.includes("web", false, false)); | |
| 393 | } | |
| 394 | ||
| 395 | #[test] | |
| 396 | fn a_private_source_never_runs_in_a_public_repository() { | |
| 397 | let all = Target { repositories: vec!["*".into()], visibility: "all".into() }; | |
| 398 | assert!(all.includes("web", true, true)); | |
| 399 | assert!(!all.includes("site", false, true)); | |
| 400 | } | |
| 401 | ||
| 402 | #[test] | |
| 403 | fn modes_and_names_read_as_written() { | |
| 404 | assert_eq!(Mode::parse("Required"), Some(Mode::Required)); | |
| 405 | assert_eq!(Mode::parse("default").map(Mode::as_str), Some("default")); | |
| 406 | assert_eq!(Mode::parse("optional"), None); | |
| 407 | assert!(valid_name("deploy") && valid_name("code-scanning-2")); | |
| 408 | for bad in ["", "Deploy", "a/b", "a.yml", "../x", &"a".repeat(65)] { | |
| 409 | assert!(!valid_name(bad), "{bad}"); | |
| 410 | } | |
| 411 | } | |
| 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. | 412 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.