#!/usr/bin/env bash # deploy.sh — prepare a deployment directory for the current repository. # # Designed to be copied verbatim into other Gitea-hosted service repos on this # machine. Repo-specific values (service name, image path, project id) are # discovered from the `origin` remote and the Gitea API, so the only thing # baked into the convention is the host-port formula: # # host_port = 5000 + 10 * + # # Usage: # deploy/deploy.sh [service_number] # # Output: # /-/docker-compose.yml # /-/.deploy.meta # /-/.env (copied from .env.example # on first run if absent) # # Environment overrides: # DEPLOY_ROOT base directory (default: $HOME/deployments) # TEMPLATE path to compose template (default: # /deploy/docker-compose.template.yml) # GITEA_BASE_URL e.g. http://192.168.33.22:3300 # (default: http://:3300) # GITEA_API_BASE full API base (default: $GITEA_BASE_URL/api/v1) # GITEA_TOKEN token for private repos (optional) # GITEA_PROJECT_ID skip API discovery and use this id # REGISTRY container registry host[:port] # (default: :3300) # IMAGE_REPO full image path host[:port]/owner/repo # (default: $REGISTRY//) # INCLUDE_PATHS_FILE newline-delimited repo-relative paths to copy into the # deployment dir (default: /deploy/include-paths.txt) set -euo pipefail die() { printf 'deploy: %s\n' "$*" >&2; exit 1; } usage() { sed -n '2,/^set -euo pipefail$/p' "$0" \ | sed '$d' \ | sed 's/^# \{0,1\}//' exit 1 } [ "$#" -ge 1 ] || usage case "$1" in -h|--help|help) usage;; esac VERSION="$1" SERVICE_NUMBER="${2:-0}" case "$SERVICE_NUMBER" in ''|*[!0-9]*) die "service_number must be a non-negative integer: $SERVICE_NUMBER";; esac case "$VERSION" in ''|*[[:space:]]*) die "version must be non-empty and contain no whitespace";; esac SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]:-$0}")" && pwd)" REPO_DIR="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)" \ || die "must be run from inside a git working tree" TEMPLATE="${TEMPLATE:-$REPO_DIR/deploy/docker-compose.template.yml}" DEPLOY_ROOT="${DEPLOY_ROOT:-$HOME/deployments}" INCLUDE_PATHS_FILE="${INCLUDE_PATHS_FILE:-$REPO_DIR/deploy/include-paths.txt}" [ -r "$TEMPLATE" ] || die "template not found: $TEMPLATE" remote_url="$(git -C "$REPO_DIR" remote get-url origin)" \ || die "no 'origin' remote configured" REMOTE_HOST=""; REMOTE_OWNER=""; REMOTE_REPO="" parse_remote() { local url="$1" rest host_part path_part url="${url%.git}" case "$url" in ssh://*) rest="${url#ssh://}" rest="${rest#*@}" host_part="${rest%%/*}" path_part="${rest#*/}" REMOTE_HOST="${host_part%%:*}" ;; http://*|https://*) rest="${url#*://}" host_part="${rest%%/*}" path_part="${rest#*/}" REMOTE_HOST="${host_part%%:*}" ;; *@*:*) rest="${url#*@}" REMOTE_HOST="${rest%%:*}" path_part="${rest#*:}" ;; *) die "cannot parse git remote URL: $url" ;; esac REMOTE_OWNER="${path_part%/*}" REMOTE_REPO="${path_part##*/}" [ -n "$REMOTE_HOST" ] && [ -n "$REMOTE_OWNER" ] && [ -n "$REMOTE_REPO" ] \ || die "could not extract host/owner/repo from: $url" } parse_remote "$remote_url" GITEA_BASE_URL="${GITEA_BASE_URL:-http://${REMOTE_HOST}:3300}" GITEA_API_BASE="${GITEA_API_BASE:-$GITEA_BASE_URL/api/v1}" REGISTRY="${REGISTRY:-${REMOTE_HOST}:3300}" IMAGE_REPO="${IMAGE_REPO:-${REGISTRY}/${REMOTE_OWNER}/${REMOTE_REPO}}" if [ -z "${GITEA_PROJECT_ID:-}" ]; then api_url="${GITEA_API_BASE}/repos/${REMOTE_OWNER}/${REMOTE_REPO}" printf 'deploy: looking up project id at %s\n' "$api_url" >&2 curl_args=(-fsS --max-time 10) if [ -n "${GITEA_TOKEN:-}" ]; then curl_args+=(-H "Authorization: token ${GITEA_TOKEN}") fi if ! resp="$(curl "${curl_args[@]}" "$api_url")"; then die "Gitea API call failed: $api_url set GITEA_PROJECT_ID= to bypass discovery, or set GITEA_API_BASE / GITEA_TOKEN to override defaults." fi GITEA_PROJECT_ID="$(printf '%s' "$resp" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')" \ || die "could not extract repo id from Gitea response" fi case "$GITEA_PROJECT_ID" in ''|*[!0-9]*) die "GITEA_PROJECT_ID is not a non-negative integer: $GITEA_PROJECT_ID";; esac HOST_PORT=$(( 5000 + 10 * GITEA_PROJECT_ID + SERVICE_NUMBER )) deploy_dir="$DEPLOY_ROOT/${REMOTE_REPO}-${SERVICE_NUMBER}" mkdir -p "$deploy_dir" out_file="$deploy_dir/docker-compose.yml" if [ -r "$INCLUDE_PATHS_FILE" ]; then while IFS= read -r include_path || [ -n "$include_path" ]; do include_path="${include_path%%#*}" include_path="$(printf '%s' "$include_path" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" [ -n "$include_path" ] || continue src_path="$REPO_DIR/$include_path" dst_path="$deploy_dir/$include_path" [ -e "$src_path" ] || die "include path listed but missing: $include_path" mkdir -p "$(dirname "$dst_path")" rm -rf "$dst_path" cp -R "$src_path" "$dst_path" done < "$INCLUDE_PATHS_FILE" fi awk \ -v image="$IMAGE_REPO" \ -v tag="$VERSION" \ -v host_port="$HOST_PORT" \ -v service="$REMOTE_REPO" \ -v service_num="$SERVICE_NUMBER" \ -v project_id="$GITEA_PROJECT_ID" \ '{ gsub(/__IMAGE_TAG__/, tag); gsub(/__IMAGE__/, image); gsub(/__HOST_PORT__/, host_port); gsub(/__SERVICE_NAME__/, service); gsub(/__SERVICE_NUMBER__/, service_num); gsub(/__GITEA_PROJECT_ID__/, project_id); print; }' "$TEMPLATE" > "$out_file" # Sanity check: the template authors might leave unresolved tokens by mistake. if grep -q '__[A-Z_]\+__' "$out_file"; then printf 'deploy: WARNING: unresolved __TOKEN__ placeholders remain in %s\n' \ "$out_file" >&2 fi env_example="$REPO_DIR/.env.example" if [ ! -e "$deploy_dir/.env" ] && [ -r "$env_example" ]; then cp "$env_example" "$deploy_dir/.env" env_seeded=1 else env_seeded=0 fi cat > "$deploy_dir/.deploy.meta" <