Skip to content
152 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 {
73 json!({ "ok": true, "value": true })
74 };
75 kept.lock().unwrap().push((path, body));
76 let text = answer.to_string();
77 let _ = write!(
78 stream,
79 "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{text}",
80 text.len()
81 );
82 }
83 });
84 (base, seen)
85}
86
87fn deploy(dir: &Path, api: &str, extra: &[(&str, &str)]) -> std::process::Output {
88 let mut command = runner();
89 command
90 .arg("deploy")
91 .current_dir(dir)
92 .env_remove("GIT_REMOTE")
93 .env("G1T_API", api)
94 .env("DEPLOY_ID", "dpl_1")
95 .env("DEPLOY_TOKEN", "build-token-0123456789")
96 .env("BUILD_SECRETS", r#"{"API_KEY":"very-secret-value"}"#)
97 .env("BUILD_ENV", r#"{"PUBLIC_URL":"https://example.com"}"#);
98 for (name, value) in extra {
99 command.env(name, value);
100 }
101 command.output().unwrap()
102}
103
104#[test]
105fn deploy_builds_the_checkout_it_runs_in_without_cloning() {
106 let (api, seen) = fake_api();
107 let dir = checkout("deploy", &[("site/index.html", "<h1>hello</h1>"), ("site/about.html", "<p>about</p>"), ("README.md", "not served")]);
108 let output = deploy(&dir, &api, &[("ROOT_DIR", "site")]);
109 let stdout = String::from_utf8_lossy(&output.stdout);
110 assert!(output.status.success(), "{stdout}\n{}", String::from_utf8_lossy(&output.stderr));
111 // Its log is the step's too.
112 assert!(stdout.contains("as it is checked out"), "{stdout}");
113 assert!(stdout.contains("Uploading 2 files."), "{stdout}");
114 let seen = seen.lock().unwrap();
115 let steps: Vec<&str> = seen.iter().map(|(path, _)| path.as_str()).collect();
116 assert_eq!(steps, ["/deployments/jobs/dpl_1/started", "/deployments/jobs/dpl_1/session", "/deployments/jobs/dpl_1/finish"]);
117 for (_, body) in seen.iter() {
118 assert_eq!(body["token"], "build-token-0123456789");
119 }
120 let manifest = seen[1].1["manifest"].as_object().unwrap();
121 let mut files: Vec<&String> = manifest.keys().collect();
122 files.sort();
123 assert_eq!(files, ["/about.html", "/index.html"]);
124 let finish = &seen[2].1;
125 assert_eq!(finish["detected"], "html");
126 assert_eq!(finish["completionJwt"], "complete-me");
127 assert!(!finish["log"].as_str().unwrap().contains("build-token-0123456789"));
128}
129
130#[test]
131fn a_failed_build_is_reported_and_fails_the_step() {
132 let (api, seen) = fake_api();
133 let dir = checkout("fail", &[("README.md", "nothing to serve")]);
134 let output = deploy(&dir, &api, &[]);
135 assert_eq!(output.status.code(), Some(1));
136 let stdout = String::from_utf8_lossy(&output.stdout);
137 assert!(stdout.contains("found nothing to serve"), "{stdout}");
138 let seen = seen.lock().unwrap();
139 assert_eq!(seen.last().unwrap().0, "/deployments/jobs/dpl_1/fail");
140 // A missing root directory says so.
141 drop(seen);
142 let missing = deploy(&dir, &api, &[("ROOT_DIR", "apps/web")]);
143 assert_eq!(missing.status.code(), Some(1));
144 assert!(String::from_utf8_lossy(&missing.stdout).contains("does not exist in this checkout"));
145}
146
147#[test]
148fn deploy_needs_its_build() {
149 let output = runner().arg("deploy").env_remove("DEPLOY_ID").env_remove("DEPLOY_TOKEN").output().unwrap();
150 assert_eq!(output.status.code(), Some(2));
151 assert!(String::from_utf8_lossy(&output.stderr).contains("DEPLOY_ID"));
152}