Add reusable deployment helper
All checks were successful
Publish container image / Build and push image (push) Successful in 2s
All checks were successful
Publish container image / Build and push image (push) Successful in 2s
This commit is contained in:
49
README.md
49
README.md
@@ -71,6 +71,55 @@ docker compose up -d --build
|
|||||||
|
|
||||||
Then open `http://<host>:8091`.
|
Then open `http://<host>:8091`.
|
||||||
|
|
||||||
|
## Deployment helper for published images
|
||||||
|
|
||||||
|
For host deployments on this machine, use `deploy/deploy.sh` instead of editing
|
||||||
|
compose files by hand. The script is meant to be copied into other repos too: it
|
||||||
|
infers the repo name from `origin`, looks up the numeric Gitea repo id via the
|
||||||
|
API, computes the host port as `5000 + 10*repo_id + service_number`, copies the
|
||||||
|
repo's deployment assets, and writes a ready-to-run deployment directory.
|
||||||
|
|
||||||
|
For Arcana FX the Gitea repo id is `4`, so the default deployment port is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
5000 + 10*4 + 0 = 5040
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate a deployment directory for a tagged image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/deploy.sh v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
That creates `~/deployments/arcana-fx-0/` containing:
|
||||||
|
|
||||||
|
- `docker-compose.yml` pinned to `192.168.33.22:3300/gemma/arcana-fx:v0.1.0`
|
||||||
|
- `.env` copied from `.env.example` on first run
|
||||||
|
- `config/` copied from the repo via `deploy/include-paths.txt`
|
||||||
|
- `.deploy.meta` with the resolved repo id, version, and host port
|
||||||
|
|
||||||
|
Then edit the generated `.env` and start it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/deployments/arcana-fx-0
|
||||||
|
$EDITOR .env
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
A second service instance can use another service number, which shifts the port
|
||||||
|
by 1 while keeping the same repo-id block:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/deploy.sh v0.1.0 1
|
||||||
|
# host port becomes 5041
|
||||||
|
```
|
||||||
|
|
||||||
|
When copying this deployment helper to another repo, update
|
||||||
|
`deploy/docker-compose.template.yml` for that app's internal port and mounts,
|
||||||
|
and `deploy/include-paths.txt` for any extra files/directories that must travel
|
||||||
|
with the generated compose file.
|
||||||
|
|
||||||
## Container publishing pipeline
|
## Container publishing pipeline
|
||||||
|
|
||||||
A Gitea Actions workflow lives at `.gitea/workflows/release-container.yml`.
|
A Gitea Actions workflow lives at `.gitea/workflows/release-container.yml`.
|
||||||
|
|||||||
214
deploy/deploy.sh
Executable file
214
deploy/deploy.sh
Executable file
@@ -0,0 +1,214 @@
|
|||||||
|
#!/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
|
||||||
23
deploy/docker-compose.template.yml
Normal file
23
deploy/docker-compose.template.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by deploy/deploy.sh from this repo's deployment template.
|
||||||
|
# Internal port and mounts are service-specific; host port and image tag are
|
||||||
|
# filled in by the deployment helper.
|
||||||
|
|
||||||
|
services:
|
||||||
|
__SERVICE_NAME__:
|
||||||
|
image: __IMAGE__:__IMAGE_TAG__
|
||||||
|
container_name: __SERVICE_NAME__-__SERVICE_NUMBER__
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
PORT: 8091
|
||||||
|
ports:
|
||||||
|
- "__HOST_PORT__:8091"
|
||||||
|
volumes:
|
||||||
|
- ./config:/app/config:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8091/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
3
deploy/include-paths.txt
Normal file
3
deploy/include-paths.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Repo-relative files/directories to copy alongside docker-compose.yml into the
|
||||||
|
# generated deployment directory.
|
||||||
|
config
|
||||||
Reference in New Issue
Block a user