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 /." 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}/:${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" lock_owned=false write_lock_metadata() { printf '%s\n' "$$" > "${lock_dir}/pid" printf '%s\n' "${RELEASE_ID}" > "${lock_dir}/release_id" printf '%s\n' "${IMAGE_TAG}" > "${lock_dir}/image_tag" printf '%s\n' "${SERVICE_LIST}" > "${lock_dir}/services" date -u '+%Y-%m-%dT%H:%M:%SZ' > "${lock_dir}/created_at" 2>/dev/null || true } print_lock_metadata() { echo " pid: $(cat "${lock_dir}/pid" 2>/dev/null || echo unknown)" echo " release: $(cat "${lock_dir}/release_id" 2>/dev/null || echo unknown)" echo " image tag: $(cat "${lock_dir}/image_tag" 2>/dev/null || echo unknown)" echo " services: $(cat "${lock_dir}/services" 2>/dev/null || echo unknown)" echo " created: $(cat "${lock_dir}/created_at" 2>/dev/null || echo unknown)" } acquire_deploy_lock() { if mkdir "${lock_dir}" 2>/dev/null; then lock_owned=true write_lock_metadata return 0 fi existing_pid="$(cat "${lock_dir}/pid" 2>/dev/null || true)" if [ -z "${existing_pid}" ]; then sleep 2 existing_pid="$(cat "${lock_dir}/pid" 2>/dev/null || true)" fi case "${existing_pid}" in ""|*[!0-9]*) echo "Another deployment may already be running: ${lock_dir}" print_lock_metadata echo "This lock has no PID metadata, so it may have been created by an older workflow." echo "After confirming no NAS Compose deployment is active, remove it with:" echo " rm -rf '${lock_dir}'" exit 1 ;; *) if kill -0 "${existing_pid}" 2>/dev/null; then echo "Another deployment is already running: ${lock_dir}" print_lock_metadata exit 1 fi ;; esac echo "Removing stale deployment lock: ${lock_dir}" print_lock_metadata rm -rf "${lock_dir}" if ! mkdir "${lock_dir}" 2>/dev/null; then echo "Another deployment is already running: ${lock_dir}" exit 1 fi lock_owned=true write_lock_metadata } release_deploy_lock() { if [ "${lock_owned}" = "true" ]; then owner_pid="$(cat "${lock_dir}/pid" 2>/dev/null || true)" if [ "${owner_pid}" = "$$" ]; then rm -rf "${lock_dir}" fi fi } acquire_deploy_lock trap 'release_deploy_lock' 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}/:${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}"