ci(deploy): publish images via Gitea Registry and verify on CD

CI workflows now own image publishing: backend-ci and frontend-ci push
service images to the Gitea Registry tagged with commit8. CD workflows
(nas-compose-deploy, nas-compose-service-deploy, offline-package, the
new nas-k3s-deploy) pull and verify those tags instead of rebuilding,
so deployments fail fast when CI hasn't produced the matching image.
The shared deploy/scripts/gitea-registry-images.sh helper drives both
the ensure (CI) and verify (CD) paths, and package.sh can now hydrate
its local images from the registry via SOURCE_IMAGE_REGISTRY.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 23:54:33 +08:00
parent b71e14df66
commit e0b6c1a9fa
10 changed files with 1869 additions and 87 deletions
+178 -14
View File
@@ -31,8 +31,44 @@ on:
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 short commit SHA."
description: "Docker image tag. Defaults to the pushed commit SHA first 8 chars."
required: false
default: ""
keep_releases:
@@ -56,10 +92,19 @@ jobs:
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 }}
REQUESTED_IMAGE_TAG: ${{ inputs.image_tag }}
KEEP_RELEASES: ${{ inputs.keep_releases }}
NAS_DEPLOY_ENV: ${{ secrets.NAS_DEPLOY_ENV }}
DOCKER_BUILDKIT: "1"
IMAGE_LIST: migrate tenant-api ops-api worker-generate kol-assist-worker scheduler frontend ops-web
steps:
- name: Checkout
@@ -68,7 +113,7 @@ jobs:
- name: Install SSH tools
run: |
apt-get update
apt-get install -y openssh-client sshpass tar
apt-get install -y curl openssh-client sshpass tar
- name: Resolve deployment metadata
run: |
@@ -108,6 +153,44 @@ jobs:
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"
@@ -134,9 +217,45 @@ jobs:
*[!0-9]*|"") KEEP_RELEASES=5 ;;
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
image_tag="${REQUESTED_IMAGE_TAG}"
if [ -z "${image_tag}" ]; then
image_tag="$(git rev-parse --short=12 HEAD)"
image_tag="$(git rev-parse --short=8 HEAD)"
fi
case "${image_tag}" in
@@ -149,6 +268,7 @@ jobs:
release_id="$(date -u +%Y%m%d%H%M%S)-$(git rev-parse --short=12 HEAD)"
{
echo "REGISTRY_HOST=${REGISTRY_HOST}"
echo "IMAGE_TAG=${image_tag}"
echo "RELEASE_ID=${release_id}"
echo "KEEP_RELEASES=${KEEP_RELEASES}"
@@ -157,6 +277,15 @@ jobs:
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
if ! bash deploy/scripts/gitea-registry-images.sh verify; then
echo "::error::Missing image(s) for commit ${IMAGE_TAG}. CI must publish images before CD."
exit 1
fi
- name: Prepare SSH known hosts
run: |
@@ -205,12 +334,12 @@ jobs:
docker exec "${postgres_name}" createdb -U geo geo_monitoring
echo "Building migration image..."
DOCKER_BUILDKIT=1 docker build \
--target migrate \
--tag "${migrate_image}" \
-f server/Dockerfile \
server/
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"
@@ -274,9 +403,11 @@ jobs:
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
@@ -297,7 +428,8 @@ jobs:
fi
SUDO_PASSWORD="$(printf '%s' "${SUDO_PASSWORD_B64}" | base64 -d 2>/dev/null || true)"
unset SUDO_PASSWORD_B64
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
@@ -311,6 +443,17 @@ jobs:
docker_cmd compose "$@"
}
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
@@ -323,18 +466,38 @@ jobs:
echo "Docker access mode: ${DOCKER_MODE}"
compose_cmd version
IMAGE_TAG="${IMAGE_TAG}" DOCKER_BUILDKIT=1 compose_cmd \
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
IMAGE_REGISTRY="${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}" \
IMAGE_TAG="${IMAGE_TAG}" 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 \
IMAGE_REGISTRY="${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}" \
IMAGE_TAG="${IMAGE_TAG}" compose_cmd \
--env-file "${shared_dir}/.env" \
-f "${release_dir}/deploy/docker-compose.yaml" \
up -d --build --remove-orphans
pull migrate tenant-api ops-api worker-generate kol-assist-worker scheduler frontend ops-web
IMAGE_REGISTRY="${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}" \
IMAGE_TAG="${IMAGE_TAG}" compose_cmd \
--env-file "${shared_dir}/.env" \
-f "${release_dir}/deploy/docker-compose.yaml" \
up -d --no-build --remove-orphans
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" \
-f "${release_dir}/deploy/docker-compose.yaml" \
@@ -348,7 +511,7 @@ jobs:
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"
"DEPLOY_BASE='${DEPLOY_BASE}' RELEASE_ID='${RELEASE_ID}' IMAGE_TAG='${IMAGE_TAG}' 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: |
@@ -360,6 +523,7 @@ jobs:
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 ""
echo "Useful NAS commands:"
echo ""