feat(builder): merge deb-publisher + publish-docker-image into contract-driven builder skill
- skills/builder: SKILL.md, README.md, references/contract.md (make/publish contract v1), references/registry.md - scripts/check.py: executable contract checker (make dry-run probes, secret scan, push thin-wrapper and script path checks; --build verifies real .deb) - scripts/upload_deb.sh: migrated from deb-publisher, adds project .env auto-load and dirty-worktree publish gate - scripts/publish_docker.sh: migrated from publish-docker-image publish.sh, now env-first (DOCKER_REGISTRY/REPOSITORY/IMAGE_TAG/PLATFORMS), refuses floating latest and multi-platform --load - scripts/verify_deb.sh: metadata/content/sha256 verification with v-prefix normalization - orc: deb+docker stages both route to $builder; routing table, DAGs, README, config untouched stage names; tests updated - ack delivery.md + skiff source-model.md: reference builder - remove skills/deb-publisher and skills/publish-docker-image
This commit is contained in:
Executable
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
sed -n '2,30p' "$0"
|
||||
}
|
||||
|
||||
# Build and publish a Docker image with buildx. Configuration comes from the
|
||||
# environment first (optionally loaded from the project root .env); flags
|
||||
# override.
|
||||
#
|
||||
# Usage:
|
||||
# publish_docker.sh [--registry HOST] [--repository PATH] [--tag TAG] \
|
||||
# [--platform LIST] [options]
|
||||
#
|
||||
# Environment:
|
||||
# DOCKER_REGISTRY Required (or --registry)
|
||||
# DOCKER_REPOSITORY Optional, default: git repository name (or --repository)
|
||||
# IMAGE_TAG Optional, default: git describe --tags --always --dirty (or --tag)
|
||||
# PLATFORMS Optional, default: linux/amd64 (or --platform)
|
||||
# DOCKER_DOCKERFILE Optional, default: Dockerfile (--file)
|
||||
# DOCKER_CONTEXT Optional, default: . (--context)
|
||||
# DOCKER_BUILDER Optional buildx builder name (--builder)
|
||||
# ALLOW_UNCOMMITTED=1 Publish despite a dirty working tree
|
||||
#
|
||||
# Options:
|
||||
# --load Load a single-platform image instead of pushing
|
||||
# --dry-run Print the resolved build without executing it
|
||||
# -h, --help Show this help
|
||||
|
||||
project_root=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
||||
|
||||
# Load project .env without printing values; explicitly exported shell values keep precedence.
|
||||
if [[ -n "$project_root" && -f "$project_root/.env" ]]; then
|
||||
while IFS='=' read -r key value; do
|
||||
key=${key%%[[:space:]]*}
|
||||
[[ -z "$key" || "$key" == \#* ]] && continue
|
||||
if [[ -n "${!key:-}" ]]; then
|
||||
continue # shell value already set: wins over .env
|
||||
fi
|
||||
value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'}
|
||||
printf -v "$key" '%s' "$value"
|
||||
export "$key"
|
||||
done < <(grep -v '^[[:space:]]*$' "$project_root/.env")
|
||||
fi
|
||||
|
||||
git_repo_name=
|
||||
if [[ -n "$project_root" ]]; then
|
||||
git_repo_name=$(basename "$(git -C "$project_root" rev-parse --show-toplevel)")
|
||||
fi
|
||||
|
||||
registry=${DOCKER_REGISTRY:-}
|
||||
repository=${DOCKER_REPOSITORY:-$git_repo_name}
|
||||
tag=${IMAGE_TAG:-}
|
||||
platform=${PLATFORMS:-linux/amd64}
|
||||
dockerfile=${DOCKER_DOCKERFILE:-Dockerfile}
|
||||
build_context=${DOCKER_CONTEXT:-.}
|
||||
builder=${DOCKER_BUILDER:-}
|
||||
mode=push
|
||||
dry_run=false
|
||||
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
--registry) registry=$2; shift 2 ;;
|
||||
--repository) repository=$2; shift 2 ;;
|
||||
--tag) tag=$2; shift 2 ;;
|
||||
--platform) platform=$2; shift 2 ;;
|
||||
--file) dockerfile=$2; shift 2 ;;
|
||||
--context) build_context=$2; shift 2 ;;
|
||||
--builder) builder=$2; shift 2 ;;
|
||||
--load) mode=load; shift ;;
|
||||
--dry-run) dry_run=true; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Error: unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$registry" && ( "$registry" == *://* || "$registry" == */* ) ]]; then
|
||||
echo "Error: registry must be a bare host without scheme or slash: $registry" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -z "$registry" ]]; then
|
||||
echo "Error: DOCKER_REGISTRY (or --registry) is required." >&2
|
||||
echo "Set it in the environment or the project root .env." >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -z "$repository" || "$repository" == /* || "$repository" == */ || "$repository" != */* ]]; then
|
||||
echo "Error: repository must be namespace/name without leading or trailing slash: $repository" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -z "$tag" ]]; then
|
||||
if [[ -n "$project_root" ]]; then
|
||||
tag=$(git -C "$project_root" describe --tags --always --dirty 2>/dev/null) || tag=
|
||||
fi
|
||||
if [[ -z "$tag" ]]; then
|
||||
echo "Error: IMAGE_TAG (or --tag) is required outside a git repository." >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
if [[ "$tag" == *:* || "$tag" == */* ]]; then
|
||||
echo "Error: tag must not contain : or /: $tag" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$tag" == latest && ${ALLOW_LATEST:-0} != 1 && "$mode" == push ]]; then
|
||||
echo "Error: refusing to publish floating tag 'latest'; pass an explicit version." >&2
|
||||
echo "Set ALLOW_LATEST=1 only when the user explicitly asked for 'latest'." >&2
|
||||
exit 3
|
||||
fi
|
||||
if [[ "$mode" == load && "$platform" == *,* ]]; then
|
||||
echo "Error: --load cannot be combined with multiple platforms: $platform" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -f "$dockerfile" ]]; then
|
||||
echo "Error: Dockerfile not found: $dockerfile" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -d "$build_context" ]]; then
|
||||
echo "Error: build context not found: $build_context" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$dry_run" == false ]] && ! command -v docker >/dev/null 2>&1; then
|
||||
echo "Error: docker is required." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Dirty-tree gate: publishing uncommitted content requires explicit opt-in.
|
||||
if [[ "$mode" == push && "$dry_run" == false && -n "$project_root" ]] \
|
||||
&& git -C "$project_root" rev-parse HEAD >/dev/null 2>&1; then
|
||||
if [[ ${ALLOW_UNCOMMITTED:-0} != 1 ]] && ! git -C "$project_root" diff-index --quiet HEAD -- 2>/dev/null; then
|
||||
echo "Error: working tree has uncommitted changes; refusing to publish." >&2
|
||||
echo "Commit first, or set ALLOW_UNCOMMITTED=1 to publish anyway." >&2
|
||||
exit 3
|
||||
fi
|
||||
fi
|
||||
|
||||
image_ref="${registry}/${repository}:${tag}"
|
||||
build_cmd=(docker buildx build --file "$dockerfile" --platform "$platform" --tag "$image_ref")
|
||||
if [[ -n "$builder" ]]; then
|
||||
build_cmd+=(--builder "$builder")
|
||||
fi
|
||||
if [[ "$mode" == push ]]; then
|
||||
build_cmd+=(--push)
|
||||
else
|
||||
build_cmd+=(--load)
|
||||
fi
|
||||
build_cmd+=("$build_context")
|
||||
|
||||
printf 'Image: %s\n' "$image_ref"
|
||||
printf 'Platform: %s\n' "$platform"
|
||||
printf 'Dockerfile: %s\n' "$dockerfile"
|
||||
printf 'Context: %s\n' "$build_context"
|
||||
printf 'Mode: %s\n' "$mode"
|
||||
|
||||
if [[ "$dry_run" == true ]]; then
|
||||
printf 'Command: %s\n' "${build_cmd[*]}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"${build_cmd[@]}"
|
||||
|
||||
if [[ "$mode" == push ]]; then
|
||||
docker buildx imagetools inspect "$image_ref"
|
||||
fi
|
||||
Reference in New Issue
Block a user