#!/usr/bin/env bash # # Fixed pipeline (order is fixed): # 1. git add -A # 2. git commit (or --allow-empty if nothing staged) # 3. GIT_COMMIT → Docker --build-arg (baked into binaries) # 4. docker login ghcr.io — gh auth token + gh api user (one step; fails if gh/docker not ready) # 5. buildx --push per arch + imagetools manifest (server + client) — always, no skips # 6. git push — always # # Configuration (use your machine / repo — no hardcoded owner or registry): # - Remote to parse for ghcr.io/owner/repo: git config ship.remote (default: github) # - Optional full registry base (no tag): git config ship.registryBase ghcr.io/org/image # - Image tags: git config ship.tagServer / ship.tagClient # - Platforms (comma-separated): git config ship.platforms # - Buildx builder: git config ship.buildxBuilder (default: orbstack if present, else default) # # The push remote URL must be github.com (ssh or https). Owner/repo set GHCR path unless ship.registryBase is set. # # Usage: ./ship.sh set -euo pipefail if [[ $# -lt 1 ]] || [[ -z "${1// }" ]]; then echo "usage: $0 " >&2 exit 1 fi COMMIT_MSG="$*" ROOT="$(cd "$(dirname "$0")" && pwd)" cd "$ROOT" for cmd in git docker gh; do command -v "$cmd" >/dev/null 2>&1 || { echo "ship: fatal: missing required command: $cmd" >&2 exit 1 } done if ! docker buildx version >/dev/null 2>&1; then echo "ship: fatal: docker buildx not available" >&2 exit 1 fi BRANCH="$(git branch --show-current)" if [[ -z "$BRANCH" ]]; then echo "ship: fatal: detached HEAD" >&2 exit 1 fi GIT_REMOTE="$(git config ship.remote 2>/dev/null)" || true GIT_REMOTE="${GIT_REMOTE:-github}" if ! REMOTE_URL="$(git remote get-url "$GIT_REMOTE" 2>/dev/null)"; then echo "ship: fatal: git remote '$GIT_REMOTE' not found (set: git config ship.remote )" >&2 exit 1 fi REMOTE_OWNER="" REMOTE_REPO="" if [[ "$REMOTE_URL" =~ ^git@github\.com:([^/]+)/(.+)$ ]]; then REMOTE_OWNER="${BASH_REMATCH[1]}" REMOTE_REPO="${BASH_REMATCH[2]%.git}" elif [[ "$REMOTE_URL" =~ ^https://github\.com/([^/]+)/([^/?#]+) ]]; then REMOTE_OWNER="${BASH_REMATCH[1]}" REMOTE_REPO="${BASH_REMATCH[2]%.git}" else echo "ship: fatal: remote '$GIT_REMOTE' URL is not github.com: $REMOTE_URL" >&2 exit 1 fi REGISTRY_BASE="$(git config ship.registryBase 2>/dev/null)" || true if [[ -z "$REGISTRY_BASE" ]]; then o_lc="$(echo "$REMOTE_OWNER" | tr '[:upper:]' '[:lower:]')" r_lc="$(echo "$REMOTE_REPO" | tr '[:upper:]' '[:lower:]')" REGISTRY_BASE="ghcr.io/${o_lc}/${r_lc}" fi REGISTRY_BASE="${REGISTRY_BASE%/}" TAG_SERVER="$(git config ship.tagServer 2>/dev/null)" || true TAG_SERVER="${TAG_SERVER:-production}" TAG_CLIENT="$(git config ship.tagClient 2>/dev/null)" || true TAG_CLIENT="${TAG_CLIENT:-client-production}" PLATFORMS="$(git config ship.platforms 2>/dev/null)" || true PLATFORMS="${PLATFORMS:-linux/amd64,linux/arm64}" BUILDER_OVERRIDE="$(git config ship.buildxBuilder 2>/dev/null)" || true echo "==> ship: remote=$GIT_REMOTE ($REMOTE_OWNER/$REMOTE_REPO)" echo "==> ship: registry=$REGISTRY_BASE tags=$TAG_SERVER $TAG_CLIENT" echo "==> ship: platforms=$PLATFORMS" echo "==> [1/6] git add -A" git add -A echo "==> [2/6] git commit" if git diff --cached --quiet; then git commit --allow-empty -m "$COMMIT_MSG" else git commit -m "$COMMIT_MSG" fi GIT_COMMIT="$(git rev-parse HEAD)" BA=(--build-arg "GIT_COMMIT=${GIT_COMMIT}") echo "==> GIT_COMMIT=${GIT_COMMIT}" echo "==> [3/6] docker login ghcr.io" if ! printf '%s' "$(gh auth token)" | docker login ghcr.io -u "$(gh api user -q .login)" --password-stdin; then echo "ship: fatal: docker login ghcr.io failed" >&2 exit 1 fi pick_builder() { if [[ -n "$BUILDER_OVERRIDE" ]]; then echo "$BUILDER_OVERRIDE" return fi if docker buildx inspect orbstack >/dev/null 2>&1; then echo "orbstack" else echo "default" fi } platform_slug() { local p="${1// /}" p="${p#linux/}" echo "${p//\//-}" } push_multi() { local target="$1" local final_tag="$2" local builder="$3" local base="${final_tag%:*}" local name="${final_tag##*:}" local arch_refs=() local p slug arch_tag IFS=',' read -ra plats <<< "$PLATFORMS" for p in "${plats[@]}"; do p="${p// /}" [[ -z "$p" ]] && continue slug="$(platform_slug "$p")" arch_tag="${base}:${name}-${slug}" echo "==> [4/6] docker buildx push ${target} ${p} -> ${arch_tag}" docker buildx build \ "${BA[@]}" \ --builder "$builder" \ --platform "$p" \ --target "$target" \ -t "$arch_tag" \ --push \ --provenance=false \ "$ROOT" arch_refs+=("$arch_tag") done echo "==> [5/6] imagetools manifest ${final_tag}" docker buildx imagetools create -t "$final_tag" "${arch_refs[@]}" } BUILDER="$(pick_builder)" echo "==> buildx builder: ${BUILDER}" SERVER_TAG="${REGISTRY_BASE}:${TAG_SERVER}" CLIENT_TAG="${REGISTRY_BASE}:${TAG_CLIENT}" push_multi server "$SERVER_TAG" "$BUILDER" push_multi client "$CLIENT_TAG" "$BUILDER" echo "==> [6/6] git push ${GIT_REMOTE} ${BRANCH}" git push "$GIT_REMOTE" "$BRANCH" echo "ship: ok commit=${GIT_COMMIT}" echo "ship: images ${SERVER_TAG} ${CLIENT_TAG}"