name: Build and Deploy Gateway on: push: branches: [master] workflow_dispatch: 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.12.13 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 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 }}" 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 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 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'; then echo "Runtime base image ${{ env.BASE_IMAGE }} is missing npm; 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 \ --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 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