Skip to content
152 linesCodeBlameRaw

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.

Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.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}

This file's history is long; its oldest lines are credited to the oldest commit read.