258 lines
8.4 KiB
YAML
258 lines
8.4 KiB
YAML
name: Deploy NAS Compose
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
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"
|
|
deploy_base:
|
|
description: "Base deployment directory on the NAS"
|
|
required: true
|
|
default: "/home/admin/geo-rankly"
|
|
image_tag:
|
|
description: "Docker image tag. Defaults to the short commit SHA."
|
|
required: false
|
|
default: ""
|
|
keep_releases:
|
|
description: "Number of old release directories to keep"
|
|
required: false
|
|
default: "5"
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
deploy:
|
|
name: Deploy Docker Compose to NAS
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 180
|
|
env:
|
|
NAS_HOST: ${{ inputs.host }}
|
|
NAS_PORT: ${{ inputs.port }}
|
|
NAS_USER: ${{ inputs.user }}
|
|
DEPLOY_BASE: ${{ inputs.deploy_base }}
|
|
REQUESTED_IMAGE_TAG: ${{ inputs.image_tag }}
|
|
KEEP_RELEASES: ${{ inputs.keep_releases }}
|
|
NAS_SSH_PASSWORD: ${{ secrets.NAS_SSH_PASSWORD }}
|
|
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 openssh-client sshpass tar
|
|
|
|
- name: Resolve deployment metadata
|
|
run: |
|
|
set -eu
|
|
|
|
if [ -z "${NAS_SSH_PASSWORD:-}" ]; then
|
|
echo "::error::Missing repository secret NAS_SSH_PASSWORD"
|
|
exit 1
|
|
fi
|
|
|
|
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=5 ;;
|
|
esac
|
|
|
|
image_tag="${REQUESTED_IMAGE_TAG}"
|
|
if [ -z "${image_tag}" ]; then
|
|
image_tag="$(git rev-parse --short=12 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
|
|
|
|
release_id="$(date -u +%Y%m%d%H%M%S)-$(git rev-parse --short=12 HEAD)"
|
|
|
|
{
|
|
echo "IMAGE_TAG=${image_tag}"
|
|
echo "RELEASE_ID=${release_id}"
|
|
echo "KEEP_RELEASES=${KEEP_RELEASES}"
|
|
} >> "${GITHUB_ENV}"
|
|
|
|
echo "Image tag: ${image_tag}"
|
|
echo "Release ID: ${release_id}"
|
|
echo "Deploy base: ${DEPLOY_BASE}"
|
|
|
|
- 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: 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 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')"
|
|
|
|
{
|
|
printf 'SUDO_PASSWORD_B64=%s\n' "${sudo_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)"
|
|
unset SUDO_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 "$@"
|
|
}
|
|
|
|
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
|
|
|
|
IMAGE_TAG="${IMAGE_TAG}" DOCKER_BUILDKIT=1 compose_cmd \
|
|
--env-file "${shared_dir}/.env" \
|
|
-f "${release_dir}/deploy/docker-compose.yaml" \
|
|
config >/tmp/geo-rankly-compose.yaml
|
|
|
|
IMAGE_TAG="${IMAGE_TAG}" DOCKER_BUILDKIT=1 compose_cmd \
|
|
--env-file "${shared_dir}/.env" \
|
|
-f "${release_dir}/deploy/docker-compose.yaml" \
|
|
up -d --build --remove-orphans
|
|
|
|
ln -sfn "${release_dir}" "${DEPLOY_BASE}/current"
|
|
|
|
IMAGE_TAG="${IMAGE_TAG}" compose_cmd \
|
|
--env-file "${shared_dir}/.env" \
|
|
-f "${release_dir}/deploy/docker-compose.yaml" \
|
|
ps
|
|
|
|
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
|
|
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}' KEEP_RELEASES='${KEEP_RELEASES}' sh -se"
|
|
|
|
- name: Deployment summary
|
|
run: |
|
|
summary_file="${GITHUB_STEP_SUMMARY:-/tmp/nas-compose-deploy-summary.md}"
|
|
{
|
|
echo "### NAS Docker Compose deployment"
|
|
echo ""
|
|
echo "- Host: ${NAS_HOST}:${NAS_PORT}"
|
|
echo "- Deploy base: ${DEPLOY_BASE}"
|
|
echo "- Release: ${RELEASE_ID}"
|
|
echo "- Image tag: ${IMAGE_TAG}"
|
|
echo ""
|
|
echo "Useful NAS commands:"
|
|
echo ""
|
|
echo '```sh'
|
|
echo "cd ${DEPLOY_BASE}/current"
|
|
echo "docker compose --env-file ${DEPLOY_BASE}/shared/.env -f deploy/docker-compose.yaml ps"
|
|
echo "docker compose --env-file ${DEPLOY_BASE}/shared/.env -f deploy/docker-compose.yaml logs -f tenant-api"
|
|
echo '```'
|
|
} >> "${summary_file}"
|