230 lines
10 KiB
YAML
230 lines
10 KiB
YAML
name: Build and Deploy Gateway
|
|
|
|
on:
|
|
push:
|
|
branches: [master]
|
|
workflow_dispatch:
|
|
inputs:
|
|
hard_deploy:
|
|
description: Always rebuild and deploy both Gateway images
|
|
required: false
|
|
default: false
|
|
type: boolean
|
|
|
|
env:
|
|
DEPLOY_PATH: /opt/vpn-proxy
|
|
BASE_IMAGE: vpn-proxy-runtime-base:bookworm-slim
|
|
NODE_BUILD_IMAGE: mirror.gcr.io/library/node:20.19-bookworm
|
|
RUNTIME_BASE_SOURCE_IMAGE: mirror.gcr.io/library/debian:bookworm-slim
|
|
APT_MIRROR: http://mirror.yandex.ru/debian
|
|
APT_SECURITY_MIRROR: http://mirror.yandex.ru/debian-security
|
|
SINGBOX_VERSION: 1.13.18
|
|
|
|
jobs:
|
|
build-and-push:
|
|
runs-on: ubuntu-22.04
|
|
outputs:
|
|
affected_components: ${{ steps['gateway-build'].outputs.affected_components }}
|
|
restart_scope: ${{ steps['gateway-build'].outputs.restart_scope }}
|
|
steps:
|
|
- name: Clone repository
|
|
env:
|
|
GIT_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
SERVER_HOST=$(echo "${{ gitea.server_url }}" | sed 's|https\?://||')
|
|
rm -rf repo
|
|
git clone "http://${{ gitea.actor }}:${GIT_TOKEN}@${SERVER_HOST}/${{ gitea.repository }}.git" repo
|
|
cd repo
|
|
git checkout ${{ gitea.sha }}
|
|
|
|
- name: Build and push gateway image
|
|
id: gateway-build
|
|
env:
|
|
HARD_DEPLOY_INPUT: ${{ inputs.hard_deploy }}
|
|
run: |
|
|
set -euo pipefail
|
|
cd repo
|
|
|
|
REGISTRY_HOST=$(echo "${{ gitea.server_url }}" | sed 's|https\?://||')
|
|
IMAGE="${REGISTRY_HOST}/${{ gitea.repository }}/gateway"
|
|
CONTROL_IMAGE="${IMAGE}-control"
|
|
DATAPLANE_IMAGE="${IMAGE}-dataplane"
|
|
|
|
EVENT_NAME="${{ gitea.event_name }}"
|
|
case "${EVENT_NAME}:${HARD_DEPLOY_INPUT}" in
|
|
workflow_dispatch:true) HARD_DEPLOY=true ;;
|
|
workflow_dispatch:false|workflow_dispatch:|push:false|push:) HARD_DEPLOY=false ;;
|
|
*) echo "Invalid hard deploy request: ${EVENT_NAME}:${HARD_DEPLOY_INPUT}" >&2; exit 1 ;;
|
|
esac
|
|
BEFORE_SHA="${{ gitea.event.before }}"
|
|
ZERO_SHA="0000000000000000000000000000000000000000"
|
|
if [ "$EVENT_NAME" = "push" ] \
|
|
&& [ -n "$BEFORE_SHA" ] \
|
|
&& [ "$BEFORE_SHA" != "$ZERO_SHA" ] \
|
|
&& git cat-file -e "${BEFORE_SHA}^{commit}" 2>/dev/null; then
|
|
CHANGED_FILES="$(git diff --no-renames --name-only "$BEFORE_SHA" "${{ gitea.sha }}")"
|
|
elif [ "$EVENT_NAME" = "push" ]; then
|
|
CHANGED_FILES="package.json"
|
|
else
|
|
CHANGED_FILES="$(git diff-tree --no-renames --no-commit-id --name-only -r -m HEAD)"
|
|
fi
|
|
if command -v node >/dev/null 2>&1; then
|
|
RUNTIME_IMPACT="$(printf '%s\n' "$CHANGED_FILES" | node scripts/runtime-impact.mjs --stdin)"
|
|
else
|
|
if ! docker image inspect "${{ env.BASE_IMAGE }}" >/dev/null 2>&1 \
|
|
|| ! docker run --rm "${{ env.BASE_IMAGE }}" sh -lc 'command -v node >/dev/null'; then
|
|
echo "Cannot classify runtime impact: Node and the existing runtime base are unavailable." >&2
|
|
exit 1
|
|
fi
|
|
RUNTIME_IMPACT="$(printf '%s\n' "$CHANGED_FILES" | docker run --rm -i \
|
|
-v "$PWD:/work" \
|
|
-w /work \
|
|
"${{ env.BASE_IMAGE }}" \
|
|
node scripts/runtime-impact.mjs --stdin)"
|
|
fi
|
|
AFFECTED_COMPONENTS="$(printf '%s\n' "$RUNTIME_IMPACT" | sed -n 's/^affected-components=//p')"
|
|
RESTART_SCOPE="$(printf '%s\n' "$RUNTIME_IMPACT" | sed -n 's/^restart-scope=//p')"
|
|
case "${AFFECTED_COMPONENTS}:${RESTART_SCOPE}" in
|
|
none:none|control:control|dataplane:both|control+dataplane:both) ;;
|
|
*) echo "Invalid runtime impact: ${RUNTIME_IMPACT}" >&2; exit 1 ;;
|
|
esac
|
|
DOCKER_BUILD_OPTIONS=()
|
|
if [ "$HARD_DEPLOY" = "true" ]; then
|
|
echo "Hard deploy requested: forcing no-cache rebuild and deploy of both Gateway images."
|
|
AFFECTED_COMPONENTS="control+dataplane"
|
|
RESTART_SCOPE="both"
|
|
DOCKER_BUILD_OPTIONS=(--no-cache)
|
|
fi
|
|
echo "Affected components: ${AFFECTED_COMPONENTS}"
|
|
echo "Restart scope: ${RESTART_SCOPE}"
|
|
echo "affected_components=${AFFECTED_COMPONENTS}" >> "$GITHUB_OUTPUT"
|
|
echo "restart_scope=${RESTART_SCOPE}" >> "$GITHUB_OUTPUT"
|
|
if command -v npm >/dev/null 2>&1; then
|
|
npm ci --no-audit --no-fund
|
|
npm run typecheck
|
|
npm run check:boundaries
|
|
npm test
|
|
npm run build:production
|
|
else
|
|
if ! docker run --rm "${{ env.NODE_BUILD_IMAGE }}" sh -lc 'command -v npm >/dev/null && command -v git >/dev/null && test -x /bin/bash'; then
|
|
echo "Cannot validate change: host npm and the Node 20.19 build toolchain are unavailable." >&2
|
|
exit 1
|
|
fi
|
|
echo "Host npm not found; validating inside ${{ env.NODE_BUILD_IMAGE }}"
|
|
docker run --rm \
|
|
--network host \
|
|
-v "$PWD:/work" \
|
|
-w /work \
|
|
"${{ env.NODE_BUILD_IMAGE }}" \
|
|
sh -lc '
|
|
npm ci --no-audit --no-fund
|
|
npm_status=$?
|
|
if [ "$npm_status" -ne 0 ] || [ ! -x node_modules/.bin/tsc ]; then
|
|
echo "npm ci failed to install the validation toolchain." >&2
|
|
tail -n 200 /root/.npm/_logs/*-debug-0.log >&2 || true
|
|
exit 1
|
|
fi
|
|
npm run typecheck && npm run check:boundaries && npm test && npm run build:production
|
|
'
|
|
fi
|
|
if [ "$RESTART_SCOPE" = "none" ]; then
|
|
echo "Image build and push skipped: no Gateway runtime impact."
|
|
exit 0
|
|
fi
|
|
|
|
echo "Build runner: $(hostname)"
|
|
echo "Base image: ${{ env.BASE_IMAGE }}"
|
|
echo "Docker context: $(docker context show 2>/dev/null || true)"
|
|
docker info 2>/dev/null | sed -n '/HTTP Proxy:/p;/HTTPS Proxy:/p;/Name:/p'
|
|
|
|
if ! docker image inspect "${{ env.BASE_IMAGE }}" >/dev/null 2>&1 \
|
|
|| ! docker run --rm "${{ env.BASE_IMAGE }}" sh -lc \
|
|
'command -v npm >/dev/null && sing-box version 2>&1 | grep -Fx "sing-box version ${{ env.SINGBOX_VERSION }}"'; then
|
|
echo "Runtime base image ${{ env.BASE_IMAGE }} is missing npm or sing-box ${{ env.SINGBOX_VERSION }}; building it now."
|
|
BASE_IMAGE="${{ env.RUNTIME_BASE_SOURCE_IMAGE }}" \
|
|
RUNTIME_BASE_IMAGE="${{ env.BASE_IMAGE }}" \
|
|
APT_MIRROR="${{ env.APT_MIRROR }}" \
|
|
APT_SECURITY_MIRROR="${{ env.APT_SECURITY_MIRROR }}" \
|
|
SINGBOX_VERSION="${{ env.SINGBOX_VERSION }}" \
|
|
./scripts/build-runtime-base.sh
|
|
fi
|
|
|
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY_HOST" -u "${{ gitea.actor }}" --password-stdin
|
|
DOCKER_BUILDKIT=1 docker build \
|
|
"${DOCKER_BUILD_OPTIONS[@]}" \
|
|
--network host \
|
|
--pull=false \
|
|
--build-arg NODE_BUILD_IMAGE="${{ env.NODE_BUILD_IMAGE }}" \
|
|
--build-arg BASE_IMAGE="${{ env.BASE_IMAGE }}" \
|
|
--build-arg SINGBOX_VERSION="${{ env.SINGBOX_VERSION }}" \
|
|
--build-arg INSTALL_RUNTIME_DEPS=false \
|
|
--build-arg INSTALL_SINGBOX=false \
|
|
-t "${CONTROL_IMAGE}:latest" \
|
|
-t "${CONTROL_IMAGE}:${{ gitea.sha }}" \
|
|
-t "${DATAPLANE_IMAGE}:latest" \
|
|
-t "${DATAPLANE_IMAGE}:${{ gitea.sha }}" \
|
|
.
|
|
docker run --rm --entrypoint sing-box "${CONTROL_IMAGE}:${{ gitea.sha }}" version 2>&1 \
|
|
| grep -Fx "sing-box version ${{ env.SINGBOX_VERSION }}"
|
|
docker run --rm --entrypoint sing-box "${DATAPLANE_IMAGE}:${{ gitea.sha }}" version 2>&1 \
|
|
| grep -Fx "sing-box version ${{ env.SINGBOX_VERSION }}"
|
|
docker push "${CONTROL_IMAGE}:latest"
|
|
docker push "${CONTROL_IMAGE}:${{ gitea.sha }}"
|
|
docker push "${DATAPLANE_IMAGE}:latest"
|
|
docker push "${DATAPLANE_IMAGE}:${{ gitea.sha }}"
|
|
|
|
deploy:
|
|
runs-on: lxc-111
|
|
needs: build-and-push
|
|
steps:
|
|
- name: Clone repository
|
|
env:
|
|
GIT_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
SERVER_HOST=$(echo "${{ gitea.server_url }}" | sed 's|https\?://||')
|
|
rm -rf repo
|
|
git clone --depth 2 "http://${{ gitea.actor }}:${GIT_TOKEN}@${SERVER_HOST}/${{ gitea.repository }}.git" repo
|
|
cd repo
|
|
git checkout ${{ gitea.sha }}
|
|
|
|
- name: Pull and deploy gateway image
|
|
run: |
|
|
set -euo pipefail
|
|
cd repo
|
|
|
|
REGISTRY_HOST=$(echo "${{ gitea.server_url }}" | sed 's|https\?://||')
|
|
IMAGE="${REGISTRY_HOST}/${{ gitea.repository }}/gateway"
|
|
CONTROL_IMAGE="${IMAGE}-control:${{ gitea.sha }}"
|
|
DATAPLANE_IMAGE="${IMAGE}-dataplane:${{ gitea.sha }}"
|
|
AFFECTED_COMPONENTS="${{ needs['build-and-push'].outputs.affected_components }}"
|
|
RESTART_SCOPE="${{ needs['build-and-push'].outputs.restart_scope }}"
|
|
case "${AFFECTED_COMPONENTS}:${RESTART_SCOPE}" in
|
|
none:none|control:control|dataplane:both|control+dataplane:both) ;;
|
|
*) echo "Invalid runtime impact output: ${AFFECTED_COMPONENTS}:${RESTART_SCOPE}" >&2; exit 1 ;;
|
|
esac
|
|
if [ "$RESTART_SCOPE" = "none" ]; then
|
|
echo "Deploy skipped: no Gateway runtime impact."
|
|
exit 0
|
|
fi
|
|
UPDATE_DATAPLANE=false
|
|
if [ "$RESTART_SCOPE" = "both" ]; then
|
|
UPDATE_DATAPLANE=true
|
|
fi
|
|
|
|
echo "Deploy runner: $(hostname)"
|
|
echo "Affected components: ${AFFECTED_COMPONENTS}"
|
|
echo "Restart scope: ${RESTART_SCOPE}"
|
|
echo "Update dataplane: ${UPDATE_DATAPLANE}"
|
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY_HOST" -u "${{ gitea.actor }}" --password-stdin
|
|
DEPLOY_PATH="${{ env.DEPLOY_PATH }}" \
|
|
CONTROL_IMAGE="${CONTROL_IMAGE}" \
|
|
DATAPLANE_IMAGE="${DATAPLANE_IMAGE}" \
|
|
UPDATE_DATAPLANE="${UPDATE_DATAPLANE}" \
|
|
bash scripts/deploy-gateway.sh
|
|
VERSION_JSON="$(curl --noproxy '*' -fsS http://127.0.0.1:3456/api/version)"
|
|
printf '%s\n' "$VERSION_JSON"
|
|
printf '%s\n' "$VERSION_JSON" \
|
|
| grep -F "\"singBox\":\"${{ env.SINGBOX_VERSION }}\""
|