371 lines
9.5 KiB
Bash
Executable File
371 lines
9.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
|
|
default_repo="joafri"
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage:
|
|
./scripts/build-and-push-image.sh --app-id <app-id> --tag <tag> [options]
|
|
|
|
Required:
|
|
--app-id <app-id> App folder under Apps/, for example: caddy-autogen
|
|
--tag <tag> Docker tag base (must not be latest)
|
|
|
|
Options:
|
|
--component <value> Optional. Service name OR build context path under app.
|
|
If omitted, build all services with build contexts.
|
|
--repo <namespace> Docker namespace/org (default: joafri)
|
|
--skip-non-buildable Exit 0 (with warning) when app has no services with build context
|
|
--no-push Build only, no push or manifest creation
|
|
-h, --help Show this help
|
|
|
|
Behavior:
|
|
- Reads buildable services from Apps/<app-id>/docker-compose.yaml.
|
|
- Reads architectures from x-casaos.architectures in the same compose file.
|
|
- Builds each architecture separately and tags as <tag>-<arch>.
|
|
- When pushing, creates a manifest tag <tag> from successful arch tags.
|
|
- Fail-open per architecture: failed arch builds are reported as warnings.
|
|
|
|
Examples:
|
|
./scripts/build-and-push-image.sh --app-id caddy-autogen --tag 0.2.0
|
|
./scripts/build-and-push-image.sh --app-id caddy-autogen --component agent --tag 0.2.0
|
|
USAGE
|
|
}
|
|
|
|
app_id=""
|
|
component=""
|
|
tag=""
|
|
repo_ns="$default_repo"
|
|
push_image=1
|
|
skip_non_buildable=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--app-id)
|
|
app_id="${2:-}"
|
|
shift 2
|
|
;;
|
|
--component)
|
|
component="${2:-}"
|
|
shift 2
|
|
;;
|
|
--tag)
|
|
tag="${2:-}"
|
|
shift 2
|
|
;;
|
|
--repo)
|
|
repo_ns="${2:-}"
|
|
shift 2
|
|
;;
|
|
--no-push)
|
|
push_image=0
|
|
shift
|
|
;;
|
|
--skip-non-buildable)
|
|
skip_non_buildable=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "ERROR: unknown argument: $1" >&2
|
|
usage
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$app_id" || -z "$tag" ]]; then
|
|
echo "ERROR: --app-id and --tag are required" >&2
|
|
usage
|
|
exit 2
|
|
fi
|
|
|
|
if [[ "$tag" == "latest" ]]; then
|
|
echo "ERROR: tag 'latest' is not allowed by repo policy" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$app_id" =~ [^a-z0-9-] ]]; then
|
|
echo "ERROR: app-id must match [a-z0-9-]+" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$repo_ns" =~ [^a-zA-Z0-9._-] ]]; then
|
|
echo "ERROR: repo namespace contains invalid characters" >&2
|
|
exit 1
|
|
fi
|
|
|
|
component="${component#./}"
|
|
if [[ -n "$component" && ( "$component" == /* || "$component" == *".."* ) ]]; then
|
|
echo "ERROR: --component must be service name or relative subpath under Apps/$app_id" >&2
|
|
exit 1
|
|
fi
|
|
|
|
app_dir="$repo_root/Apps/$app_id"
|
|
compose_file="$app_dir/docker-compose.yaml"
|
|
|
|
if [[ ! -d "$app_dir" ]]; then
|
|
echo "ERROR: app directory not found: $app_dir" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "$compose_file" ]]; then
|
|
echo "ERROR: missing compose file: $compose_file" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo "ERROR: docker is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! docker buildx version >/dev/null 2>&1; then
|
|
echo "ERROR: docker buildx is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
echo "ERROR: jq is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
map_arch_to_platform() {
|
|
case "$1" in
|
|
amd64) echo "linux/amd64" ;;
|
|
arm64) echo "linux/arm64" ;;
|
|
arm) echo "linux/arm/v7" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
read_compose_architectures() {
|
|
awk '
|
|
/^x-casaos:[[:space:]]*$/ { in_casa=1; next }
|
|
in_casa && /^[^[:space:]]/ { in_casa=0 }
|
|
in_casa && /^[[:space:]]+architectures:[[:space:]]*$/ { in_arch=1; next }
|
|
in_arch {
|
|
if ($0 ~ /^[[:space:]]*-[[:space:]]*[A-Za-z0-9_-]+[[:space:]]*$/) {
|
|
line=$0
|
|
sub(/^[[:space:]]*-[[:space:]]*/, "", line)
|
|
sub(/[[:space:]]*$/, "", line)
|
|
print line
|
|
next
|
|
}
|
|
if ($0 ~ /^[[:space:]]*$/) next
|
|
in_arch=0
|
|
}
|
|
' "$compose_file"
|
|
}
|
|
|
|
compose_json="$(mktemp)"
|
|
cleanup() {
|
|
rm -f "$compose_json"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
if [[ -z "${AppID:-}" ]]; then
|
|
export AppID="$app_id"
|
|
fi
|
|
|
|
docker compose -f "$compose_file" config --format json > "$compose_json"
|
|
|
|
mapfile -t all_services < <(
|
|
jq -r '.services | to_entries[] | select(.value.build != null) | .key' "$compose_json"
|
|
)
|
|
|
|
if [[ ${#all_services[@]} -eq 0 ]]; then
|
|
if [[ "$skip_non_buildable" -eq 1 ]]; then
|
|
echo "WARN: no buildable services found in $compose_file (skipped)"
|
|
exit 0
|
|
fi
|
|
echo "ERROR: no buildable services found in $compose_file" >&2
|
|
echo "Hint: pass --skip-non-buildable for batch loops over mixed app types." >&2
|
|
exit 1
|
|
fi
|
|
|
|
mapfile -t compose_arches < <(read_compose_architectures)
|
|
if [[ ${#compose_arches[@]} -eq 0 ]]; then
|
|
compose_arches=(amd64)
|
|
echo "WARN: no x-casaos.architectures found, defaulting to amd64"
|
|
fi
|
|
|
|
selected_services=()
|
|
if [[ -n "$component" ]]; then
|
|
for svc in "${all_services[@]}"; do
|
|
context_abs="$(jq -r --arg svc "$svc" '.services[$svc].build.context // empty' "$compose_json")"
|
|
if [[ -z "$context_abs" ]]; then
|
|
continue
|
|
fi
|
|
|
|
context_rel="$context_abs"
|
|
if [[ "$context_abs" == "$app_dir" ]]; then
|
|
context_rel="."
|
|
elif [[ "$context_abs" == "$app_dir"/* ]]; then
|
|
context_rel="${context_abs#"$app_dir"/}"
|
|
fi
|
|
|
|
if [[ "$svc" == "$component" || "$context_rel" == "$component" ]]; then
|
|
selected_services+=("$svc")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#selected_services[@]} -eq 0 ]]; then
|
|
echo "ERROR: --component '$component' did not match any build service name or context path" >&2
|
|
echo "Build services in this app:" >&2
|
|
for svc in "${all_services[@]}"; do
|
|
ctx="$(jq -r --arg svc "$svc" '.services[$svc].build.context // empty' "$compose_json")"
|
|
if [[ "$ctx" == "$app_dir" ]]; then
|
|
ctx_rel="."
|
|
elif [[ "$ctx" == "$app_dir"/* ]]; then
|
|
ctx_rel="${ctx#"$app_dir"/}"
|
|
else
|
|
ctx_rel="$ctx"
|
|
fi
|
|
echo " - $svc (context: $ctx_rel)" >&2
|
|
done
|
|
exit 1
|
|
fi
|
|
else
|
|
selected_services=("${all_services[@]}")
|
|
fi
|
|
|
|
git_sha="unknown"
|
|
if git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
git_sha="$(git -C "$repo_root" rev-parse --short HEAD)"
|
|
fi
|
|
|
|
source_url="unknown"
|
|
if git -C "$repo_root" remote get-url origin >/dev/null 2>&1; then
|
|
source_url="$(git -C "$repo_root" remote get-url origin)"
|
|
fi
|
|
|
|
echo "App: $app_id"
|
|
echo "Compose: $compose_file"
|
|
echo "Services: ${selected_services[*]}"
|
|
echo "Architectures: ${compose_arches[*]}"
|
|
if [[ "$push_image" -eq 1 ]]; then
|
|
echo "Mode: build + push + manifest"
|
|
else
|
|
echo "Mode: build only (--no-push)"
|
|
fi
|
|
|
|
warn_messages=()
|
|
total_success=0
|
|
|
|
for svc in "${selected_services[@]}"; do
|
|
context_abs="$(jq -r --arg svc "$svc" '.services[$svc].build.context // empty' "$compose_json")"
|
|
dockerfile_rel="$(jq -r --arg svc "$svc" '.services[$svc].build.dockerfile // "Dockerfile"' "$compose_json")"
|
|
|
|
if [[ -z "$context_abs" ]]; then
|
|
warn_messages+=("$svc: missing build context in compose config, skipped")
|
|
continue
|
|
fi
|
|
|
|
case "$context_abs" in
|
|
"$app_dir"|"$app_dir"/*) ;;
|
|
*)
|
|
warn_messages+=("$svc: build context outside app dir ($context_abs), skipped")
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
dockerfile_abs="$context_abs/$dockerfile_rel"
|
|
if [[ ! -f "$dockerfile_abs" ]]; then
|
|
warn_messages+=("$svc: Dockerfile missing at $dockerfile_abs, skipped")
|
|
continue
|
|
fi
|
|
|
|
image_repo="${repo_ns}/${app_id}-${svc}"
|
|
success_arch_refs=()
|
|
|
|
echo
|
|
echo "=== Service: $svc ==="
|
|
echo "Context: $context_abs"
|
|
echo "Dockerfile: $dockerfile_abs"
|
|
echo "Image repo: $image_repo"
|
|
|
|
for arch in "${compose_arches[@]}"; do
|
|
if ! platform="$(map_arch_to_platform "$arch")"; then
|
|
warn_messages+=("$svc: unsupported architecture '$arch' in x-casaos.architectures")
|
|
continue
|
|
fi
|
|
|
|
arch_ref="${image_repo}:${tag}-${arch}"
|
|
echo "Building $svc for $arch ($platform) -> $arch_ref"
|
|
|
|
build_cmd=(
|
|
docker buildx build
|
|
--platform "$platform"
|
|
--file "$dockerfile_abs"
|
|
--tag "$arch_ref"
|
|
--label "org.opencontainers.image.source=$source_url"
|
|
--label "org.opencontainers.image.revision=$git_sha"
|
|
)
|
|
|
|
if [[ "$push_image" -eq 1 ]]; then
|
|
build_cmd+=(--push)
|
|
else
|
|
build_cmd+=(--load)
|
|
fi
|
|
|
|
build_cmd+=("$context_abs")
|
|
|
|
if "${build_cmd[@]}"; then
|
|
success_arch_refs+=("$arch_ref")
|
|
total_success=$((total_success + 1))
|
|
else
|
|
warn_messages+=("$svc: build failed for arch '$arch' ($platform)")
|
|
fi
|
|
done
|
|
|
|
if [[ ${#success_arch_refs[@]} -eq 0 ]]; then
|
|
warn_messages+=("$svc: no successful architecture builds")
|
|
continue
|
|
fi
|
|
|
|
if [[ "$push_image" -eq 1 ]]; then
|
|
manifest_ref="${image_repo}:${tag}"
|
|
echo "Creating manifest tag: $manifest_ref"
|
|
if docker buildx imagetools create -t "$manifest_ref" "${success_arch_refs[@]}"; then
|
|
echo "Manifest created: $manifest_ref"
|
|
else
|
|
warn_messages+=("$svc: failed to create manifest tag ${manifest_ref}")
|
|
fi
|
|
else
|
|
first_ref="${success_arch_refs[0]}"
|
|
local_ref="${image_repo}:${tag}"
|
|
if docker tag "$first_ref" "$local_ref"; then
|
|
echo "Tagged local alias: $local_ref -> $first_ref"
|
|
else
|
|
warn_messages+=("$svc: failed to tag local alias ${local_ref}")
|
|
fi
|
|
fi
|
|
done
|
|
|
|
echo
|
|
if [[ "$total_success" -eq 0 ]]; then
|
|
echo "ERROR: no successful builds were produced" >&2
|
|
if [[ ${#warn_messages[@]} -gt 0 ]]; then
|
|
echo "Warnings:" >&2
|
|
for w in "${warn_messages[@]}"; do
|
|
echo " - $w" >&2
|
|
done
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
echo "Build completed with $total_success successful arch build(s)."
|
|
|
|
if [[ ${#warn_messages[@]} -gt 0 ]]; then
|
|
echo
|
|
echo "Warnings (fail-open):"
|
|
for w in "${warn_messages[@]}"; do
|
|
echo " - $w"
|
|
done
|
|
fi
|