From 8c9c83c845c5e175b3f9b332ddc279c702765a75 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 19 Mar 2026 08:04:00 +0800 Subject: [PATCH] refactor(ci): matrix frontend dns updates --- .env.example | 2 + .../service_release_frontend-deploy.yml | 12 ++- docs/usage/deployment.md | 11 ++- scripts/github-actions/ensure-frontend-dns.sh | 75 +++++++++++++++---- 4 files changed, 77 insertions(+), 23 deletions(-) diff --git a/.env.example b/.env.example index aed33b1..30c0226 100644 --- a/.env.example +++ b/.env.example @@ -43,6 +43,8 @@ NEXT_PUBLIC_GISCUS_CATEGORY_ID=DIC_kwDOQoiZ_s4Clj_q INTERNAL_SERVICE_TOKEN= # Cloudflare Web Analytics GraphQL credentials +CLOUDFLARE_DNS_API_TOKEN= +CLOUDFLARE_DNS_ZONE_TAG= CLOUDFLARE_API_TOKEN= CLOUDFLARE_ACCOUNT_ID= CLOUDFLARE_WEB_ANALYTICS_SITE_TAG= diff --git a/.github/workflows/service_release_frontend-deploy.yml b/.github/workflows/service_release_frontend-deploy.yml index 434ada2..e1771a5 100644 --- a/.github/workflows/service_release_frontend-deploy.yml +++ b/.github/workflows/service_release_frontend-deploy.yml @@ -111,10 +111,16 @@ jobs: build-args: ${{ steps.build_args.outputs.build_args }} stage-2-update-dns: - name: "2. Update Frontend DNS" + name: "2. Update Frontend DNS (${{ matrix.domain }})" runs-on: ubuntu-latest needs: stage-1-build-image environment: production + strategy: + fail-fast: false + matrix: + domain: + - cn.svc.plus + - cn.onwalk.net steps: - name: Checkout Repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 @@ -122,12 +128,10 @@ jobs: - name: Ensure Cloudflare DNS Records env: CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }} - CLOUDFLARE_ZONE_TAG: ${{ vars.CLOUDFLARE_ZONE_TAG }} run: > bash scripts/github-actions/ensure-frontend-dns.sh "${{ env.DEPLOY_HOST }}" - "${{ env.PRIMARY_DOMAIN }}" - "${{ env.SECONDARY_DOMAIN }}" + "${{ matrix.domain }}" stage-3-deploy: name: "3. Deploy Frontend Stack" diff --git a/docs/usage/deployment.md b/docs/usage/deployment.md index 86ed278..0828528 100644 --- a/docs/usage/deployment.md +++ b/docs/usage/deployment.md @@ -88,6 +88,8 @@ Repository/environment variables recommended: - `NEXT_PUBLIC_GISCUS_*` - `NEXT_PUBLIC_STRIPE_*` - `NEXT_PUBLIC_PAYPAL_CLIENT_ID` +- `CLOUDFLARE_ZONE_TAG` if homepage Cloudflare analytics are enabled at runtime +- `CLOUDFLARE_DNS_ZONE_TAG` only for single-domain manual DNS override; the GitHub Actions DNS stage resolves zones from each domain automatically ## Release Flow @@ -95,10 +97,11 @@ Repository/environment variables recommended: 2. GitHub Actions clones `knowledge/`. 3. Docker builds the frontend image with the public `NEXT_PUBLIC_*` values needed at build time. 4. The image is pushed to GHCR. -5. The workflow renders `.env.runtime`. -6. The workflow uploads `docker-compose.yml`, `Caddyfile`, and `.env.runtime` to the host. -7. The host pulls the new image, refreshes the static asset volume, and starts `dashboard + caddy`. -8. The workflow verifies `cn.svc.plus` and `cn.onwalk.net`. +5. The workflow runs a matrix DNS stage, updating one public domain per job. +6. The workflow renders `.env.runtime`. +7. The workflow uploads `docker-compose.yml`, `Caddyfile`, and `.env.runtime` to the host. +8. The host pulls the new image, refreshes the static asset volume, and starts `dashboard + caddy`. +9. The workflow verifies `cn.svc.plus` and `cn.onwalk.net`. ## Verification Commands diff --git a/scripts/github-actions/ensure-frontend-dns.sh b/scripts/github-actions/ensure-frontend-dns.sh index 401daf0..788a88a 100755 --- a/scripts/github-actions/ensure-frontend-dns.sh +++ b/scripts/github-actions/ensure-frontend-dns.sh @@ -7,9 +7,14 @@ if [[ "${1-}" == "--dry-run" ]]; then shift fi -TARGET_IP="${1:?usage: ensure-frontend-dns.sh [--dry-run] }" -PRIMARY_DOMAIN="${2:?usage: ensure-frontend-dns.sh [--dry-run] }" -SECONDARY_DOMAIN="${3:?usage: ensure-frontend-dns.sh [--dry-run] }" +TARGET_IP="${1:?usage: ensure-frontend-dns.sh [--dry-run] [domain...]}" +shift + +if [[ "$#" -lt 1 ]]; then + echo "usage: ensure-frontend-dns.sh [--dry-run] [domain...]" >&2 + exit 1 +fi + PROXIED="${CLOUDFLARE_PROXIED:-true}" require_env() { @@ -26,6 +31,45 @@ json_get() { python3 -c "import json,sys; data=json.load(sys.stdin); ${expression}" } +cloudflare_api_get() { + local path="$1" + curl -fsS \ + -H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \ + -H "Content-Type: application/json" \ + "https://api.cloudflare.com/client/v4${path}" +} + +resolve_zone_for_domain() { + local domain="$1" + local candidate="${domain%.}" + local response + local zone_id + + if [[ -n "${CLOUDFLARE_DNS_ZONE_TAG-}" ]]; then + printf '%s\t%s\n' "${CLOUDFLARE_DNS_ZONE_TAG}" "override" + return 0 + fi + + while [[ "${candidate}" == *.* ]]; do + response="$(cloudflare_api_get "/zones?name=${candidate}")" + if [[ "$(printf '%s' "${response}" | json_get 'print("true" if data.get("success") else "false")')" != "true" ]]; then + echo "Failed to query Cloudflare zones for ${candidate}" >&2 + exit 1 + fi + + zone_id="$(printf '%s' "${response}" | json_get 'result=data.get("result", []); print(result[0]["id"] if result else "")')" + if [[ -n "${zone_id}" ]]; then + printf '%s\t%s\n' "${zone_id}" "${candidate}" + return 0 + fi + + candidate="${candidate#*.}" + done + + echo "Unable to resolve Cloudflare zone for ${domain}" >&2 + exit 1 +} + dns_payload() { local domain="$1" DOMAIN="${domain}" TARGET_IP="${TARGET_IP}" PROXIED="${PROXIED}" python3 -c \ @@ -37,6 +81,8 @@ upsert_record() { local payload local response local record_id + local zone_id + local zone_name payload="$(dns_payload "${domain}")" @@ -46,12 +92,11 @@ upsert_record() { return 0 fi - response="$( - curl -fsS \ - -H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \ - -H "Content-Type: application/json" \ - "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_TAG}/dns_records?type=A&name=${domain}" - )" + IFS=$'\t' read -r zone_id zone_name <&2 @@ -66,7 +111,7 @@ upsert_record() { -H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \ -H "Content-Type: application/json" \ --data "${payload}" \ - "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_TAG}/dns_records/${record_id}" + "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}" )" else response="$( @@ -74,7 +119,7 @@ upsert_record() { -H "Authorization: Bearer ${CLOUDFLARE_DNS_API_TOKEN}" \ -H "Content-Type: application/json" \ --data "${payload}" \ - "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_TAG}/dns_records" + "https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records" )" fi @@ -83,13 +128,13 @@ upsert_record() { exit 1 fi - printf 'updated: %s -> %s\n' "${domain}" "${TARGET_IP}" + printf 'updated: %s -> %s (zone %s)\n' "${domain}" "${TARGET_IP}" "${zone_name}" } if [[ "${DRY_RUN}" != "true" ]]; then require_env CLOUDFLARE_DNS_API_TOKEN - require_env CLOUDFLARE_ZONE_TAG fi -upsert_record "${PRIMARY_DOMAIN}" -upsert_record "${SECONDARY_DOMAIN}" +for domain in "$@"; do + upsert_record "${domain}" +done