98f73c5bea
Introduces Aliyun OSS as an alternative to MinIO; deploy scripts and CI workflows now read object_storage.provider from config and conditionally include or skip MinIO resources in both Docker Compose and k3s paths. Also adds ops scheduler domain and its migration tables. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
787 lines
28 KiB
YAML
787 lines
28 KiB
YAML
name: Deploy NAS Compose Service
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
service:
|
|
description: "Service or group to update"
|
|
required: true
|
|
default: "frontend"
|
|
type: choice
|
|
options:
|
|
- frontend
|
|
- ops-web
|
|
- browser-fetch
|
|
- tenant-api
|
|
- ops-api
|
|
- worker-generate
|
|
- kol-assist-worker
|
|
- scheduler
|
|
- web
|
|
- backend
|
|
- all-apps
|
|
run_migrations:
|
|
description: "Run database migrations before restart"
|
|
required: true
|
|
default: "auto"
|
|
type: choice
|
|
options:
|
|
- auto
|
|
- yes
|
|
- no
|
|
validate_migrations:
|
|
description: "Validate migrations on an empty Postgres first when migrations run"
|
|
required: true
|
|
default: "true"
|
|
type: choice
|
|
options:
|
|
- "true"
|
|
- "false"
|
|
host:
|
|
description: "NAS SSH host"
|
|
required: true
|
|
default: "192.168.100.19"
|
|
port:
|
|
description: "NAS SSH port"
|
|
required: true
|
|
default: "22"
|
|
user:
|
|
description: "NAS SSH user"
|
|
required: true
|
|
default: "admin"
|
|
password_source:
|
|
description: "NAS SSH password source"
|
|
required: true
|
|
default: "repository_secret"
|
|
type: choice
|
|
options:
|
|
- repository_secret
|
|
- workflow_input
|
|
ssh_password:
|
|
description: "NAS SSH password. Used only when password_source is workflow_input."
|
|
required: false
|
|
default: ""
|
|
deploy_base:
|
|
description: "Base deployment directory on the NAS"
|
|
required: true
|
|
default: "/vol1/1000/application/geo"
|
|
registry_host:
|
|
description: "Gitea Registry host, without scheme"
|
|
required: true
|
|
default: "192.168.100.19:13000"
|
|
registry_owner:
|
|
description: "Gitea package owner"
|
|
required: true
|
|
default: "root"
|
|
registry_image_prefix:
|
|
description: "Image package prefix. Images become <prefix>/<service>."
|
|
required: true
|
|
default: "geo-rankly"
|
|
registry_username:
|
|
description: "Gitea Registry username"
|
|
required: true
|
|
default: "root"
|
|
registry_password_source:
|
|
description: "Gitea Registry password source"
|
|
required: true
|
|
default: "repository_secret"
|
|
type: choice
|
|
options:
|
|
- repository_secret
|
|
- workflow_input
|
|
registry_password:
|
|
description: "Gitea Registry password/token. Used only when registry_password_source is workflow_input."
|
|
required: false
|
|
default: ""
|
|
registry_plain_http:
|
|
description: "Registry uses plain HTTP and must be configured as an insecure registry."
|
|
required: true
|
|
default: "true"
|
|
type: choice
|
|
options:
|
|
- "true"
|
|
- "false"
|
|
image_tag:
|
|
description: "Docker image tag. Defaults to the pushed commit SHA first 8 chars."
|
|
required: false
|
|
default: ""
|
|
keep_releases:
|
|
description: "Number of old release directories to keep. 0 keeps all releases."
|
|
required: false
|
|
default: "0"
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
deploy-service:
|
|
name: Deploy selected service to NAS
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 180
|
|
env:
|
|
NAS_HOST: ${{ inputs.host }}
|
|
NAS_PORT: ${{ inputs.port }}
|
|
NAS_USER: ${{ inputs.user }}
|
|
NAS_SSH_PASSWORD_SOURCE: ${{ inputs.password_source }}
|
|
NAS_SSH_PASSWORD_INPUT: ${{ inputs.ssh_password }}
|
|
NAS_SSH_PASSWORD_SECRET: ${{ secrets.NAS_SSH_PASSWORD }}
|
|
DEPLOY_BASE: ${{ inputs.deploy_base }}
|
|
REGISTRY_HOST: ${{ inputs.registry_host }}
|
|
REGISTRY_OWNER: ${{ inputs.registry_owner }}
|
|
REGISTRY_IMAGE_PREFIX: ${{ inputs.registry_image_prefix }}
|
|
REGISTRY_USERNAME: ${{ inputs.registry_username }}
|
|
REGISTRY_PASSWORD_SOURCE: ${{ inputs.registry_password_source }}
|
|
REGISTRY_PASSWORD_INPUT: ${{ inputs.registry_password }}
|
|
REGISTRY_PASSWORD_SECRET: ${{ secrets.REGISTRY_PASSWORD }}
|
|
REGISTRY_PLAIN_HTTP: ${{ inputs.registry_plain_http }}
|
|
SELECTED_SERVICE: ${{ inputs.service }}
|
|
REQUESTED_IMAGE_TAG: ${{ inputs.image_tag }}
|
|
REQUESTED_RUN_MIGRATIONS: ${{ inputs.run_migrations }}
|
|
VALIDATE_MIGRATIONS: ${{ inputs.validate_migrations }}
|
|
KEEP_RELEASES: ${{ inputs.keep_releases }}
|
|
NAS_DEPLOY_ENV: ${{ secrets.NAS_DEPLOY_ENV }}
|
|
DOCKER_BUILDKIT: "1"
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Install SSH tools
|
|
run: |
|
|
apt-get update
|
|
apt-get install -y curl openssh-client sshpass tar
|
|
|
|
- name: Resolve deployment metadata
|
|
run: |
|
|
set -eu
|
|
|
|
case "${NAS_SSH_PASSWORD_SOURCE:-repository_secret}" in
|
|
repository_secret)
|
|
nas_ssh_password="${NAS_SSH_PASSWORD_SECRET:-}"
|
|
;;
|
|
workflow_input)
|
|
nas_ssh_password="${NAS_SSH_PASSWORD_INPUT:-}"
|
|
;;
|
|
*)
|
|
echo "::error::password_source must be repository_secret or workflow_input"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if [ -n "${nas_ssh_password}" ]; then
|
|
echo "::add-mask::${nas_ssh_password}"
|
|
fi
|
|
|
|
{
|
|
echo "NAS_SSH_PASSWORD<<__NAS_SSH_PASSWORD__"
|
|
printf '%s\n' "${nas_ssh_password}"
|
|
echo "__NAS_SSH_PASSWORD__"
|
|
echo "NAS_SSH_PASSWORD_INPUT="
|
|
echo "NAS_SSH_PASSWORD_SECRET="
|
|
} >> "${GITHUB_ENV}"
|
|
|
|
if [ -z "${nas_ssh_password}" ]; then
|
|
if [ "${NAS_SSH_PASSWORD_SOURCE:-repository_secret}" = "workflow_input" ]; then
|
|
echo "::error::Missing workflow input ssh_password"
|
|
else
|
|
echo "::error::Missing repository secret NAS_SSH_PASSWORD"
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
case "${REGISTRY_PASSWORD_SOURCE:-repository_secret}" in
|
|
repository_secret)
|
|
registry_password="${REGISTRY_PASSWORD_SECRET:-}"
|
|
;;
|
|
workflow_input)
|
|
registry_password="${REGISTRY_PASSWORD_INPUT:-}"
|
|
;;
|
|
*)
|
|
echo "::error::registry_password_source must be repository_secret or workflow_input"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
if [ -n "${registry_password}" ]; then
|
|
echo "::add-mask::${registry_password}"
|
|
fi
|
|
|
|
{
|
|
echo "REGISTRY_PASSWORD<<__REGISTRY_PASSWORD__"
|
|
printf '%s\n' "${registry_password}"
|
|
echo "__REGISTRY_PASSWORD__"
|
|
echo "REGISTRY_PASSWORD_INPUT="
|
|
echo "REGISTRY_PASSWORD_SECRET="
|
|
} >> "${GITHUB_ENV}"
|
|
|
|
if [ -z "${registry_password}" ]; then
|
|
if [ "${REGISTRY_PASSWORD_SOURCE:-repository_secret}" = "workflow_input" ]; then
|
|
echo "::error::Missing workflow input registry_password"
|
|
else
|
|
echo "::error::Missing repository secret REGISTRY_PASSWORD"
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
REGISTRY_HOST="${REGISTRY_HOST#http://}"
|
|
REGISTRY_HOST="${REGISTRY_HOST#https://}"
|
|
REGISTRY_HOST="${REGISTRY_HOST%/}"
|
|
|
|
case "${NAS_PORT}" in
|
|
*[!0-9]*|"")
|
|
echo "::error::NAS SSH port must be numeric"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "${DEPLOY_BASE}" in
|
|
/*) ;;
|
|
*)
|
|
echo "::error::deploy_base must be an absolute path"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "${DEPLOY_BASE}" in
|
|
*[!A-Za-z0-9_./-]*)
|
|
echo "::error::deploy_base contains unsupported characters"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "${KEEP_RELEASES}" in
|
|
*[!0-9]*|"") KEEP_RELEASES=0 ;;
|
|
esac
|
|
|
|
case "${REGISTRY_HOST}" in
|
|
*[!A-Za-z0-9_.:-]*|"")
|
|
echo "::error::registry_host must be a host[:port] without scheme"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "${REGISTRY_OWNER}" in
|
|
*[!A-Za-z0-9_.-]*|"")
|
|
echo "::error::registry_owner contains unsupported characters"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "${REGISTRY_IMAGE_PREFIX}" in
|
|
*[!A-Za-z0-9_.-]*|"")
|
|
echo "::error::registry_image_prefix contains unsupported characters"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "${REGISTRY_USERNAME}" in
|
|
*[!A-Za-z0-9_.@-]*|"")
|
|
echo "::error::registry_username contains unsupported characters"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "${REGISTRY_PLAIN_HTTP}" in
|
|
true|false) ;;
|
|
*)
|
|
echo "::error::registry_plain_http must be true or false"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
auto_migrations=false
|
|
case "${SELECTED_SERVICE}" in
|
|
frontend)
|
|
service_list="frontend"
|
|
;;
|
|
ops-web)
|
|
service_list="ops-web"
|
|
;;
|
|
browser-fetch)
|
|
service_list="browser-fetch"
|
|
;;
|
|
tenant-api)
|
|
service_list="tenant-api"
|
|
auto_migrations=true
|
|
;;
|
|
ops-api)
|
|
service_list="ops-api"
|
|
auto_migrations=true
|
|
;;
|
|
worker-generate)
|
|
service_list="worker-generate"
|
|
auto_migrations=true
|
|
;;
|
|
kol-assist-worker)
|
|
service_list="kol-assist-worker"
|
|
auto_migrations=true
|
|
;;
|
|
scheduler)
|
|
service_list="scheduler"
|
|
auto_migrations=true
|
|
;;
|
|
web)
|
|
service_list="frontend ops-web"
|
|
;;
|
|
backend)
|
|
service_list="tenant-api browser-fetch ops-api worker-generate kol-assist-worker scheduler"
|
|
auto_migrations=true
|
|
;;
|
|
all-apps)
|
|
service_list="tenant-api browser-fetch ops-api worker-generate kol-assist-worker scheduler frontend ops-web"
|
|
auto_migrations=true
|
|
;;
|
|
*)
|
|
echo "::error::Unsupported service: ${SELECTED_SERVICE}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "${REQUESTED_RUN_MIGRATIONS}" in
|
|
auto)
|
|
run_migrations="${auto_migrations}"
|
|
;;
|
|
yes)
|
|
run_migrations=true
|
|
;;
|
|
no)
|
|
run_migrations=false
|
|
;;
|
|
*)
|
|
echo "::error::run_migrations must be auto, yes, or no"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
case "${VALIDATE_MIGRATIONS}" in
|
|
true|false) ;;
|
|
*)
|
|
echo "::error::validate_migrations must be true or false"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
image_tag="${REQUESTED_IMAGE_TAG}"
|
|
if [ -z "${image_tag}" ]; then
|
|
image_tag="$(git rev-parse --short=8 HEAD)"
|
|
fi
|
|
|
|
case "${image_tag}" in
|
|
*[!A-Za-z0-9_.-]*|"")
|
|
echo "::error::image_tag may only contain letters, numbers, underscore, dot, and dash"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
service_slug="$(printf '%s' "${SELECTED_SERVICE}" | tr -c 'A-Za-z0-9_.-' '-')"
|
|
release_id="$(date -u +%Y%m%d%H%M%S)-$(git rev-parse --short=12 HEAD)-${service_slug}"
|
|
|
|
{
|
|
echo "REGISTRY_HOST=${REGISTRY_HOST}"
|
|
echo "IMAGE_TAG=${image_tag}"
|
|
echo "RELEASE_ID=${release_id}"
|
|
echo "SERVICE_LIST=${service_list}"
|
|
echo "RUN_MIGRATIONS=${run_migrations}"
|
|
echo "KEEP_RELEASES=${KEEP_RELEASES}"
|
|
} >> "${GITHUB_ENV}"
|
|
|
|
echo "Selected service: ${SELECTED_SERVICE}"
|
|
echo "Compose services: ${service_list}"
|
|
echo "Run migrations: ${run_migrations}"
|
|
echo "Validate migrations: ${VALIDATE_MIGRATIONS}"
|
|
echo "Image tag: ${image_tag}"
|
|
echo "Release ID: ${release_id}"
|
|
echo "Deploy base: ${DEPLOY_BASE}"
|
|
echo "Gitea Registry: ${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}/<service>:${image_tag}"
|
|
|
|
- name: Verify current images in Gitea Registry
|
|
run: |
|
|
set -eu
|
|
|
|
image_list="${SERVICE_LIST}"
|
|
if [ "${RUN_MIGRATIONS}" = "true" ]; then
|
|
case " ${image_list} " in
|
|
*" migrate "*) ;;
|
|
*) image_list="migrate ${image_list}" ;;
|
|
esac
|
|
fi
|
|
|
|
IMAGE_LIST="${image_list}" bash deploy/scripts/gitea-registry-images.sh verify || {
|
|
echo "::error::Missing image(s) for commit ${IMAGE_TAG}. CI must publish images before CD."
|
|
echo "::error::Frontend images come from Frontend CI; backend/migrate images come from Backend CI."
|
|
exit 1
|
|
}
|
|
|
|
- name: Prepare SSH known hosts
|
|
run: |
|
|
set -eu
|
|
mkdir -p ~/.ssh
|
|
chmod 700 ~/.ssh
|
|
ssh-keyscan -p "${NAS_PORT}" "${NAS_HOST}" >> ~/.ssh/known_hosts
|
|
|
|
- name: Validate migrations on empty Postgres
|
|
run: |
|
|
set -eu
|
|
|
|
if [ "${RUN_MIGRATIONS}" != "true" ] || [ "${VALIDATE_MIGRATIONS}" != "true" ]; then
|
|
echo "Skipping migration preflight."
|
|
exit 0
|
|
fi
|
|
|
|
test_name="geo-service-migration-preflight-${GITHUB_RUN_ID:-manual}-${GITHUB_RUN_NUMBER:-0}"
|
|
network_name="${test_name}-net"
|
|
postgres_name="${test_name}-postgres"
|
|
migrate_image="${test_name}-migrate"
|
|
|
|
cleanup() {
|
|
docker rm -f "${postgres_name}" >/dev/null 2>&1 || true
|
|
docker network rm "${network_name}" >/dev/null 2>&1 || true
|
|
docker image rm "${migrate_image}" >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
docker network create "${network_name}" >/dev/null
|
|
docker run -d \
|
|
--name "${postgres_name}" \
|
|
--network "${network_name}" \
|
|
-e POSTGRES_DB=geo \
|
|
-e POSTGRES_USER=geo \
|
|
-e POSTGRES_PASSWORD=geo_dev \
|
|
postgres:16-alpine >/dev/null
|
|
|
|
echo "Waiting for empty Postgres..."
|
|
for i in $(seq 1 60); do
|
|
if docker exec "${postgres_name}" pg_isready -U geo -d geo >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
if [ "${i}" -eq 60 ]; then
|
|
docker logs "${postgres_name}" || true
|
|
echo "::error::Postgres did not become ready"
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
docker exec "${postgres_name}" createdb -U geo geo_monitoring
|
|
|
|
printf '%s\n' "${REGISTRY_PASSWORD}" \
|
|
| docker login "${REGISTRY_HOST}" -u "${REGISTRY_USERNAME}" --password-stdin
|
|
|
|
echo "Pulling migration image..."
|
|
docker pull "${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}/migrate:${IMAGE_TAG}"
|
|
docker tag "${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}/migrate:${IMAGE_TAG}" "${migrate_image}"
|
|
|
|
database_url="postgres://geo:geo_dev@${postgres_name}:5432/geo?sslmode=disable"
|
|
monitoring_database_url="postgres://geo:geo_dev@${postgres_name}:5432/geo_monitoring?sslmode=disable"
|
|
ops_database_url="postgres://geo:geo_dev@${postgres_name}:5432/geo?sslmode=disable&x-migrations-table=schema_migrations_ops"
|
|
|
|
echo "Running tenant migrations on an empty database..."
|
|
docker run --rm --network "${network_name}" "${migrate_image}" \
|
|
-path /migrations \
|
|
-database "${database_url}" \
|
|
up
|
|
|
|
echo "Running monitoring migrations on an empty database..."
|
|
docker run --rm --network "${network_name}" "${migrate_image}" \
|
|
-path /migrations_monitoring \
|
|
-database "${monitoring_database_url}" \
|
|
up
|
|
|
|
echo "Running ops migrations on an empty database..."
|
|
docker run --rm --network "${network_name}" "${migrate_image}" \
|
|
-path /migrations_ops \
|
|
-database "${ops_database_url}" \
|
|
up
|
|
|
|
echo "Migration preflight passed."
|
|
|
|
- name: Upload release to NAS
|
|
run: |
|
|
set -eu
|
|
|
|
remote="${NAS_USER}@${NAS_HOST}"
|
|
ssh_opts="-p ${NAS_PORT} -o BatchMode=no -o StrictHostKeyChecking=yes"
|
|
release_dir="${DEPLOY_BASE}/releases/${RELEASE_ID}"
|
|
|
|
SSHPASS="${NAS_SSH_PASSWORD}" sshpass -e ssh ${ssh_opts} "${remote}" \
|
|
"mkdir -p '${release_dir}' '${DEPLOY_BASE}/shared'"
|
|
|
|
git archive --format=tar.gz HEAD \
|
|
| SSHPASS="${NAS_SSH_PASSWORD}" sshpass -e ssh ${ssh_opts} "${remote}" \
|
|
"tar -xzf - -C '${release_dir}'"
|
|
|
|
- name: Upload deploy env when provided
|
|
run: |
|
|
set -eu
|
|
|
|
if [ -z "${NAS_DEPLOY_ENV:-}" ]; then
|
|
echo "NAS_DEPLOY_ENV is empty; keeping existing NAS .env or creating from .env.example during deploy."
|
|
exit 0
|
|
fi
|
|
|
|
remote="${NAS_USER}@${NAS_HOST}"
|
|
ssh_opts="-p ${NAS_PORT} -o BatchMode=no -o StrictHostKeyChecking=yes"
|
|
|
|
printf '%s\n' "${NAS_DEPLOY_ENV}" \
|
|
| SSHPASS="${NAS_SSH_PASSWORD}" sshpass -e ssh ${ssh_opts} "${remote}" \
|
|
"cat > '${DEPLOY_BASE}/shared/.env' && chmod 600 '${DEPLOY_BASE}/shared/.env'"
|
|
|
|
- name: Deploy selected service with Docker Compose
|
|
run: |
|
|
set -eu
|
|
|
|
remote="${NAS_USER}@${NAS_HOST}"
|
|
ssh_opts="-p ${NAS_PORT} -o BatchMode=no -o StrictHostKeyChecking=yes"
|
|
sudo_password_b64="$(printf '%s' "${NAS_SSH_PASSWORD}" | base64 | tr -d '\n')"
|
|
registry_password_b64="$(printf '%s' "${REGISTRY_PASSWORD}" | base64 | tr -d '\n')"
|
|
|
|
{
|
|
printf 'SUDO_PASSWORD_B64=%s\n' "${sudo_password_b64}"
|
|
printf 'REGISTRY_PASSWORD_B64=%s\n' "${registry_password_b64}"
|
|
cat <<'REMOTE_SCRIPT'
|
|
set -eu
|
|
|
|
release_dir="${DEPLOY_BASE}/releases/${RELEASE_ID}"
|
|
shared_dir="${DEPLOY_BASE}/shared"
|
|
lock_dir="${DEPLOY_BASE}/.deploy.lock"
|
|
|
|
if ! mkdir "${lock_dir}" 2>/dev/null; then
|
|
echo "Another deployment is already running: ${lock_dir}"
|
|
exit 1
|
|
fi
|
|
trap 'rmdir "${lock_dir}"' EXIT INT TERM
|
|
|
|
if [ ! -f "${shared_dir}/.env" ]; then
|
|
cp "${release_dir}/deploy/.env.example" "${shared_dir}/.env"
|
|
chmod 600 "${shared_dir}/.env"
|
|
echo "Created ${shared_dir}/.env from .env.example. Edit it on the NAS before production traffic if secrets are still placeholders."
|
|
fi
|
|
|
|
SUDO_PASSWORD="$(printf '%s' "${SUDO_PASSWORD_B64}" | base64 -d 2>/dev/null || true)"
|
|
REGISTRY_PASSWORD="$(printf '%s' "${REGISTRY_PASSWORD_B64}" | base64 -d 2>/dev/null || true)"
|
|
unset SUDO_PASSWORD_B64 REGISTRY_PASSWORD_B64
|
|
|
|
docker_cmd() {
|
|
if [ "${DOCKER_MODE}" = "sudo" ]; then
|
|
printf '%s\n' "${SUDO_PASSWORD}" | sudo -S -p "" docker "$@"
|
|
else
|
|
docker "$@"
|
|
fi
|
|
}
|
|
|
|
compose_cmd() {
|
|
docker_cmd compose "$@"
|
|
}
|
|
|
|
ensure_config_aliases() {
|
|
for yaml in config.yaml config.local.yaml ops-config.yaml ops-config.local.yaml; do
|
|
yml="${yaml%.yaml}.yml"
|
|
if [ ! -f "${release_dir}/deploy/${yaml}" ] && [ -f "${release_dir}/deploy/${yml}" ]; then
|
|
cp "${release_dir}/deploy/${yml}" "${release_dir}/deploy/${yaml}"
|
|
echo "Created ${yaml} from ${yml} for deployment mounts."
|
|
fi
|
|
done
|
|
if [ ! -f "${release_dir}/deploy/prompts.yml" ] && [ -f "${release_dir}/deploy/prompts.yaml" ]; then
|
|
cp "${release_dir}/deploy/prompts.yaml" "${release_dir}/deploy/prompts.yml"
|
|
echo "Created prompts.yml from prompts.yaml for deployment mounts."
|
|
fi
|
|
}
|
|
|
|
config_file_path() {
|
|
base="$1"
|
|
for file in "${release_dir}/deploy/${base}.yaml" "${release_dir}/deploy/${base}.yml"; do
|
|
if [ -f "${file}" ]; then
|
|
printf '%s' "${file}"
|
|
return 0
|
|
fi
|
|
done
|
|
}
|
|
|
|
read_object_storage_provider() {
|
|
provider="minio"
|
|
for file in "$(config_file_path config)" "$(config_file_path config.local)"; do
|
|
if [ -z "${file}" ] || [ ! -f "${file}" ]; then
|
|
continue
|
|
fi
|
|
next="$(
|
|
awk '
|
|
/^[[:space:]]*object_storage:[[:space:]]*$/ {
|
|
in_section=1
|
|
base=match($0, /[^[:space:]]/) - 1
|
|
next
|
|
}
|
|
in_section {
|
|
indent=match($0, /[^[:space:]]/) - 1
|
|
if ($0 ~ /^[[:space:]]*$/ || $0 ~ /^[[:space:]]*#/) {
|
|
next
|
|
}
|
|
if (indent <= base) {
|
|
in_section=0
|
|
next
|
|
}
|
|
if ($0 ~ /^[[:space:]]*provider[[:space:]]*:/) {
|
|
value=$0
|
|
sub(/^[[:space:]]*provider[[:space:]]*:[[:space:]]*/, "", value)
|
|
sub(/[[:space:]]+#.*$/, "", value)
|
|
gsub(/^["'\'']|["'\'']$/, "", value)
|
|
print value
|
|
}
|
|
}
|
|
' "${file}" | tail -1
|
|
)"
|
|
if [ -n "${next}" ]; then
|
|
provider="${next}"
|
|
fi
|
|
done
|
|
printf '%s' "$(printf '%s' "${provider}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
|
|
}
|
|
|
|
use_minio_for_object_storage() {
|
|
case "$(read_object_storage_provider)" in
|
|
""|minio|mino)
|
|
return 0
|
|
;;
|
|
aliyun|aliyun_oss|aliyun-oss|oss)
|
|
return 1
|
|
;;
|
|
*)
|
|
return 0
|
|
;;
|
|
esac
|
|
}
|
|
|
|
compose_files() {
|
|
printf '%s\n' -f "${release_dir}/deploy/docker-compose.yaml"
|
|
if ! use_minio_for_object_storage; then
|
|
printf '%s\n' -f "${release_dir}/deploy/docker-compose.aliyun-oss.yaml"
|
|
fi
|
|
}
|
|
|
|
docker_login() {
|
|
if [ "${DOCKER_MODE}" = "sudo" ]; then
|
|
printf '%s\n' "${SUDO_PASSWORD}" | sudo -S -p "" -v
|
|
printf '%s\n' "${REGISTRY_PASSWORD}" \
|
|
| sudo -p "" docker login "${REGISTRY_HOST}" -u "${REGISTRY_USERNAME}" --password-stdin
|
|
else
|
|
printf '%s\n' "${REGISTRY_PASSWORD}" \
|
|
| docker login "${REGISTRY_HOST}" -u "${REGISTRY_USERNAME}" --password-stdin
|
|
fi
|
|
}
|
|
|
|
if docker info >/dev/null 2>&1; then
|
|
DOCKER_MODE="direct"
|
|
elif command -v sudo >/dev/null 2>&1 && [ -n "${SUDO_PASSWORD}" ] && printf '%s\n' "${SUDO_PASSWORD}" | sudo -S -p "" docker info >/dev/null 2>&1; then
|
|
DOCKER_MODE="sudo"
|
|
else
|
|
echo "Cannot access Docker daemon as $(id -un). Add the user to the docker group or allow sudo docker."
|
|
exit 1
|
|
fi
|
|
|
|
echo "Docker access mode: ${DOCKER_MODE}"
|
|
compose_cmd version
|
|
|
|
if [ "${REGISTRY_PLAIN_HTTP}" = "true" ] \
|
|
&& ! docker_cmd info 2>/dev/null \
|
|
| sed -n '/Insecure Registries:/,/Live Restore/p' \
|
|
| grep -Fq "${REGISTRY_HOST}"; then
|
|
echo "${REGISTRY_HOST} is not listed in Docker insecure registries on the NAS."
|
|
echo "Configure Docker daemon.json with: {\"insecure-registries\":[\"${REGISTRY_HOST}\"]}"
|
|
exit 1
|
|
fi
|
|
|
|
docker_login
|
|
ensure_config_aliases
|
|
provider="$(read_object_storage_provider)"
|
|
if use_minio_for_object_storage; then
|
|
echo "object_storage.provider=${provider}; deploying MinIO."
|
|
else
|
|
echo "object_storage.provider=${provider}; skipping MinIO."
|
|
export COMPOSE_PROFILES=""
|
|
docker_cmd rm -f geo-minio geo-minio-init >/dev/null 2>&1 || true
|
|
fi
|
|
|
|
IMAGE_REGISTRY="${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}" \
|
|
IMAGE_TAG="${IMAGE_TAG}" compose_cmd \
|
|
--env-file "${shared_dir}/.env" \
|
|
$(compose_files) \
|
|
config >/tmp/geo-rankly-compose-service.yaml
|
|
|
|
if [ "${RUN_MIGRATIONS}" = "true" ]; then
|
|
echo "Running database migrations..."
|
|
IMAGE_REGISTRY="${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}" \
|
|
IMAGE_TAG="${IMAGE_TAG}" compose_cmd \
|
|
--env-file "${shared_dir}/.env" \
|
|
$(compose_files) \
|
|
rm -f -s migrate >/dev/null 2>&1 || true
|
|
|
|
IMAGE_REGISTRY="${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}" \
|
|
IMAGE_TAG="${IMAGE_TAG}" compose_cmd \
|
|
--env-file "${shared_dir}/.env" \
|
|
$(compose_files) \
|
|
pull migrate
|
|
|
|
IMAGE_REGISTRY="${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}" \
|
|
IMAGE_TAG="${IMAGE_TAG}" compose_cmd \
|
|
--env-file "${shared_dir}/.env" \
|
|
$(compose_files) \
|
|
up --no-build --force-recreate migrate
|
|
fi
|
|
|
|
echo "Pulling selected service image(s): ${SERVICE_LIST}"
|
|
IMAGE_REGISTRY="${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}" \
|
|
IMAGE_TAG="${IMAGE_TAG}" compose_cmd \
|
|
--env-file "${shared_dir}/.env" \
|
|
$(compose_files) \
|
|
pull ${SERVICE_LIST}
|
|
|
|
echo "Restarting selected service(s): ${SERVICE_LIST}"
|
|
IMAGE_REGISTRY="${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}" \
|
|
IMAGE_TAG="${IMAGE_TAG}" compose_cmd \
|
|
--env-file "${shared_dir}/.env" \
|
|
$(compose_files) \
|
|
up -d --no-build --no-deps --force-recreate ${SERVICE_LIST}
|
|
|
|
ln -sfn "${release_dir}" "${DEPLOY_BASE}/current"
|
|
|
|
IMAGE_REGISTRY="${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}" \
|
|
IMAGE_TAG="${IMAGE_TAG}" compose_cmd \
|
|
--env-file "${shared_dir}/.env" \
|
|
$(compose_files) \
|
|
ps ${SERVICE_LIST}
|
|
|
|
if [ "${KEEP_RELEASES}" -gt 0 ] 2>/dev/null; then
|
|
find "${DEPLOY_BASE}/releases" -mindepth 1 -maxdepth 1 -type d \
|
|
| sort -r \
|
|
| awk "NR>${KEEP_RELEASES}" \
|
|
| xargs -r rm -rf
|
|
else
|
|
echo "Release cleanup disabled for component deployment."
|
|
fi
|
|
REMOTE_SCRIPT
|
|
} | SSHPASS="${NAS_SSH_PASSWORD}" sshpass -e ssh ${ssh_opts} "${remote}" \
|
|
"DEPLOY_BASE='${DEPLOY_BASE}' RELEASE_ID='${RELEASE_ID}' IMAGE_TAG='${IMAGE_TAG}' SERVICE_LIST='${SERVICE_LIST}' RUN_MIGRATIONS='${RUN_MIGRATIONS}' KEEP_RELEASES='${KEEP_RELEASES}' REGISTRY_HOST='${REGISTRY_HOST}' REGISTRY_OWNER='${REGISTRY_OWNER}' REGISTRY_IMAGE_PREFIX='${REGISTRY_IMAGE_PREFIX}' REGISTRY_USERNAME='${REGISTRY_USERNAME}' REGISTRY_PLAIN_HTTP='${REGISTRY_PLAIN_HTTP}' sh -se"
|
|
|
|
- name: Deployment summary
|
|
run: |
|
|
summary_file="${GITHUB_STEP_SUMMARY:-/tmp/nas-compose-service-deploy-summary.md}"
|
|
{
|
|
echo "### NAS selected-service deployment"
|
|
echo ""
|
|
echo "- Host: ${NAS_HOST}:${NAS_PORT}"
|
|
echo "- Deploy base: ${DEPLOY_BASE}"
|
|
echo "- Release: ${RELEASE_ID}"
|
|
echo "- Image tag: ${IMAGE_TAG}"
|
|
echo "- Gitea Registry: ${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}/<service>:${IMAGE_TAG}"
|
|
echo "- Selected input: ${SELECTED_SERVICE}"
|
|
echo "- Compose service(s): ${SERVICE_LIST}"
|
|
echo "- Run migrations: ${RUN_MIGRATIONS}"
|
|
echo ""
|
|
echo "Useful NAS commands:"
|
|
echo ""
|
|
echo '```sh'
|
|
echo "cd ${DEPLOY_BASE}/current/deploy"
|
|
echo "docker compose --env-file ${DEPLOY_BASE}/shared/.env ps ${SERVICE_LIST}"
|
|
echo "docker compose --env-file ${DEPLOY_BASE}/shared/.env logs -f ${SERVICE_LIST}"
|
|
echo '```'
|
|
} >> "${summary_file}"
|