#!/usr/bin/env bash set -euo pipefail # Obtain a verified artifact for an exact git commit. # Build may have happened locally or in CI; this script only reports identity # after the artifact exists. Does not print tokens. # # Usage: # obtain.sh [--sha SHA] [--branch NAME] [--root DIR] [--timeout SEC] # # BUILD_VENUE (makefile.builder, default local): # local HEAD must be SHA; run `make -f makefile.builder docker` or `deb` # ci wait for this SHA via the project CI_STATUS_SCRIPT, then inspect the image # # Exit: # 0 artifact verified; prints ARTIFACT= / DIGEST= / SHA= / VENUE= # 1 no pipeline, job failed, inspect failed # 2 usage / config # 3 CI status script cannot query (ci venue) usage() { sed -n '2,20p' "$0" } sha= branch= root= timeout_sec=1200 while (($#)); do case "$1" in --sha) sha=$2; shift 2 ;; --branch) branch=$2; shift 2 ;; --root) root=$2; shift 2 ;; --timeout) timeout_sec=$2; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "Error: unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done if [[ -z "$root" ]]; then root=$(git rev-parse --show-toplevel 2>/dev/null || true) fi if [[ -z "$root" ]]; then echo "Error: --root must be a git project" >&2 exit 2 fi cd "$root" if [[ -z "$sha" ]]; then sha=$(git rev-parse HEAD) else sha=$(git rev-parse "$sha") fi # Load .env.builder without printing values; shell values win. Do not read `.env`. if [[ -f "$root/.env.builder" ]]; then while IFS='=' read -r key value; do key=${key%%[[:space:]]*} [[ -z "$key" || "$key" == \#* ]] && continue if [[ -n "${!key:-}" ]]; then continue fi value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'} [[ -z "$value" ]] && continue printf -v "$key" '%s' "$value" export "$key" done < <(grep -v '^[[:space:]]*$' "$root/.env.builder") fi script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) version_sh=$script_dir/version.sh wait_ci=$script_dir/wait_ci.sh venue=${BUILD_VENUE:-} registry=${DOCKER_REGISTRY:-} repository=${DOCKER_REPOSITORY:-} if [[ -f "$root/makefile.builder" ]]; then if [[ -z "$venue" ]]; then venue=$(make -s -C "$root" -f makefile.builder venue 2>/dev/null || true) fi if [[ -z "$registry" ]]; then registry=$(make -s -C "$root" -f makefile.builder print-DOCKER_REGISTRY 2>/dev/null || true) fi if [[ -z "$repository" ]]; then repository=$(make -s -C "$root" -f makefile.builder print-DOCKER_REPOSITORY 2>/dev/null || true) fi fi venue=${venue:-local} docker_tag_for_sha() { local tmp tag tmp=$(mktemp -d "${TMPDIR:-/tmp}/builder-obtain.XXXXXX") git -C "$root" worktree add --detach "$tmp" "$sha" >/dev/null if [[ -n "$branch" ]]; then tag=$(CI_COMMIT_BRANCH="$branch" "$version_sh" -C "$tmp" --docker) else tag=$("$version_sh" -C "$tmp" --docker) fi git -C "$root" worktree remove --force "$tmp" >/dev/null printf '%s' "$tag" } inspect_image() { local image=$1 out digest if ! command -v docker >/dev/null 2>&1; then echo "Error: docker is required to inspect $image" >&2 exit 1 fi out=$(docker buildx imagetools inspect "$image" 2>/dev/null \ || docker manifest inspect "$image" 2>/dev/null \ || true) if [[ -z "$out" ]]; then echo "Error: cannot inspect $image" >&2 exit 1 fi digest=$(printf '%s\n' "$out" | awk '/^Digest:/ {print $2; exit}') if [[ -z "$digest" ]]; then digest=$(python3 -c 'import json,sys try: d=json.load(sys.stdin) except Exception: sys.exit(0) print(d.get("digest") or "")' <<<"$out") fi if [[ -z "$digest" ]]; then echo "Error: inspect of $image had no digest" >&2 exit 1 fi printf '%s' "$digest" } case "$venue" in ci) "$wait_ci" --sha "$sha" --root "$root" --timeout "$timeout_sec" if [[ ! -x "$version_sh" ]]; then echo "Error: version.sh not found: $version_sh" >&2 exit 2 fi if [[ -z "$registry" || -z "$repository" ]]; then echo "Error: DOCKER_REGISTRY and DOCKER_REPOSITORY are required to inspect the CI image" >&2 exit 2 fi tag=$(docker_tag_for_sha) image="${registry}/${repository}:${tag}" digest=$(inspect_image "$image") echo "ARTIFACT=$image" echo "DIGEST=$digest" echo "SHA=$sha" echo "VENUE=ci" echo "TAG=$tag" ;; local) head=$(git rev-parse HEAD) if [[ "$head" != "$sha" ]]; then echo "Error: local obtain requires HEAD=$sha (HEAD is $head)" >&2 exit 2 fi if [[ ! -f "$root/makefile.builder" ]]; then echo "Error: no makefile.builder" >&2 exit 2 fi if make -s -C "$root" -f makefile.builder -n docker >/dev/null 2>&1; then make -C "$root" -f makefile.builder docker if [[ -n "${IMAGE_TAG:-}" ]]; then tag=$IMAGE_TAG elif [[ -x "$version_sh" ]]; then tag=$("$version_sh" --docker) else echo "Error: cannot derive IMAGE_TAG" >&2 exit 2 fi name=$(make -s -C "$root" -f makefile.builder print-PROJECT_NAME 2>/dev/null || basename "$root") image="${name}:${tag}" if [[ -n "$registry" && -n "$repository" ]]; then image="${registry}/${repository}:${tag}" fi echo "ARTIFACT=$image" echo "DIGEST=local" echo "SHA=$sha" echo "VENUE=local" echo "TAG=$tag" elif make -s -C "$root" -f makefile.builder -n deb >/dev/null 2>&1; then make -C "$root" -f makefile.builder deb echo "ARTIFACT=deb" echo "SHA=$sha" echo "VENUE=local" else echo "Error: local venue has no docker or deb target" >&2 exit 2 fi ;; *) echo "Error: BUILD_VENUE must be local or ci (got ${venue})" >&2 exit 2 ;; esac