All checks were successful
Publish container image / Build and push image (push) Successful in 2s
215 lines
6.9 KiB
Bash
Executable File
215 lines
6.9 KiB
Bash
Executable File
#!/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 * <gitea_project_id> + <service_number>
|
|
#
|
|
# Usage:
|
|
# deploy/deploy.sh <version> [service_number]
|
|
#
|
|
# Output:
|
|
# <DEPLOY_ROOT>/<repo>-<service_number>/docker-compose.yml
|
|
# <DEPLOY_ROOT>/<repo>-<service_number>/.deploy.meta
|
|
# <DEPLOY_ROOT>/<repo>-<service_number>/.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:
|
|
# <repo>/deploy/docker-compose.template.yml)
|
|
# GITEA_BASE_URL e.g. http://192.168.33.22:3300
|
|
# (default: http://<remote-host>: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: <remote-host>:3300)
|
|
# IMAGE_REPO full image path host[:port]/owner/repo
|
|
# (default: $REGISTRY/<owner>/<repo>)
|
|
# INCLUDE_PATHS_FILE newline-delimited repo-relative paths to copy into the
|
|
# deployment dir (default: <repo>/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=<n> 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" <<EOF
|
|
service=$REMOTE_REPO
|
|
service_number=$SERVICE_NUMBER
|
|
version=$VERSION
|
|
image=$IMAGE_REPO:$VERSION
|
|
host_port=$HOST_PORT
|
|
gitea_project_id=$GITEA_PROJECT_ID
|
|
gitea_owner=$REMOTE_OWNER
|
|
gitea_host=$REMOTE_HOST
|
|
include_paths_file=$(basename "$INCLUDE_PATHS_FILE")
|
|
generated_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
EOF
|
|
|
|
cat <<EOF
|
|
deploy: prepared $deploy_dir
|
|
service: $REMOTE_REPO (#${SERVICE_NUMBER})
|
|
image: $IMAGE_REPO:$VERSION
|
|
host port: $HOST_PORT (5000 + 10*${GITEA_PROJECT_ID} + ${SERVICE_NUMBER})
|
|
EOF
|
|
|
|
if [ "$env_seeded" = 1 ]; then
|
|
printf 'deploy: seeded .env from .env.example — EDIT IT before starting\n'
|
|
fi
|
|
|
|
cat <<EOF
|
|
|
|
Next steps:
|
|
cd "$deploy_dir"
|
|
\$EDITOR .env
|
|
docker compose pull
|
|
docker compose up -d
|
|
EOF
|