Skip to content
191 linesCodeBlameRaw
1//! `g1t-runner deploy` and `g1t-runner detect` as a workflow step runs
2//! them: in a checkout of its own, with nothing cloned, reporting to an API
3//! (here a fake one on localhost) with the build's token.
4
5use std::io::{BufRead, BufReader, Read, Write};
6use std::net::TcpListener;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9use std::sync::{Arc, Mutex};
10
11use serde_json::{Value, json};
12
13fn runner() -> Command {
14 Command::new(env!("CARGO_BIN_EXE_g1t-runner"))
15}
16
17/// A fresh checkout holding `files`.
18fn checkout(name: &str, files: &[(&str, &str)]) -> PathBuf {
19 let dir = std::env::temp_dir().join(format!("g1t-runner-cli-{name}-{}", std::process::id()));
20 let _ = std::fs::remove_dir_all(&dir);
21 std::fs::create_dir_all(&dir).unwrap();
22 for (path, text) in files {
23 let path = dir.join(path);
24 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
25 std::fs::write(path, text).unwrap();
26 }
27 dir
28}
29
30#[test]
31fn detect_prints_what_the_build_would_find() {
32 let dir = checkout("detect", &[("site/index.html", "<h1>hello</h1>")]);
33 let output = runner().arg("detect").args(["--root-dir", "site"]).current_dir(&dir).env_remove("GIT_REMOTE").output().unwrap();
34 assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
35 assert_eq!(String::from_utf8_lossy(&output.stdout), "kind=html\ninstall=\nbuild=\noutput-dir=.\n");
36 let unknown = runner().args(["detect", "--nope"]).current_dir(&dir).output().unwrap();
37 assert_eq!(unknown.status.code(), Some(2));
38}
39
40/// The API's `/deployments/jobs/<id>/<step>`: answers each step as the
41/// deployments service does, and keeps what it was sent.
42fn fake_api() -> (String, Arc<Mutex<Vec<(String, Value)>>>) {
43 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
44 let base = format!("http://{}", listener.local_addr().unwrap());
45 let seen = Arc::new(Mutex::new(Vec::new()));
46 let kept = seen.clone();
47 std::thread::spawn(move || {
48 for stream in listener.incoming() {
49 let Ok(mut stream) = stream else { continue };
50 let mut reader = BufReader::new(stream.try_clone().unwrap());
51 let mut first = String::new();
52 reader.read_line(&mut first).unwrap();
53 let path = first.split_whitespace().nth(1).unwrap_or_default().to_owned();
54 let mut length = 0;
55 loop {
56 let mut line = String::new();
57 reader.read_line(&mut line).unwrap();
58 if line.trim().is_empty() {
59 break;
60 }
61 if let Some((name, value)) = line.split_once(':')
62 && name.eq_ignore_ascii_case("content-length")
63 {
64 length = value.trim().parse().unwrap_or(0);
65 }
66 }
67 let mut body = vec![0; length];
68 reader.read_exact(&mut body).unwrap();
69 let body: Value = serde_json::from_slice(&body).unwrap_or(Value::Null);
70 let answer = if path.ends_with("/session") {
71 json!({ "ok": true, "value": { "jwt": "complete-me", "buckets": [], "upload_url": "http://127.0.0.1:9/never" } })
72 } else if path.ends_with("/registry") {
73 // As the deployments service answers while it runs no containers.
74 json!({ "ok": false, "error": { "code": "not_enabled", "message": "Docker deployments are not on." } })
75 } else {
76 json!({ "ok": true, "value": true })
77 };
78 kept.lock().unwrap().push((path, body));
79 let text = answer.to_string();
80 let _ = write!(
81 stream,
82 "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{text}",
83 text.len()
84 );
85 }
86 });
87 (base, seen)
88}
89
90fn deploy(dir: &Path, api: &str, extra: &[(&str, &str)]) -> std::process::Output {
91 let mut command = runner();
92 command
93 .arg("deploy")
94 .current_dir(dir)
95 .env_remove("GIT_REMOTE")
96 .env("G1T_API", api)
97 .env("DEPLOY_ID", "dpl_1")
98 .env("DEPLOY_TOKEN", "build-token-0123456789")
99 .env("BUILD_SECRETS", r#"{"API_KEY":"very-secret-value"}"#)
100 .env("BUILD_ENV", r#"{"PUBLIC_URL":"https://example.com"}"#);
101 for (name, value) in extra {
102 command.env(name, value);
103 }
104 command.output().unwrap()
105}
106
107#[test]
108fn deploy_builds_the_checkout_it_runs_in_without_cloning() {
109 let (api, seen) = fake_api();
110 let dir = checkout("deploy", &[("site/index.html", "<h1>hello</h1>"), ("site/about.html", "<p>about</p>"), ("README.md", "not served")]);
111 let output = deploy(&dir, &api, &[("ROOT_DIR", "site")]);
112 let stdout = String::from_utf8_lossy(&output.stdout);
113 assert!(output.status.success(), "{stdout}\n{}", String::from_utf8_lossy(&output.stderr));
114 // Its log is the step's too.
115 assert!(stdout.contains("as it is checked out"), "{stdout}");
116 assert!(stdout.contains("Uploading 2 files."), "{stdout}");
117 let seen = seen.lock().unwrap();
118 let steps: Vec<&str> = seen.iter().map(|(path, _)| path.as_str()).collect();
119 assert_eq!(steps, ["/deployments/jobs/dpl_1/started", "/deployments/jobs/dpl_1/session", "/deployments/jobs/dpl_1/finish"]);
120 for (_, body) in seen.iter() {
121 assert_eq!(body["token"], "build-token-0123456789");
122 }
123 let manifest = seen[1].1["manifest"].as_object().unwrap();
124 let mut files: Vec<&String> = manifest.keys().collect();
125 files.sort();
126 assert_eq!(files, ["/about.html", "/index.html"]);
127 let finish = &seen[2].1;
128 assert_eq!(finish["detected"], "html");
129 assert_eq!(finish["completionJwt"], "complete-me");
130 assert!(!finish["log"].as_str().unwrap().contains("build-token-0123456789"));
131}
132
133#[test]
134fn a_failed_build_is_reported_and_fails_the_step() {
135 let (api, seen) = fake_api();
136 let dir = checkout("fail", &[("README.md", "nothing to serve")]);
137 let output = deploy(&dir, &api, &[]);
138 assert_eq!(output.status.code(), Some(1));
139 let stdout = String::from_utf8_lossy(&output.stdout);
140 assert!(stdout.contains("found nothing to serve"), "{stdout}");
141 let seen = seen.lock().unwrap();
142 assert_eq!(seen.last().unwrap().0, "/deployments/jobs/dpl_1/fail");
143 // A missing root directory says so.
144 drop(seen);
145 let missing = deploy(&dir, &api, &[("ROOT_DIR", "apps/web")]);
146 assert_eq!(missing.status.code(), Some(1));
147 assert!(String::from_utf8_lossy(&missing.stdout).contains("does not exist in this checkout"));
148}
149
150#[test]
151fn deploy_needs_its_build() {
152 let output = runner().arg("deploy").env_remove("DEPLOY_ID").env_remove("DEPLOY_TOKEN").output().unwrap();
153 assert_eq!(output.status.code(), Some(2));
154 assert!(String::from_utf8_lossy(&output.stderr).contains("DEPLOY_ID"));
155}
156
157#[test]
158fn a_container_is_built_as_its_site_while_g1t_page_runs_none() {
159 let (api, seen) = fake_api();
160 let dir = checkout("container-site", &[("index.html", "<h1>hello</h1>"), ("Dockerfile", "FROM nginx")]);
161 let output = deploy(&dir, &api, &[]);
162 let stdout = String::from_utf8_lossy(&output.stdout);
163 assert!(output.status.success(), "{stdout}
164{}", String::from_utf8_lossy(&output.stderr));
165 assert!(stdout.contains("as a plain HTML site instead"), "{stdout}");
166 let seen = seen.lock().unwrap();
167 let steps: Vec<&str> = seen.iter().map(|(path, _)| path.as_str()).collect();
168 assert_eq!(steps[1], "/deployments/jobs/dpl_1/registry");
169 assert_eq!(seen.last().unwrap().1["detected"], "html");
170 // A server with nothing else to serve fails, saying so.
171 drop(seen);
172 let server = checkout("container-only", &[("package.json", r#"{ "scripts": { "start": "node index.js" }, "dependencies": { "express": "5" } }"#)]);
173 let output = deploy(&server, &api, &[]);
174 assert_eq!(output.status.code(), Some(1));
175 assert!(String::from_utf8_lossy(&output.stdout).contains("Docker deployments are not switched on"));
176 // Build as Static site skips the Dockerfile altogether.
177 let output = deploy(&dir, &api, &[("BUILD_AS", "static")]);
178 assert!(output.status.success());
179 let detect = runner().arg("detect").current_dir(&server).env_remove("GIT_REMOTE").output().unwrap();
180 assert_eq!(
181 String::from_utf8_lossy(&detect.stdout),
182 "kind=container
183install=
184build=docker build -f Dockerfile (written by g1t) .
185output-dir=
186framework=node
187dockerfile=
188port=8080
189"
190 );
191}