|
|
@@ -26,26 +26,43 @@ const DONE_MARKER: &str = "SM121-DGPP-DONE";
|
|
|
/// are single-quoted literals. The rev marker + DONE echo run only if every
|
|
|
/// prior step succeeded (`set -e`), so a failed build leaves no marker and the
|
|
|
/// next serve retries.
|
|
|
-pub fn build_script(repo: &str, rev: &str, dir: &str) -> String {
|
|
|
+pub fn build_script(repo: &str, rev: &str, dir: &str, config_path: &str) -> String {
|
|
|
+ // `dgpp-cluster install` is not idempotent (a re-install of the same
|
|
|
+ // release errors "already exists"). Tolerate that only when the release is
|
|
|
+ // actually installed: fall back to `releases | grep <rev-abbrev>` (the
|
|
|
+ // version string carries the first 12 rev chars). A genuinely missing
|
|
|
+ // release still fails (grep misses) → no marker → retry.
|
|
|
+ let rev12: String = rev.chars().take(12).collect();
|
|
|
format!(
|
|
|
"set -e; \
|
|
|
if [ -d \"{dir}/.git\" ]; then cd \"{dir}\" && git fetch --all --tags && git checkout '{rev}'; \
|
|
|
else git clone '{repo}' \"{dir}\" && cd \"{dir}\" && git checkout '{rev}'; fi; \
|
|
|
scripts/release.sh && \
|
|
|
- scripts/dgpp-cluster install dist/dgpp-*.tar.zst && \
|
|
|
+ (scripts/dgpp-cluster install dist/dgpp-*.tar.zst --config \"{config_path}\" \
|
|
|
+ || scripts/dgpp-cluster releases --config \"{config_path}\" | grep -qF '{rev12}') && \
|
|
|
echo '{rev}' > .sm121-dgpp-rev && echo {DONE_MARKER}"
|
|
|
)
|
|
|
}
|
|
|
|
|
|
-/// The detached tmux new-session command wrapping the build script (tee to a
|
|
|
-/// per-recipe log). Session/log names are per-recipe, like the docker build.
|
|
|
-pub fn tmux_build(recipe_name: &str, dir: &str, repo: &str, rev: &str) -> String {
|
|
|
+/// Path of the build script written to the node (heredoc) and its log.
|
|
|
+pub fn script_path(recipe_name: &str) -> String {
|
|
|
+ format!("/tmp/{recipe_name}-dgpp-build.sh")
|
|
|
+}
|
|
|
+pub fn log_path(recipe_name: &str) -> String {
|
|
|
+ format!("/tmp/{recipe_name}-dgpp-build.log")
|
|
|
+}
|
|
|
+
|
|
|
+/// The detached tmux command: run the (already-uploaded) build script through
|
|
|
+/// bash and tee its WHOLE output to the log. Writing the script to a file
|
|
|
+/// first avoids the quoting trap where `cmd && ... && echo DONE 2>&1 | tee`
|
|
|
+/// pipes only the final command — the file keeps the build output in the log.
|
|
|
+pub fn tmux_build(recipe_name: &str) -> String {
|
|
|
let session = format!("sm121-dgpp-{recipe_name}");
|
|
|
- let logfile = format!("/tmp/{recipe_name}-dgpp-build.log");
|
|
|
format!(
|
|
|
"tmux kill-session -t '{session}' 2>/dev/null || true; \
|
|
|
- tmux new-session -d -s '{session}' '{script} 2>&1 | tee {logfile}'",
|
|
|
- script = build_script(repo, rev, dir).replace('\'', r"'\''"),
|
|
|
+ tmux new-session -d -s '{session}' 'bash {script} 2>&1 | tee {log}'",
|
|
|
+ script = script_path(recipe_name),
|
|
|
+ log = log_path(recipe_name),
|
|
|
)
|
|
|
}
|
|
|
|
|
|
@@ -63,6 +80,7 @@ pub async fn provision_node(
|
|
|
node: &Node,
|
|
|
recipe_name: &str,
|
|
|
d: &DgppConfig,
|
|
|
+ config_path: &str,
|
|
|
dry_run: bool,
|
|
|
) -> Result<()> {
|
|
|
let (repo, rev) = match (d.repo.as_deref(), d.rev.as_deref()) {
|
|
|
@@ -80,7 +98,15 @@ pub async fn provision_node(
|
|
|
true,
|
|
|
)
|
|
|
.await?;
|
|
|
- run_ssh(config, node, tmux_build(recipe_name, &dir, repo, rev), true).await?;
|
|
|
+ super::write_remote(
|
|
|
+ config,
|
|
|
+ node,
|
|
|
+ &script_path(recipe_name),
|
|
|
+ &build_script(repo, rev, &dir, config_path),
|
|
|
+ true,
|
|
|
+ )
|
|
|
+ .await?;
|
|
|
+ run_ssh(config, node, tmux_build(recipe_name), true).await?;
|
|
|
return Ok(());
|
|
|
}
|
|
|
|
|
|
@@ -101,16 +127,18 @@ pub async fn provision_node(
|
|
|
"Provisioning DGPP '{rev}' on '{}' (tmux: sm121-dgpp-{recipe_name})...",
|
|
|
node.name
|
|
|
);
|
|
|
- run_ssh(
|
|
|
+ super::write_remote(
|
|
|
config,
|
|
|
node,
|
|
|
- tmux_build(recipe_name, &dir, repo, rev),
|
|
|
+ &script_path(recipe_name),
|
|
|
+ &build_script(repo, rev, &dir, config_path),
|
|
|
false,
|
|
|
)
|
|
|
.await?;
|
|
|
+ run_ssh(config, node, tmux_build(recipe_name), false).await?;
|
|
|
|
|
|
let session = format!("sm121-dgpp-{recipe_name}");
|
|
|
- let logfile = format!("/tmp/{recipe_name}-dgpp-build.log");
|
|
|
+ let logfile = log_path(recipe_name);
|
|
|
let start = std::time::Instant::now();
|
|
|
loop {
|
|
|
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
|
|
@@ -177,27 +205,28 @@ mod tests {
|
|
|
"https://github.com/HawkBearPig/dgpp.git",
|
|
|
"v1.2.3",
|
|
|
"$HOME/dgpp",
|
|
|
+ "$HOME/dgpp/cluster_sm121_x.json",
|
|
|
);
|
|
|
// dir double-quoted ($HOME expands), repo/rev single-quoted literals.
|
|
|
assert!(s.contains("git clone 'https://github.com/HawkBearPig/dgpp.git' \"$HOME/dgpp\""));
|
|
|
assert!(s.contains("git checkout 'v1.2.3'"));
|
|
|
assert!(s.contains("scripts/release.sh"));
|
|
|
- assert!(s.contains("scripts/dgpp-cluster install dist/dgpp-*.tar.zst"));
|
|
|
+ // install needs the deployment config (double-quoted → $HOME expands),
|
|
|
+ // and tolerates "already exists" via a releases|grep fallback.
|
|
|
+ assert!(s.contains("scripts/dgpp-cluster install dist/dgpp-*.tar.zst --config \"$HOME/dgpp/cluster_sm121_x.json\""));
|
|
|
+ assert!(s.contains("|| scripts/dgpp-cluster releases --config \"$HOME/dgpp/cluster_sm121_x.json\" | grep -qF 'v1.2.3'"));
|
|
|
// marker + DONE come last (after `&&`), so a failed build leaves none.
|
|
|
assert!(s.contains("echo 'v1.2.3' > .sm121-dgpp-rev && echo SM121-DGPP-DONE"));
|
|
|
assert!(s.find("scripts/release.sh").unwrap() < s.find(".sm121-dgpp-rev").unwrap());
|
|
|
}
|
|
|
|
|
|
#[test]
|
|
|
- fn tmux_build_detaches_and_tees_to_per_recipe_log() {
|
|
|
- let t = tmux_build(
|
|
|
- "glm53-flash-nvfp4-fp8-dgpp-2x",
|
|
|
- "$HOME/dgpp",
|
|
|
- "https://x/dgpp.git",
|
|
|
- "abc123",
|
|
|
- );
|
|
|
+ fn tmux_build_runs_the_uploaded_script_file_and_tees_whole_output() {
|
|
|
+ let t = tmux_build("glm53-flash-nvfp4-fp8-dgpp-2x");
|
|
|
assert!(t.contains("tmux new-session -d -s 'sm121-dgpp-glm53-flash-nvfp4-fp8-dgpp-2x'"));
|
|
|
- assert!(t.contains("tee /tmp/glm53-flash-nvfp4-fp8-dgpp-2x-dgpp-build.log"));
|
|
|
+ // the whole script's output is teed (bash <file> 2>&1 | tee <log>), not
|
|
|
+ // just the final echo — the file avoids the pipe-binds-to-last-cmd trap.
|
|
|
+ assert!(t.contains("bash /tmp/glm53-flash-nvfp4-fp8-dgpp-2x-dgpp-build.sh 2>&1 | tee /tmp/glm53-flash-nvfp4-fp8-dgpp-2x-dgpp-build.log"));
|
|
|
// stale-session guard so a rerun doesn't hit "duplicate session".
|
|
|
assert!(t.contains("tmux kill-session -t 'sm121-dgpp-glm53-flash-nvfp4-fp8-dgpp-2x'"));
|
|
|
}
|