huskies: merge 1118 story Automate per-project docker image builds (huskies-project-base + per-stack overlays)
This commit is contained in:
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Build all project images in dependency order:
|
||||
# huskies → huskies-project-base → huskies-project-<stack> (one per stack fragment)
|
||||
#
|
||||
# Run this after `script/docker_rebuild` or whenever you add a new stack.
|
||||
# Safe to re-run: each step re-tags the image with the latest layers.
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
if [[ -f .env ]]; then
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
CACHE_FLAG=""
|
||||
if [[ "${1:-}" == "--no-cache" ]]; then
|
||||
CACHE_FLAG="--no-cache"
|
||||
fi
|
||||
|
||||
echo "==> Building huskies"
|
||||
docker build $CACHE_FLAG -t huskies -f docker/Dockerfile .
|
||||
|
||||
echo "==> Building huskies-project-base"
|
||||
docker build $CACHE_FLAG -t huskies-project-base -f docker/Dockerfile.base .
|
||||
|
||||
for fragment in docker/stacks/*/Dockerfile.fragment; do
|
||||
stack=$(basename "$(dirname "$fragment")")
|
||||
image="huskies-project-${stack}"
|
||||
echo "==> Building ${image}"
|
||||
(printf 'FROM huskies-project-base\n'; cat "$fragment") \
|
||||
| docker build $CACHE_FLAG -t "$image" -
|
||||
done
|
||||
|
||||
echo "All project images built."
|
||||
@@ -24,4 +24,6 @@ docker compose -f docker/docker-compose.yml down
|
||||
docker compose -f docker/docker-compose.yml build $CACHE_FLAG
|
||||
docker compose -f docker/docker-compose.yml up -d
|
||||
|
||||
script/build-project-images $CACHE_FLAG
|
||||
|
||||
echo "Rebuild complete. Logs: docker compose -f docker/docker-compose.yml logs -f"
|
||||
|
||||
@@ -518,7 +518,7 @@ async fn handle_adopt_project(
|
||||
Ok(out) => {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let _ = tokio::fs::remove_dir_all(&ssh_key_dir).await;
|
||||
format!("Docker container launch failed: {}", stderr.trim())
|
||||
interpret_docker_run_error(&stderr, &image)
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = tokio::fs::remove_dir_all(&ssh_key_dir).await;
|
||||
@@ -891,9 +891,9 @@ pub async fn handle_new_project(
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let _ = tokio::fs::remove_dir_all(&host_path).await;
|
||||
let _ = tokio::fs::remove_dir_all(&ssh_key_dir).await;
|
||||
let base_msg = interpret_docker_run_error(&stderr, &image);
|
||||
format!(
|
||||
"Docker container launch failed: {}\n\nPartial state removed at `{}`.",
|
||||
stderr.trim(),
|
||||
"{base_msg}\n\nPartial state removed at `{}`.",
|
||||
host_path.display()
|
||||
)
|
||||
}
|
||||
@@ -908,6 +908,22 @@ pub async fn handle_new_project(
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a failed `docker run` stderr into an actionable chat message.
|
||||
///
|
||||
/// When Docker cannot find the image locally it prints `Unable to find image`.
|
||||
/// That bare error is unhelpful — this function maps it to a message that tells
|
||||
/// the user which script to run. All other failures are passed through as-is.
|
||||
fn interpret_docker_run_error(stderr: &str, image: &str) -> String {
|
||||
if stderr.contains("Unable to find image") {
|
||||
format!(
|
||||
"Image `{image}` not found locally. \
|
||||
Build project images first by running `script/build-project-images`."
|
||||
)
|
||||
} else {
|
||||
format!("Docker container launch failed: {}", stderr.trim())
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a free TCP port by attempting to bind starting from `start`.
|
||||
///
|
||||
/// Scans up to 100 ports above `start` and returns the first available one.
|
||||
@@ -1257,6 +1273,37 @@ mod tests {
|
||||
assert!(warnings.is_empty());
|
||||
}
|
||||
|
||||
// ── Missing-image error path ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn interpret_docker_run_error_missing_image_points_at_script() {
|
||||
let stderr = "Unable to find image 'huskies-project-rust:latest' locally\n\
|
||||
docker: Error response from daemon: pull access denied";
|
||||
let msg = interpret_docker_run_error(stderr, "huskies-project-rust");
|
||||
assert!(
|
||||
msg.contains("script/build-project-images"),
|
||||
"expected script name in error message, got: {msg}"
|
||||
);
|
||||
assert!(
|
||||
msg.contains("huskies-project-rust"),
|
||||
"expected image name in error message, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpret_docker_run_error_other_failure_passes_through() {
|
||||
let stderr = "Error response from daemon: container name already in use";
|
||||
let msg = interpret_docker_run_error(stderr, "huskies-project-rust");
|
||||
assert!(
|
||||
msg.contains("Docker container launch failed"),
|
||||
"expected generic error message, got: {msg}"
|
||||
);
|
||||
assert!(
|
||||
msg.contains("container name already in use"),
|
||||
"expected original stderr in message, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_stack_jvm_build_gradle_kts() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user