Add joafri image metadata in compose and multi-arch build/push script
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
name: sample-app
|
||||
|
||||
x-image:
|
||||
namespace: ${IMAGE_NAMESPACE:-joafri}
|
||||
tag: ${IMAGE_TAG:-main}
|
||||
|
||||
services:
|
||||
app:
|
||||
image: ghcr.io/example/sample-app:1.0.0
|
||||
image: ${IMAGE_NAMESPACE:-joafri}/sample-app-app:${IMAGE_TAG:-main}
|
||||
container_name: sample-app
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
name: caddy-autogen
|
||||
|
||||
x-image:
|
||||
namespace: ${IMAGE_NAMESPACE:-joafri}
|
||||
tag: ${IMAGE_TAG:-main}
|
||||
|
||||
services:
|
||||
caddy:
|
||||
image: ${IMAGE_NAMESPACE:-joafri}/caddy-autogen-caddy:${IMAGE_TAG:-main}
|
||||
build:
|
||||
context: ./caddy
|
||||
dockerfile: Dockerfile
|
||||
@@ -79,6 +84,7 @@ services:
|
||||
- ALL
|
||||
|
||||
discovery-agent:
|
||||
image: ${IMAGE_NAMESPACE:-joafri}/caddy-autogen-discovery-agent:${IMAGE_TAG:-main}
|
||||
build:
|
||||
context: ./agent
|
||||
dockerfile: Dockerfile
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
name: docker-ip-addr-manager
|
||||
|
||||
x-image:
|
||||
namespace: ${IMAGE_NAMESPACE:-joafri}
|
||||
tag: ${IMAGE_TAG:-main}
|
||||
|
||||
services:
|
||||
app:
|
||||
image: ${IMAGE_NAMESPACE:-joafri}/docker-ip-addr-manager-app:${IMAGE_TAG:-main}
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: steam-headless
|
||||
|
||||
x-image:
|
||||
namespace: ${IMAGE_NAMESPACE:-joafri}
|
||||
tag: ${IMAGE_TAG:-main}
|
||||
|
||||
services:
|
||||
steam:
|
||||
image: lscr.io/linuxserver/steam:version-f4f48542@sha256:d7b9fbf302e05ae79248d1171fe9751b354f8397eafa1e13a3df0aa6a75de0b4
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
name: steam-moonlight
|
||||
|
||||
x-image:
|
||||
namespace: ${IMAGE_NAMESPACE:-joafri}
|
||||
tag: ${IMAGE_TAG:-main}
|
||||
|
||||
x-steam-common: &steam-common
|
||||
image: josh5/steam-headless:debian-0.2.0@sha256:540366bee31297c5679a5006a84dbca039ca62aaab695852b51b5f62dffd2c14
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -20,6 +20,8 @@ Skelett för att bygga och underhålla ZimaOS/CasaOS-appar i ett eget appstore-r
|
||||
├── featured-apps.json
|
||||
├── recommend-list.json
|
||||
└── scripts/
|
||||
├── build-appstore-zip.sh
|
||||
├── build-and-push-image.sh
|
||||
└── validate-appstore.sh
|
||||
```
|
||||
|
||||
@@ -46,6 +48,16 @@ Inför release/publicering, kör strikt validering för högrisk-inställningar:
|
||||
./scripts/validate-appstore.sh --enforce-risk-docs
|
||||
```
|
||||
|
||||
Bygg och publicera app-specifika custom images (Docker Hub namespace `joafri` som default):
|
||||
|
||||
```bash
|
||||
./scripts/build-and-push-image.sh --app-id caddy-autogen --tag 0.1.0
|
||||
```
|
||||
|
||||
Scriptet läser `Apps/<app-id>/docker-compose.yaml` och bygger alla services som har `build:` om `--component` inte anges.
|
||||
`--component` kan användas för en enskild service (service-namn eller context-path).
|
||||
Arkitekturer hämtas från `x-casaos.architectures`; builden kör fail-open per arkitektur och visar varningar i slutet för misslyckade arch-builds.
|
||||
|
||||
## Säkerhetsriktlinjer
|
||||
|
||||
- Undvik privilegierad container, host network och `docker.sock` om det inte är absolut nödvändigt.
|
||||
|
||||
Executable
+355
@@ -0,0 +1,355 @@
|
||||
#!/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)
|
||||
--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
|
||||
|
||||
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
|
||||
;;
|
||||
-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
|
||||
|
||||
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
|
||||
echo "ERROR: no buildable services found in $compose_file" >&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
|
||||
Reference in New Issue
Block a user