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 build $CACHE_FLAG
|
||||||
docker compose -f docker/docker-compose.yml up -d
|
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"
|
echo "Rebuild complete. Logs: docker compose -f docker/docker-compose.yml logs -f"
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ async fn handle_adopt_project(
|
|||||||
Ok(out) => {
|
Ok(out) => {
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
let _ = tokio::fs::remove_dir_all(&ssh_key_dir).await;
|
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) => {
|
Err(e) => {
|
||||||
let _ = tokio::fs::remove_dir_all(&ssh_key_dir).await;
|
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 stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
let _ = tokio::fs::remove_dir_all(&host_path).await;
|
let _ = tokio::fs::remove_dir_all(&host_path).await;
|
||||||
let _ = tokio::fs::remove_dir_all(&ssh_key_dir).await;
|
let _ = tokio::fs::remove_dir_all(&ssh_key_dir).await;
|
||||||
|
let base_msg = interpret_docker_run_error(&stderr, &image);
|
||||||
format!(
|
format!(
|
||||||
"Docker container launch failed: {}\n\nPartial state removed at `{}`.",
|
"{base_msg}\n\nPartial state removed at `{}`.",
|
||||||
stderr.trim(),
|
|
||||||
host_path.display()
|
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`.
|
/// 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.
|
/// Scans up to 100 ports above `start` and returns the first available one.
|
||||||
@@ -1257,6 +1273,37 @@ mod tests {
|
|||||||
assert!(warnings.is_empty());
|
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]
|
#[test]
|
||||||
fn detect_stack_jvm_build_gradle_kts() {
|
fn detect_stack_jvm_build_gradle_kts() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user