feat(console): add single-node frontend release flow
This commit is contained in:
parent
f7041a1410
commit
fea1ab6640
15
.dockerignore
Normal file
15
.dockerignore
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.git
|
||||||
|
.github
|
||||||
|
.next
|
||||||
|
.contentlayer
|
||||||
|
node_modules
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
test-results
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
deploy/single-node/.env.runtime
|
||||||
|
knowledge/.git
|
||||||
25
.env.example
25
.env.example
@ -1,3 +1,26 @@
|
|||||||
|
# Frontend site base URLs
|
||||||
|
APP_BASE_URL=
|
||||||
|
NEXT_PUBLIC_APP_BASE_URL=
|
||||||
|
NEXT_PUBLIC_SITE_URL=
|
||||||
|
NEXT_PUBLIC_LOGIN_URL=
|
||||||
|
NEXT_PUBLIC_DOCS_BASE_URL=
|
||||||
|
SESSION_COOKIE_SECURE=true
|
||||||
|
NEXT_PUBLIC_SESSION_COOKIE_SECURE=true
|
||||||
|
RUNTIME_HOSTNAME=
|
||||||
|
NEXT_RUNTIME_HOSTNAME=
|
||||||
|
DEPLOYMENT_HOSTNAME=
|
||||||
|
RUNTIME_ENV=prod
|
||||||
|
REGION=cn
|
||||||
|
NEXT_PUBLIC_RUNTIME_ENVIRONMENT=prod
|
||||||
|
NEXT_PUBLIC_RUNTIME_REGION=cn
|
||||||
|
|
||||||
|
# Upstream service endpoints
|
||||||
|
ACCOUNT_SERVICE_URL=https://accounts.svc.plus
|
||||||
|
NEXT_PUBLIC_ACCOUNT_SERVICE_URL=https://accounts.svc.plus
|
||||||
|
SERVER_SERVICE_URL=https://api.svc.plus
|
||||||
|
NEXT_PUBLIC_SERVER_SERVICE_URL=https://api.svc.plus
|
||||||
|
SERVER_SERVICE_INTERNAL_URL=
|
||||||
|
|
||||||
# OpenClaw assistant integrations
|
# OpenClaw assistant integrations
|
||||||
# Use environment variables to prefill the assistant and integrations page.
|
# Use environment variables to prefill the assistant and integrations page.
|
||||||
# Values are read server-side and are not hardcoded into the UI.
|
# Values are read server-side and are not hardcoded into the UI.
|
||||||
@ -23,6 +46,7 @@ INTERNAL_SERVICE_TOKEN=
|
|||||||
CLOUDFLARE_API_TOKEN=
|
CLOUDFLARE_API_TOKEN=
|
||||||
CLOUDFLARE_ACCOUNT_ID=
|
CLOUDFLARE_ACCOUNT_ID=
|
||||||
CLOUDFLARE_WEB_ANALYTICS_SITE_TAG=
|
CLOUDFLARE_WEB_ANALYTICS_SITE_TAG=
|
||||||
|
CLOUDFLARE_ZONE_TAG=
|
||||||
|
|
||||||
# Root email whitelist for privileged user-creation actions (comma-separated)
|
# Root email whitelist for privileged user-creation actions (comma-separated)
|
||||||
# Default: admin@svc.plus
|
# Default: admin@svc.plus
|
||||||
@ -30,6 +54,7 @@ ROOT_EMAIL_WHITELIST=admin@svc.plus
|
|||||||
|
|
||||||
# Stripe public price ids used by /prices, product pages, and /panel/subscription
|
# Stripe public price ids used by /prices, product pages, and /panel/subscription
|
||||||
# These values are safe to expose to the browser. Use Stripe test-mode price ids for local/dev.
|
# These values are safe to expose to the browser. Use Stripe test-mode price ids for local/dev.
|
||||||
|
NEXT_PUBLIC_PAYPAL_CLIENT_ID=
|
||||||
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO=
|
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO=
|
||||||
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION=
|
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION=
|
||||||
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO=
|
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO=
|
||||||
|
|||||||
175
.github/workflows/service_release_frontend-deploy.yml
vendored
Normal file
175
.github/workflows/service_release_frontend-deploy.yml
vendored
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
name: Service Release Frontend Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
image_tag:
|
||||||
|
description: Optional image tag override. Defaults to the current commit SHA.
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/service_release_frontend-deploy.yml"
|
||||||
|
- "deploy/single-node/**"
|
||||||
|
- "scripts/github-actions/**"
|
||||||
|
- "src/**"
|
||||||
|
- "public/**"
|
||||||
|
- "scripts/**"
|
||||||
|
- "config/**"
|
||||||
|
- "package.json"
|
||||||
|
- "Dockerfile"
|
||||||
|
- ".env.example"
|
||||||
|
- "next.config.mjs"
|
||||||
|
- "tailwind.config.js"
|
||||||
|
- "postcss.config.mjs"
|
||||||
|
- "tsconfig.json"
|
||||||
|
- "contentlayer.config.ts"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: frontend-prod
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: 47.120.61.35
|
||||||
|
DEPLOY_USER: root
|
||||||
|
DEPLOY_DIR: /opt/console-svc-plus
|
||||||
|
PRIMARY_DOMAIN: cn.svc.plus
|
||||||
|
SECONDARY_DOMAIN: cn.onwalk.net
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prepare:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
ghcr_namespace: ${{ steps.meta.outputs.ghcr_namespace }}
|
||||||
|
image_tag: ${{ steps.meta.outputs.image_tag }}
|
||||||
|
image_ref: ${{ steps.meta.outputs.image_ref }}
|
||||||
|
steps:
|
||||||
|
- name: Compute image metadata
|
||||||
|
id: meta
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
image_tag="${{ github.event.inputs.image_tag }}"
|
||||||
|
if [[ -z "${image_tag}" ]]; then
|
||||||
|
image_tag="${GITHUB_SHA}"
|
||||||
|
fi
|
||||||
|
ghcr_namespace="${GITHUB_REPOSITORY_OWNER,,}"
|
||||||
|
echo "ghcr_namespace=${ghcr_namespace}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "image_tag=${image_tag}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "image_ref=ghcr.io/${ghcr_namespace}/dashboard:${image_tag}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: prepare
|
||||||
|
environment: production
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Clone knowledge content
|
||||||
|
run: git clone --depth=1 https://github.com/Cloud-Neutral-Workshop/knowledge.git knowledge
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ github.token }}
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push frontend image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ needs.prepare.outputs.image_ref }}
|
||||||
|
build-args: |
|
||||||
|
NODE_BUILDER_IMAGE=node:22-bookworm
|
||||||
|
NODE_RUNTIME_IMAGE=node:22-slim
|
||||||
|
CONTENTLAYER_BUILD=true
|
||||||
|
NEXT_PUBLIC_APP_BASE_URL=${{ vars.NEXT_PUBLIC_APP_BASE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }}
|
||||||
|
NEXT_PUBLIC_SITE_URL=${{ vars.NEXT_PUBLIC_SITE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }}
|
||||||
|
NEXT_PUBLIC_LOGIN_URL=${{ vars.NEXT_PUBLIC_LOGIN_URL || format('https://{0}/login', env.PRIMARY_DOMAIN) }}
|
||||||
|
NEXT_PUBLIC_DOCS_BASE_URL=${{ vars.NEXT_PUBLIC_DOCS_BASE_URL || format('https://{0}/docs', env.PRIMARY_DOMAIN) }}
|
||||||
|
NEXT_PUBLIC_RUNTIME_ENVIRONMENT=${{ vars.NEXT_PUBLIC_RUNTIME_ENVIRONMENT || 'prod' }}
|
||||||
|
NEXT_PUBLIC_RUNTIME_REGION=${{ vars.NEXT_PUBLIC_RUNTIME_REGION || 'cn' }}
|
||||||
|
NEXT_PUBLIC_GISCUS_REPO=${{ vars.NEXT_PUBLIC_GISCUS_REPO || 'cloud-neutral-toolkit/console.svc.plus' }}
|
||||||
|
NEXT_PUBLIC_GISCUS_REPO_ID=${{ vars.NEXT_PUBLIC_GISCUS_REPO_ID }}
|
||||||
|
NEXT_PUBLIC_GISCUS_CATEGORY=${{ vars.NEXT_PUBLIC_GISCUS_CATEGORY || 'General' }}
|
||||||
|
NEXT_PUBLIC_GISCUS_CATEGORY_ID=${{ vars.NEXT_PUBLIC_GISCUS_CATEGORY_ID }}
|
||||||
|
NEXT_PUBLIC_PAYPAL_CLIENT_ID=${{ vars.NEXT_PUBLIC_PAYPAL_CLIENT_ID }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION }}
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- prepare
|
||||||
|
- build
|
||||||
|
environment: production
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy frontend stack
|
||||||
|
env:
|
||||||
|
GHCR_USERNAME: ${{ github.actor }}
|
||||||
|
GHCR_PASSWORD: ${{ github.token }}
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.FRONTEND_DEPLOY_SSH_KEY }}
|
||||||
|
FRONTEND_IMAGE: ${{ needs.prepare.outputs.image_ref }}
|
||||||
|
APP_BASE_URL: ${{ vars.APP_BASE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }}
|
||||||
|
NEXT_PUBLIC_APP_BASE_URL: ${{ vars.NEXT_PUBLIC_APP_BASE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }}
|
||||||
|
NEXT_PUBLIC_SITE_URL: ${{ vars.NEXT_PUBLIC_SITE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }}
|
||||||
|
NEXT_PUBLIC_LOGIN_URL: ${{ vars.NEXT_PUBLIC_LOGIN_URL || format('https://{0}/login', env.PRIMARY_DOMAIN) }}
|
||||||
|
NEXT_PUBLIC_DOCS_BASE_URL: ${{ vars.NEXT_PUBLIC_DOCS_BASE_URL || format('https://{0}/docs', env.PRIMARY_DOMAIN) }}
|
||||||
|
NEXT_PUBLIC_RUNTIME_ENVIRONMENT: ${{ vars.NEXT_PUBLIC_RUNTIME_ENVIRONMENT || 'prod' }}
|
||||||
|
NEXT_PUBLIC_RUNTIME_REGION: ${{ vars.NEXT_PUBLIC_RUNTIME_REGION || 'cn' }}
|
||||||
|
RUNTIME_HOSTNAME: ${{ vars.RUNTIME_HOSTNAME || env.PRIMARY_DOMAIN }}
|
||||||
|
NEXT_RUNTIME_HOSTNAME: ${{ vars.NEXT_RUNTIME_HOSTNAME || env.PRIMARY_DOMAIN }}
|
||||||
|
DEPLOYMENT_HOSTNAME: ${{ vars.DEPLOYMENT_HOSTNAME || env.PRIMARY_DOMAIN }}
|
||||||
|
ACCOUNT_SERVICE_URL: ${{ vars.ACCOUNT_SERVICE_URL || 'https://accounts.svc.plus' }}
|
||||||
|
NEXT_PUBLIC_ACCOUNT_SERVICE_URL: ${{ vars.NEXT_PUBLIC_ACCOUNT_SERVICE_URL || vars.ACCOUNT_SERVICE_URL || 'https://accounts.svc.plus' }}
|
||||||
|
SERVER_SERVICE_URL: ${{ vars.SERVER_SERVICE_URL || 'https://api.svc.plus' }}
|
||||||
|
NEXT_PUBLIC_SERVER_SERVICE_URL: ${{ vars.NEXT_PUBLIC_SERVER_SERVICE_URL || vars.SERVER_SERVICE_URL || 'https://api.svc.plus' }}
|
||||||
|
SERVER_SERVICE_INTERNAL_URL: ${{ vars.SERVER_SERVICE_INTERNAL_URL }}
|
||||||
|
ROOT_EMAIL_WHITELIST: ${{ vars.ROOT_EMAIL_WHITELIST || 'admin@svc.plus' }}
|
||||||
|
OPENCLAW_GATEWAY_REMOTE_URL: ${{ vars.OPENCLAW_GATEWAY_REMOTE_URL }}
|
||||||
|
OPENCLAW_GATEWAY_TOKEN: ${{ secrets.OPENCLAW_GATEWAY_TOKEN }}
|
||||||
|
VAULT_SERVER_URL: ${{ vars.VAULT_SERVER_URL }}
|
||||||
|
VAULT_NAMESPACE: ${{ vars.VAULT_NAMESPACE }}
|
||||||
|
VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }}
|
||||||
|
APISIX_AI_GATEWAY_URL: ${{ vars.APISIX_AI_GATEWAY_URL }}
|
||||||
|
AI_GATEWAY_ACCESS_TOKEN: ${{ secrets.AI_GATEWAY_ACCESS_TOKEN }}
|
||||||
|
INTERNAL_SERVICE_TOKEN: ${{ secrets.INTERNAL_SERVICE_TOKEN }}
|
||||||
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
|
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
|
CLOUDFLARE_WEB_ANALYTICS_SITE_TAG: ${{ vars.CLOUDFLARE_WEB_ANALYTICS_SITE_TAG }}
|
||||||
|
CLOUDFLARE_ZONE_TAG: ${{ vars.CLOUDFLARE_ZONE_TAG }}
|
||||||
|
NEXT_PUBLIC_GISCUS_REPO: ${{ vars.NEXT_PUBLIC_GISCUS_REPO || 'cloud-neutral-toolkit/console.svc.plus' }}
|
||||||
|
NEXT_PUBLIC_GISCUS_REPO_ID: ${{ vars.NEXT_PUBLIC_GISCUS_REPO_ID }}
|
||||||
|
NEXT_PUBLIC_GISCUS_CATEGORY: ${{ vars.NEXT_PUBLIC_GISCUS_CATEGORY || 'General' }}
|
||||||
|
NEXT_PUBLIC_GISCUS_CATEGORY_ID: ${{ vars.NEXT_PUBLIC_GISCUS_CATEGORY_ID }}
|
||||||
|
NEXT_PUBLIC_PAYPAL_CLIENT_ID: ${{ vars.NEXT_PUBLIC_PAYPAL_CLIENT_ID }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO }}
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION: ${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION }}
|
||||||
|
run: bash scripts/github-actions/deploy-frontend-single-node.sh
|
||||||
|
|
||||||
|
- name: Verify primary domain
|
||||||
|
run: curl -fsSIL "https://${PRIMARY_DOMAIN}"
|
||||||
|
|
||||||
|
- name: Verify secondary domain redirect
|
||||||
|
run: curl -fsSIL "https://${SECONDARY_DOMAIN}"
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -55,6 +55,7 @@ coverage/
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
deploy/single-node/.env.runtime
|
||||||
|
|
||||||
# Build artifacts
|
# Build artifacts
|
||||||
build/
|
build/
|
||||||
|
|||||||
55
Dockerfile
55
Dockerfile
@ -4,6 +4,23 @@
|
|||||||
ARG NODE_BUILDER_IMAGE=node:22-bookworm
|
ARG NODE_BUILDER_IMAGE=node:22-bookworm
|
||||||
ARG NODE_RUNTIME_IMAGE=node:22-slim
|
ARG NODE_RUNTIME_IMAGE=node:22-slim
|
||||||
ARG CONTENTLAYER_BUILD=true
|
ARG CONTENTLAYER_BUILD=true
|
||||||
|
ARG NEXT_PUBLIC_APP_BASE_URL=
|
||||||
|
ARG NEXT_PUBLIC_SITE_URL=
|
||||||
|
ARG NEXT_PUBLIC_LOGIN_URL=
|
||||||
|
ARG NEXT_PUBLIC_DOCS_BASE_URL=
|
||||||
|
ARG NEXT_PUBLIC_RUNTIME_ENVIRONMENT=
|
||||||
|
ARG NEXT_PUBLIC_RUNTIME_REGION=
|
||||||
|
ARG NEXT_PUBLIC_GISCUS_REPO=
|
||||||
|
ARG NEXT_PUBLIC_GISCUS_REPO_ID=
|
||||||
|
ARG NEXT_PUBLIC_GISCUS_CATEGORY=
|
||||||
|
ARG NEXT_PUBLIC_GISCUS_CATEGORY_ID=
|
||||||
|
ARG NEXT_PUBLIC_PAYPAL_CLIENT_ID=
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO=
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION=
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO=
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION=
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO=
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION=
|
||||||
|
|
||||||
# -------------------------------------------------------
|
# -------------------------------------------------------
|
||||||
# Stage 1 — Builder (Turbopack + standalone)
|
# Stage 1 — Builder (Turbopack + standalone)
|
||||||
@ -12,8 +29,43 @@ FROM ${NODE_BUILDER_IMAGE} AS builder
|
|||||||
|
|
||||||
WORKDIR /app/dashboard
|
WORKDIR /app/dashboard
|
||||||
|
|
||||||
|
ARG NEXT_PUBLIC_APP_BASE_URL
|
||||||
|
ARG NEXT_PUBLIC_SITE_URL
|
||||||
|
ARG NEXT_PUBLIC_LOGIN_URL
|
||||||
|
ARG NEXT_PUBLIC_DOCS_BASE_URL
|
||||||
|
ARG NEXT_PUBLIC_RUNTIME_ENVIRONMENT
|
||||||
|
ARG NEXT_PUBLIC_RUNTIME_REGION
|
||||||
|
ARG NEXT_PUBLIC_GISCUS_REPO
|
||||||
|
ARG NEXT_PUBLIC_GISCUS_REPO_ID
|
||||||
|
ARG NEXT_PUBLIC_GISCUS_CATEGORY
|
||||||
|
ARG NEXT_PUBLIC_GISCUS_CATEGORY_ID
|
||||||
|
ARG NEXT_PUBLIC_PAYPAL_CLIENT_ID
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO
|
||||||
|
ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1 \
|
ENV NEXT_TELEMETRY_DISABLED=1 \
|
||||||
NEXT_PRIVATE_TURBOPACK=1
|
NEXT_PRIVATE_TURBOPACK=1 \
|
||||||
|
NEXT_PUBLIC_APP_BASE_URL=${NEXT_PUBLIC_APP_BASE_URL} \
|
||||||
|
NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL} \
|
||||||
|
NEXT_PUBLIC_LOGIN_URL=${NEXT_PUBLIC_LOGIN_URL} \
|
||||||
|
NEXT_PUBLIC_DOCS_BASE_URL=${NEXT_PUBLIC_DOCS_BASE_URL} \
|
||||||
|
NEXT_PUBLIC_RUNTIME_ENVIRONMENT=${NEXT_PUBLIC_RUNTIME_ENVIRONMENT} \
|
||||||
|
NEXT_PUBLIC_RUNTIME_REGION=${NEXT_PUBLIC_RUNTIME_REGION} \
|
||||||
|
NEXT_PUBLIC_GISCUS_REPO=${NEXT_PUBLIC_GISCUS_REPO} \
|
||||||
|
NEXT_PUBLIC_GISCUS_REPO_ID=${NEXT_PUBLIC_GISCUS_REPO_ID} \
|
||||||
|
NEXT_PUBLIC_GISCUS_CATEGORY=${NEXT_PUBLIC_GISCUS_CATEGORY} \
|
||||||
|
NEXT_PUBLIC_GISCUS_CATEGORY_ID=${NEXT_PUBLIC_GISCUS_CATEGORY_ID} \
|
||||||
|
NEXT_PUBLIC_PAYPAL_CLIENT_ID=${NEXT_PUBLIC_PAYPAL_CLIENT_ID} \
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO=${NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO} \
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION=${NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION} \
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO=${NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO} \
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION=${NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION} \
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO=${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO} \
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION=${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION}
|
||||||
|
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
# 基础镜像升级到最新
|
# 基础镜像升级到最新
|
||||||
@ -59,6 +111,7 @@ RUN apt-get update \
|
|||||||
COPY --from=builder /app/dashboard/.next/standalone ./
|
COPY --from=builder /app/dashboard/.next/standalone ./
|
||||||
COPY --from=builder /app/dashboard/.next/static ./static
|
COPY --from=builder /app/dashboard/.next/static ./static
|
||||||
COPY --from=builder /app/dashboard/public ./public
|
COPY --from=builder /app/dashboard/public ./public
|
||||||
|
COPY --from=builder /app/dashboard/knowledge ./knowledge
|
||||||
COPY --from=builder /app/dashboard/src/content/blog ./src/content/blog
|
COPY --from=builder /app/dashboard/src/content/blog ./src/content/blog
|
||||||
|
|
||||||
# ---------------------------
|
# ---------------------------
|
||||||
|
|||||||
54
deploy/single-node/.env.runtime.example
Normal file
54
deploy/single-node/.env.runtime.example
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# Compose settings
|
||||||
|
FRONTEND_IMAGE=ghcr.io/cloud-neutral-toolkit/dashboard:replace-me
|
||||||
|
PRIMARY_DOMAIN=cn.svc.plus
|
||||||
|
SECONDARY_DOMAIN=cn.onwalk.net
|
||||||
|
|
||||||
|
# Frontend runtime
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
RUNTIME_ENV=prod
|
||||||
|
REGION=cn
|
||||||
|
APP_BASE_URL=https://cn.svc.plus
|
||||||
|
NEXT_PUBLIC_APP_BASE_URL=https://cn.svc.plus
|
||||||
|
NEXT_PUBLIC_SITE_URL=https://cn.svc.plus
|
||||||
|
NEXT_PUBLIC_LOGIN_URL=https://cn.svc.plus/login
|
||||||
|
NEXT_PUBLIC_DOCS_BASE_URL=https://cn.svc.plus/docs
|
||||||
|
SESSION_COOKIE_SECURE=true
|
||||||
|
NEXT_PUBLIC_SESSION_COOKIE_SECURE=true
|
||||||
|
RUNTIME_HOSTNAME=cn.svc.plus
|
||||||
|
DEPLOYMENT_HOSTNAME=cn.svc.plus
|
||||||
|
NEXT_PUBLIC_RUNTIME_ENVIRONMENT=prod
|
||||||
|
NEXT_PUBLIC_RUNTIME_REGION=cn
|
||||||
|
|
||||||
|
# Upstream service URLs
|
||||||
|
ACCOUNT_SERVICE_URL=https://accounts.svc.plus
|
||||||
|
NEXT_PUBLIC_ACCOUNT_SERVICE_URL=https://accounts.svc.plus
|
||||||
|
SERVER_SERVICE_URL=https://api.svc.plus
|
||||||
|
NEXT_PUBLIC_SERVER_SERVICE_URL=https://api.svc.plus
|
||||||
|
SERVER_SERVICE_INTERNAL_URL=
|
||||||
|
|
||||||
|
# Optional integrations
|
||||||
|
OPENCLAW_GATEWAY_REMOTE_URL=
|
||||||
|
OPENCLAW_GATEWAY_TOKEN=
|
||||||
|
VAULT_SERVER_URL=
|
||||||
|
VAULT_NAMESPACE=
|
||||||
|
VAULT_TOKEN=
|
||||||
|
APISIX_AI_GATEWAY_URL=
|
||||||
|
AI_GATEWAY_ACCESS_TOKEN=
|
||||||
|
INTERNAL_SERVICE_TOKEN=
|
||||||
|
CLOUDFLARE_API_TOKEN=
|
||||||
|
CLOUDFLARE_ACCOUNT_ID=
|
||||||
|
CLOUDFLARE_WEB_ANALYTICS_SITE_TAG=
|
||||||
|
CLOUDFLARE_ZONE_TAG=
|
||||||
|
ROOT_EMAIL_WHITELIST=admin@svc.plus
|
||||||
|
NEXT_PUBLIC_PAYPAL_CLIENT_ID=
|
||||||
|
NEXT_PUBLIC_GISCUS_REPO=cloud-neutral-toolkit/console.svc.plus
|
||||||
|
NEXT_PUBLIC_GISCUS_REPO_ID=
|
||||||
|
NEXT_PUBLIC_GISCUS_CATEGORY=General
|
||||||
|
NEXT_PUBLIC_GISCUS_CATEGORY_ID=
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO=
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION=
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO=
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION=
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO=
|
||||||
|
NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION=
|
||||||
32
deploy/single-node/Caddyfile
Normal file
32
deploy/single-node/Caddyfile
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{$PRIMARY_DOMAIN}, {$SECONDARY_DOMAIN} {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
@secondary host {$SECONDARY_DOMAIN}
|
||||||
|
redir @secondary https://{$PRIMARY_DOMAIN}{uri} permanent
|
||||||
|
|
||||||
|
@next_static path /_next/static/*
|
||||||
|
handle @next_static {
|
||||||
|
root * /srv
|
||||||
|
header Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
@public_assets {
|
||||||
|
file {
|
||||||
|
root /srv/public
|
||||||
|
try_files {path}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handle @public_assets {
|
||||||
|
root * /srv/public
|
||||||
|
header Cache-Control "public, max-age=3600"
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
reverse_proxy dashboard:3000 {
|
||||||
|
header_up Host {host}
|
||||||
|
header_up X-Forwarded-Host {host}
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
header_up X-Forwarded-For {remote_host}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
deploy/single-node/docker-compose.yml
Normal file
54
deploy/single-node/docker-compose.yml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
services:
|
||||||
|
frontend-assets:
|
||||||
|
image: ${FRONTEND_IMAGE:?set FRONTEND_IMAGE in .env.runtime}
|
||||||
|
restart: "no"
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -eu
|
||||||
|
rm -rf /assets/_next /assets/public
|
||||||
|
mkdir -p /assets/_next /assets/public
|
||||||
|
cp -R /app/dashboard/static /assets/_next/static
|
||||||
|
cp -R /app/dashboard/public/. /assets/public
|
||||||
|
volumes:
|
||||||
|
- frontend_static:/assets
|
||||||
|
|
||||||
|
dashboard:
|
||||||
|
image: ${FRONTEND_IMAGE:?set FRONTEND_IMAGE in .env.runtime}
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env.runtime
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 3000
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: caddy:2.10-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- dashboard
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
environment:
|
||||||
|
PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:?set PRIMARY_DOMAIN in .env.runtime}
|
||||||
|
SECONDARY_DOMAIN: ${SECONDARY_DOMAIN:?set SECONDARY_DOMAIN in .env.runtime}
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- frontend_static:/srv:ro
|
||||||
|
- caddy_data:/data
|
||||||
|
- caddy_config:/config
|
||||||
|
networks:
|
||||||
|
- frontend
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
frontend_static:
|
||||||
|
caddy_data:
|
||||||
|
caddy_config:
|
||||||
@ -1,31 +1,28 @@
|
|||||||
# Deployment
|
# Deployment
|
||||||
|
|
||||||
This repository primarily delivers a web frontend experience and should document product flows, UI boundaries, and integration touchpoints.
|
## Production Baseline
|
||||||
|
|
||||||
Use this page to standardize deployment prerequisites, supported topologies, operational checks, and rollback notes.
|
- Runtime: `Caddy + Docker Compose`
|
||||||
|
- Deploy host: `47.120.61.35`
|
||||||
|
- Domains:
|
||||||
|
- `cn.svc.plus`
|
||||||
|
- `cn.onwalk.net`
|
||||||
|
- Frontend release workflow: `.github/workflows/service_release_frontend-deploy.yml`
|
||||||
|
|
||||||
## Current code-aligned notes
|
## Operating Model
|
||||||
|
|
||||||
- Documentation target: `console.svc.plus`
|
The frontend is built in GitHub Actions and shipped as a prebuilt `linux/amd64` image. The host only pulls the image and starts containers; it does not build locally.
|
||||||
- Repo kind: `frontend`
|
|
||||||
- Manifest and build evidence: package.json (`dashboard`)
|
|
||||||
- Primary implementation and ops directories: `src/`, `scripts/`, `tests/`, `config/`, `public/`
|
|
||||||
- Package scripts snapshot: `dev`, `prebuild`, `build`, `build:static`, `start`, `lint`
|
|
||||||
|
|
||||||
## Existing docs to reconcile
|
The stack is static-first:
|
||||||
|
|
||||||
|
- Caddy serves `/_next/static/*` and public assets from a shared volume.
|
||||||
|
- The Next.js standalone container serves dynamic HTML, auth endpoints, and API proxy routes.
|
||||||
|
- `knowledge/` is cloned in CI and packed into the image during the Docker build.
|
||||||
|
|
||||||
|
This baseline is intentional for the current weak-IO single-node host. If `docs.svc.plus` becomes an API-backed service later, update this page and the runbook to remove docs payload from the frontend image.
|
||||||
|
|
||||||
|
## Related Docs
|
||||||
|
|
||||||
- `development/dev-setup.md`
|
|
||||||
- `getting-started/installation.md`
|
|
||||||
- `getting-started/quickstart.md`
|
|
||||||
- `governance/release-process.md`
|
|
||||||
- `operations/runbooks/README.md`
|
|
||||||
- `operations/runbooks/rag-server.md`
|
|
||||||
- `usage/deployment.md`
|
- `usage/deployment.md`
|
||||||
- `zh/development/dev-setup.md`
|
- `governance/release-process.md`
|
||||||
|
- `development/dev-setup.md`
|
||||||
## What this page should cover next
|
|
||||||
|
|
||||||
- Describe the current implementation rather than an aspirational future-only design.
|
|
||||||
- Keep terminology aligned with the repository root README, manifests, and actual directories.
|
|
||||||
- Link deeper runbooks, specs, or subsystem notes from the legacy docs listed above.
|
|
||||||
- Verify deployment steps against current scripts, manifests, CI/CD flow, and environment contracts before each release.
|
|
||||||
|
|||||||
281
docs/plans/2026-03-18-frontend-single-node-deploy.md
Normal file
281
docs/plans/2026-03-18-frontend-single-node-deploy.md
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
# Console Frontend Single-Node Deployment Design
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Repository: `console.svc.plus`
|
||||||
|
- Target host: `root@47.120.61.35`
|
||||||
|
- Public domains:
|
||||||
|
- `cn.svc.plus`
|
||||||
|
- `cn.onwalk.net`
|
||||||
|
- Delivery mode: `GitHub Actions + GHCR + Caddy + Docker Compose`
|
||||||
|
|
||||||
|
This document defines the deployment baseline for the China-facing frontend node. The source of truth is this upstream repository. The control-plane repository may consume the repo through git submodule, but should not become the primary place where this deployment design lives.
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Provide an independent frontend deployment pipeline for `console.svc.plus` that fits the current host constraints:
|
||||||
|
|
||||||
|
- the host IO is weak
|
||||||
|
- the host must not build Docker images locally
|
||||||
|
- the frontend should run in a static-first mode where possible
|
||||||
|
- deployment logic should stay in checked-in scripts, not be embedded in GitHub Actions YAML
|
||||||
|
|
||||||
|
The result should support repeatable releases, quick rollback by image tag, and minimal work on the target machine.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
### Host constraints
|
||||||
|
|
||||||
|
- `47.120.61.35` is a single-node host
|
||||||
|
- deployment user is `root`
|
||||||
|
- local image build on the host is explicitly disallowed
|
||||||
|
- IO pressure should be minimized during release
|
||||||
|
|
||||||
|
### Application constraints
|
||||||
|
|
||||||
|
- `console.svc.plus` is not a purely static site
|
||||||
|
- auth routes, same-origin API proxy routes, and selected dynamic pages still require a running Next.js server
|
||||||
|
- some `NEXT_PUBLIC_*` variables are compiled into the frontend bundle at image build time
|
||||||
|
- `prebuild` pulls documentation and `knowledge` content, so CI must prepare those inputs before building the image
|
||||||
|
|
||||||
|
### Repository constraints
|
||||||
|
|
||||||
|
- workflow YAML should remain orchestration-only
|
||||||
|
- service-local operational notes should remain in this repo
|
||||||
|
- downstream control repos can reference this repo through submodule updates after upstream changes are pushed
|
||||||
|
|
||||||
|
## Recommended Topology
|
||||||
|
|
||||||
|
### 1. CI build on GitHub Actions
|
||||||
|
|
||||||
|
The workflow builds a single `linux/amd64` image in GitHub Actions and pushes it to GHCR.
|
||||||
|
|
||||||
|
Reasons:
|
||||||
|
|
||||||
|
- matches the target host architecture
|
||||||
|
- avoids multi-arch overhead for this single-node release path
|
||||||
|
- avoids local host build IO and CPU pressure
|
||||||
|
- keeps release artifacts immutable and rollback-friendly
|
||||||
|
|
||||||
|
### 2. Runtime on the host
|
||||||
|
|
||||||
|
Use `docker compose` with three services:
|
||||||
|
|
||||||
|
- `dashboard`: Next.js standalone runtime
|
||||||
|
- `frontend-assets`: one-shot container that copies static files from the image into a Docker volume
|
||||||
|
- `caddy`: TLS termination, redirect handling, static file serving, and reverse proxy
|
||||||
|
|
||||||
|
This keeps the host work limited to:
|
||||||
|
|
||||||
|
- image pull
|
||||||
|
- asset extraction from the image
|
||||||
|
- container restart
|
||||||
|
|
||||||
|
### 3. Static-first request flow
|
||||||
|
|
||||||
|
Caddy serves:
|
||||||
|
|
||||||
|
- `/_next/static/*`
|
||||||
|
- checked-in `public/` assets
|
||||||
|
|
||||||
|
Next.js serves:
|
||||||
|
|
||||||
|
- HTML responses
|
||||||
|
- `/api/*` routes
|
||||||
|
- auth/session flows
|
||||||
|
- dynamic pages that still depend on server runtime
|
||||||
|
|
||||||
|
This reduces repeat disk reads and network hops for the bulk of frontend traffic while preserving the dynamic behavior the app still needs.
|
||||||
|
|
||||||
|
## Build-Time vs Runtime Configuration
|
||||||
|
|
||||||
|
### Build-time config
|
||||||
|
|
||||||
|
These values must be available during Docker build because the frontend bundle reads them directly:
|
||||||
|
|
||||||
|
- `NEXT_PUBLIC_APP_BASE_URL`
|
||||||
|
- `NEXT_PUBLIC_SITE_URL`
|
||||||
|
- `NEXT_PUBLIC_LOGIN_URL`
|
||||||
|
- `NEXT_PUBLIC_DOCS_BASE_URL`
|
||||||
|
- `NEXT_PUBLIC_RUNTIME_ENVIRONMENT`
|
||||||
|
- `NEXT_PUBLIC_RUNTIME_REGION`
|
||||||
|
- `NEXT_PUBLIC_GISCUS_*`
|
||||||
|
- `NEXT_PUBLIC_PAYPAL_CLIENT_ID`
|
||||||
|
- `NEXT_PUBLIC_STRIPE_*`
|
||||||
|
|
||||||
|
These are injected in GitHub Actions as Docker build args.
|
||||||
|
|
||||||
|
### Runtime config
|
||||||
|
|
||||||
|
These values are rendered into `.env.runtime` and copied to the host:
|
||||||
|
|
||||||
|
- upstream service URLs such as `ACCOUNT_SERVICE_URL`
|
||||||
|
- tokens used only on the server side
|
||||||
|
- Cloudflare analytics credentials
|
||||||
|
- internal service token
|
||||||
|
- runtime hostname hints
|
||||||
|
|
||||||
|
This separation avoids rebuilding for purely server-side secret or endpoint changes when the public frontend bundle does not change.
|
||||||
|
|
||||||
|
## Knowledge and Docs Handling
|
||||||
|
|
||||||
|
Current decision:
|
||||||
|
|
||||||
|
- `knowledge/` is cloned during CI
|
||||||
|
- the cloned content is included in the image build context
|
||||||
|
- the built image contains the resulting content needed by the current frontend
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
|
||||||
|
- `prebuild` depends on this material
|
||||||
|
- the host should not fetch or generate content during deployment
|
||||||
|
|
||||||
|
Temporary nature:
|
||||||
|
|
||||||
|
- today the frontend still carries docs-related payload
|
||||||
|
- later, when `docs.svc.plus` becomes an API/service, docs delivery should move out of the frontend image
|
||||||
|
- that future change should reduce image size and simplify the runtime responsibilities of `console.svc.plus`
|
||||||
|
|
||||||
|
## Domain Handling
|
||||||
|
|
||||||
|
Primary domain:
|
||||||
|
|
||||||
|
- `cn.svc.plus`
|
||||||
|
|
||||||
|
Secondary domain:
|
||||||
|
|
||||||
|
- `cn.onwalk.net`
|
||||||
|
|
||||||
|
Current routing decision:
|
||||||
|
|
||||||
|
- Caddy accepts both domains
|
||||||
|
- requests for `cn.onwalk.net` are redirected permanently to `cn.svc.plus`
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
|
||||||
|
- avoid duplicate canonical origins
|
||||||
|
- keep cookie and login behavior centered on one primary host
|
||||||
|
- simplify SEO and observability interpretation
|
||||||
|
|
||||||
|
## Release Workflow
|
||||||
|
|
||||||
|
### Trigger
|
||||||
|
|
||||||
|
Independent workflow:
|
||||||
|
|
||||||
|
- `.github/workflows/service_release_frontend-deploy.yml`
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. check out repository
|
||||||
|
2. clone `knowledge`
|
||||||
|
3. build and push `ghcr.io/<owner>/dashboard:<tag>`
|
||||||
|
4. render `.env.runtime`
|
||||||
|
5. upload compose/caddy/env files to the host
|
||||||
|
6. log in to GHCR on the host
|
||||||
|
7. pull the new image
|
||||||
|
8. run `frontend-assets`
|
||||||
|
9. start or refresh `dashboard` and `caddy`
|
||||||
|
10. verify both domains
|
||||||
|
|
||||||
|
### Why separate from the existing image workflow
|
||||||
|
|
||||||
|
The existing image workflow is broader and oriented toward generic image publishing. This single-node frontend workflow needs tighter control over:
|
||||||
|
|
||||||
|
- build-time public env injection
|
||||||
|
- production deployment sequencing
|
||||||
|
- SSH-based single-host rollout
|
||||||
|
- host-specific runtime file rendering
|
||||||
|
|
||||||
|
So the frontend release path should remain explicit and independent.
|
||||||
|
|
||||||
|
## Rollback Model
|
||||||
|
|
||||||
|
Rollback unit:
|
||||||
|
|
||||||
|
- image tag reference in `.env.runtime`
|
||||||
|
|
||||||
|
Rollback steps:
|
||||||
|
|
||||||
|
1. set `FRONTEND_IMAGE` to a previous known-good tag
|
||||||
|
2. rerun `frontend-assets`
|
||||||
|
3. restart `dashboard` and `caddy`
|
||||||
|
4. verify `cn.svc.plus`
|
||||||
|
|
||||||
|
This avoids rebuilding and keeps rollback cheap on the weak-IO host.
|
||||||
|
|
||||||
|
## Security and Secret Handling
|
||||||
|
|
||||||
|
Secrets must not be committed to the repo. The workflow should consume:
|
||||||
|
|
||||||
|
- `FRONTEND_DEPLOY_SSH_KEY`
|
||||||
|
- service tokens
|
||||||
|
- vault tokens
|
||||||
|
- internal service token
|
||||||
|
- optional Cloudflare credentials
|
||||||
|
|
||||||
|
Public defaults and non-secret values belong in checked-in examples or GitHub repository/environment variables. Secret-only values stay in GitHub Secrets and are rendered into the host runtime env during deployment.
|
||||||
|
|
||||||
|
## Operational Risks
|
||||||
|
|
||||||
|
### Risk 1: build-time public env mismatch
|
||||||
|
|
||||||
|
If GitHub environment variables are incomplete, the image may build successfully but the frontend can render wrong links or lose third-party integration IDs.
|
||||||
|
|
||||||
|
Mitigation:
|
||||||
|
|
||||||
|
- keep `.env.example` aligned
|
||||||
|
- document required GitHub `vars`
|
||||||
|
- keep the build args list explicit
|
||||||
|
|
||||||
|
### Risk 2: image layout drift
|
||||||
|
|
||||||
|
If the Docker image no longer contains `/app/dashboard/static` or `/app/dashboard/public`, the `frontend-assets` step fails.
|
||||||
|
|
||||||
|
Mitigation:
|
||||||
|
|
||||||
|
- keep asset extraction paths documented
|
||||||
|
- update deploy scripts whenever Dockerfile output layout changes
|
||||||
|
|
||||||
|
### Risk 3: docs payload growth
|
||||||
|
|
||||||
|
Bundling docs and `knowledge` into the frontend image increases image size.
|
||||||
|
|
||||||
|
Mitigation:
|
||||||
|
|
||||||
|
- accept it temporarily
|
||||||
|
- revisit once `docs.svc.plus` is externalized
|
||||||
|
|
||||||
|
### Risk 4: single-node blast radius
|
||||||
|
|
||||||
|
The host handles both reverse proxy and app runtime. Misconfiguration affects the whole frontend surface.
|
||||||
|
|
||||||
|
Mitigation:
|
||||||
|
|
||||||
|
- keep compose simple
|
||||||
|
- keep Caddy config minimal
|
||||||
|
- use image-tag rollback
|
||||||
|
|
||||||
|
## Future Follow-Up
|
||||||
|
|
||||||
|
### Near term
|
||||||
|
|
||||||
|
- populate required GitHub `vars` and `secrets`
|
||||||
|
- run the workflow against `47.120.61.35`
|
||||||
|
- validate DNS, TLS, static assets, login flow, and upstream API proxy behavior
|
||||||
|
|
||||||
|
### Later
|
||||||
|
|
||||||
|
- move docs delivery out of the frontend image after `docs.svc.plus` is service/API based
|
||||||
|
- consider splitting static assets to object storage or CDN if traffic grows
|
||||||
|
- evaluate whether the host should keep only Caddy plus one app container, or whether docs can be removed entirely from this runtime
|
||||||
|
|
||||||
|
## Source of Truth Rule
|
||||||
|
|
||||||
|
For this deployment design:
|
||||||
|
|
||||||
|
- upstream repo source of truth: `console.svc.plus`
|
||||||
|
- service-local design note location: `docs/plans/`
|
||||||
|
- control-plane repo role: consume via git submodule after upstream commit is pushed
|
||||||
|
|
||||||
|
Do not move the primary design ownership to the control-plane repository.
|
||||||
5
docs/plans/README.md
Normal file
5
docs/plans/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Plans
|
||||||
|
|
||||||
|
This directory stores service-local design notes and implementation plans for `console.svc.plus`.
|
||||||
|
|
||||||
|
The source of truth stays in this upstream repository. Control-plane repositories may reference these documents through git submodule updates after upstream changes are pushed.
|
||||||
@ -3,146 +3,142 @@
|
|||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
- Runtime: `console.svc.plus`
|
- Runtime: `console.svc.plus`
|
||||||
- Frontend host: Vercel
|
- Topology: `Caddy + Docker Compose + GitHub Actions`
|
||||||
- Edge: Cloudflare
|
- Deploy host: `root@47.120.61.35`
|
||||||
- Auth backend: `https://accounts.svc.plus`
|
- Public domains:
|
||||||
|
- `https://cn.svc.plus`
|
||||||
|
- `https://cn.onwalk.net`
|
||||||
|
- Primary origin: `https://cn.svc.plus`
|
||||||
|
|
||||||
This runbook is the minimum checklist for production incidents where login or MFA stops working and browser devtools show `/api/auth/login` or `/api/auth/mfa/*` failures.
|
## Current Delivery Model
|
||||||
|
|
||||||
## Expected Request Flow
|
The production frontend is deployed as a prebuilt container image from GitHub Actions.
|
||||||
|
|
||||||
1. Browser loads `https://console.svc.plus/login`
|
- The target host does not build images locally.
|
||||||
2. Browser calls same-origin Next routes on `console.svc.plus`
|
- The workflow builds an `linux/amd64` image and pushes it to `ghcr.io/<owner>/dashboard:<sha>`.
|
||||||
3. Next route proxies server-side to `https://accounts.svc.plus/api/auth/*`
|
- The host only performs `docker login`, `docker compose pull`, static asset extraction, and `docker compose up`.
|
||||||
4. `accounts.svc.plus` returns either a session token or an MFA challenge
|
- `knowledge/` is cloned during CI build and packed into the image.
|
||||||
|
- Static assets are extracted from the image into a shared Docker volume so Caddy can serve `/_next/static/*` and checked-in public files directly.
|
||||||
|
|
||||||
The browser should not call `accounts.svc.plus` directly for login.
|
This is intentionally static-first for the current weak-IO single-node host. Dynamic HTML, auth routes, and API proxy routes still run through the Next.js container. When `docs.svc.plus` is later split into an API/service, revisit this runbook and remove docs content from the frontend image.
|
||||||
|
|
||||||
## Fast Triage
|
## Runtime Layout
|
||||||
|
|
||||||
Run these checks first:
|
Remote directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -si https://console.svc.plus/login | sed -n '1,20p'
|
/opt/console-svc-plus
|
||||||
curl -si https://console.svc.plus/api/auth/login | sed -n '1,20p'
|
|
||||||
curl -si https://accounts.svc.plus/healthz | sed -n '1,20p'
|
|
||||||
curl -si https://accounts.svc.plus/api/auth/login | sed -n '1,20p'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Interpretation:
|
Files deployed there:
|
||||||
|
|
||||||
- `console.svc.plus` returns `403` with `cf-mitigated: challenge`
|
```bash
|
||||||
Cloudflare is blocking the page or auth API before Vercel sees it.
|
docker-compose.yml
|
||||||
- `console.svc.plus/api/auth/login` returns `404`
|
Caddyfile
|
||||||
Vercel production is not serving the expected Next route, or Cloudflare is pointing at the wrong origin/deployment behavior.
|
.env.runtime
|
||||||
- `accounts.svc.plus/healthz` fails
|
```
|
||||||
Back-end outage. Fix backend first.
|
|
||||||
- `accounts.svc.plus/api/auth/login` returns `200` with `mfaRequired`
|
|
||||||
Backend is healthy; continue on console/Vercel/Cloudflare.
|
|
||||||
|
|
||||||
## Application Checks
|
Containers:
|
||||||
|
|
||||||
Verify the current build still contains the auth routes:
|
- `dashboard`: Next.js standalone runtime on port `3000`
|
||||||
|
- `frontend-assets`: one-shot task that copies `static/` and `public/` into a shared volume
|
||||||
|
- `caddy`: TLS termination and reverse proxy
|
||||||
|
|
||||||
|
## GitHub Actions Inputs
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.github/workflows/service_release_frontend-deploy.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Secrets required:
|
||||||
|
|
||||||
|
- `FRONTEND_DEPLOY_SSH_KEY`
|
||||||
|
- `OPENCLAW_GATEWAY_TOKEN` if used
|
||||||
|
- `VAULT_TOKEN` if used
|
||||||
|
- `AI_GATEWAY_ACCESS_TOKEN` if used
|
||||||
|
- `INTERNAL_SERVICE_TOKEN` if used
|
||||||
|
- `CLOUDFLARE_API_TOKEN` if used
|
||||||
|
|
||||||
|
Repository/environment variables recommended:
|
||||||
|
|
||||||
|
- `APP_BASE_URL`
|
||||||
|
- `NEXT_PUBLIC_APP_BASE_URL`
|
||||||
|
- `NEXT_PUBLIC_SITE_URL`
|
||||||
|
- `NEXT_PUBLIC_LOGIN_URL`
|
||||||
|
- `NEXT_PUBLIC_DOCS_BASE_URL`
|
||||||
|
- `ACCOUNT_SERVICE_URL`
|
||||||
|
- `NEXT_PUBLIC_ACCOUNT_SERVICE_URL`
|
||||||
|
- `SERVER_SERVICE_URL`
|
||||||
|
- `NEXT_PUBLIC_SERVER_SERVICE_URL`
|
||||||
|
- `RUNTIME_HOSTNAME`
|
||||||
|
- `DEPLOYMENT_HOSTNAME`
|
||||||
|
- `NEXT_PUBLIC_RUNTIME_ENVIRONMENT`
|
||||||
|
- `NEXT_PUBLIC_RUNTIME_REGION`
|
||||||
|
- `NEXT_PUBLIC_GISCUS_*`
|
||||||
|
- `NEXT_PUBLIC_STRIPE_*`
|
||||||
|
- `NEXT_PUBLIC_PAYPAL_CLIENT_ID`
|
||||||
|
|
||||||
|
## Release Flow
|
||||||
|
|
||||||
|
1. GitHub Actions checks out the repo.
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## Verification Commands
|
||||||
|
|
||||||
|
Local syntax checks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/console.svc.plus
|
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/console.svc.plus
|
||||||
yarn build
|
bash -n scripts/github-actions/render-frontend-runtime-env.sh
|
||||||
cat .next/app-path-routes-manifest.json | jq 'with_entries(select(.key|test("/api/auth/")))'
|
bash -n scripts/github-actions/deploy-frontend-single-node.sh
|
||||||
|
cp deploy/single-node/.env.runtime.example deploy/single-node/.env.runtime
|
||||||
|
docker compose -f deploy/single-node/docker-compose.yml --env-file deploy/single-node/.env.runtime config >/tmp/console-compose.rendered.yaml
|
||||||
|
rm -f deploy/single-node/.env.runtime
|
||||||
|
python3 - <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import yaml
|
||||||
|
yaml.safe_load(Path('.github/workflows/service_release_frontend-deploy.yml').read_text())
|
||||||
|
print('workflow yaml ok')
|
||||||
|
PY
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify the login page still uses same-origin routes:
|
Remote checks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nl -ba 'src/app/(auth)/login/LoginForm.tsx' | sed -n '64,180p'
|
ssh root@47.120.61.35 "cd /opt/console-svc-plus && docker compose --env-file .env.runtime ps"
|
||||||
nl -ba 'src/app/api/auth/login/route.ts' | sed -n '1,180p'
|
ssh root@47.120.61.35 "curl -fsSI -H 'Host: cn.svc.plus' http://127.0.0.1/"
|
||||||
nl -ba 'src/app/api/auth/mfa/verify/route.ts' | sed -n '1,180p'
|
curl -fsSIL https://cn.svc.plus
|
||||||
|
curl -fsSIL https://cn.onwalk.net
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected behavior:
|
## Failure Signatures
|
||||||
|
|
||||||
- `LoginForm` posts to `/api/auth/login`
|
- `docker login ghcr.io` fails
|
||||||
- login proxy accepts backend `mfaRequired` / `mfaTicket`
|
The workflow token or package visibility is wrong.
|
||||||
- MFA verify proxy calls `/api/auth/mfa/verify`
|
- `frontend-assets` fails
|
||||||
|
The image layout changed and no longer contains `/app/dashboard/static` or `/app/dashboard/public`.
|
||||||
|
- `cn.svc.plus` returns `502`
|
||||||
|
Caddy is up, but the `dashboard` container failed or is not reachable on port `3000`.
|
||||||
|
- `cn.onwalk.net` does not redirect
|
||||||
|
Check the deployed `Caddyfile` and domain DNS.
|
||||||
|
|
||||||
## Vercel Checks
|
## Rollback
|
||||||
|
|
||||||
In the Vercel project for `console-svc-plus`, verify:
|
1. Re-run the workflow with a previous known-good image tag.
|
||||||
|
2. Or update `/opt/console-svc-plus/.env.runtime` and set `FRONTEND_IMAGE=ghcr.io/<owner>/dashboard:<previous-tag>`.
|
||||||
1. The production deployment corresponds to the intended git commit.
|
3. Restart the services:
|
||||||
2. Framework preset is `Next.js`.
|
|
||||||
3. Build command is `yarn build` or the project default, not a static export command.
|
|
||||||
4. Output is not being overridden to static export.
|
|
||||||
5. Production Functions include `app/api/auth/login` and the other `app/api/auth/*` handlers.
|
|
||||||
6. Required runtime env vars are present for the auth proxy path if they are managed in Vercel.
|
|
||||||
|
|
||||||
If the route exists locally but Vercel returns `404`, suspect:
|
|
||||||
|
|
||||||
- wrong production deployment selected
|
|
||||||
- wrong root directory/project link
|
|
||||||
- stale alias or domain assignment
|
|
||||||
- build output mismatch between local and Vercel
|
|
||||||
|
|
||||||
## Cloudflare Checks
|
|
||||||
|
|
||||||
If `curl` shows `cf-mitigated: challenge`, check Cloudflare first.
|
|
||||||
|
|
||||||
Look for:
|
|
||||||
|
|
||||||
1. Managed Challenge or WAF custom rules affecting `/login`
|
|
||||||
2. Managed Challenge or WAF custom rules affecting `/api/auth/*`
|
|
||||||
3. Bot Fight Mode or Super Bot Fight Mode interactions
|
|
||||||
4. Transform/redirect/cache rules that alter `/api/auth/*`
|
|
||||||
5. Page Rules or Ruleset Engine policies applied only to the production hostname
|
|
||||||
|
|
||||||
Recommended policy for auth API:
|
|
||||||
|
|
||||||
- Do not cache `/api/auth/*`
|
|
||||||
- Do not apply JS challenge to `/api/auth/*`
|
|
||||||
- Keep standard security headers, but let requests reach Vercel
|
|
||||||
|
|
||||||
## Backend Verification
|
|
||||||
|
|
||||||
Use the backend directly to prove whether auth is healthy:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/accounts.svc.plus
|
ssh root@47.120.61.35 "cd /opt/console-svc-plus && docker compose --env-file .env.runtime run --rm frontend-assets"
|
||||||
set -a; source .env; set +a
|
ssh root@47.120.61.35 "cd /opt/console-svc-plus && docker compose --env-file .env.runtime up -d dashboard caddy"
|
||||||
payload=$(printf '{"identifier":"admin@svc.plus","password":"%s"}' "$SUPERADMIN_PASSWORD")
|
|
||||||
curl -sS -X POST https://accounts.svc.plus/api/auth/login \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d "$payload"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected for an MFA-enabled admin:
|
4. Verify `https://cn.svc.plus` again before closing the incident.
|
||||||
|
|
||||||
- HTTP `200`
|
|
||||||
- response contains `mfaRequired`
|
|
||||||
- response contains `mfaTicket` or `mfaToken`
|
|
||||||
|
|
||||||
## Known Failure Signatures
|
|
||||||
|
|
||||||
- `POST https://console.svc.plus/api/auth/login 404`
|
|
||||||
Likely Vercel deployment mismatch or route not published.
|
|
||||||
- `403` with `cf-mitigated: challenge`
|
|
||||||
Cloudflare blocked request before Vercel.
|
|
||||||
- login returns generic failure even though backend returns MFA challenge
|
|
||||||
Console auth proxy is not parsing MFA fields correctly.
|
|
||||||
- MFA code accepted by authenticator but web login still fails
|
|
||||||
Console proxy may be calling the setup endpoint instead of the login MFA endpoint.
|
|
||||||
|
|
||||||
## Rollback Strategy
|
|
||||||
|
|
||||||
When a release breaks auth:
|
|
||||||
|
|
||||||
1. Remove or relax Cloudflare rules affecting `/login` and `/api/auth/*`
|
|
||||||
2. Re-point domain to last known-good Vercel production deployment
|
|
||||||
3. Roll back `console.svc.plus`
|
|
||||||
4. Only then consider `accounts.svc.plus` rollback
|
|
||||||
|
|
||||||
## Related Files
|
|
||||||
|
|
||||||
- `src/app/(auth)/login/LoginForm.tsx`
|
|
||||||
- `src/app/api/auth/login/route.ts`
|
|
||||||
- `src/app/api/auth/mfa/status/route.ts`
|
|
||||||
- `src/app/api/auth/mfa/verify/route.ts`
|
|
||||||
- `src/server/serviceConfig.ts`
|
|
||||||
|
|||||||
@ -1,31 +1,28 @@
|
|||||||
# 部署
|
# 部署
|
||||||
|
|
||||||
该仓库以 Web 前端体验为主,文档需要覆盖产品流程、界面边界与集成触点。
|
## 生产基线
|
||||||
|
|
||||||
本页用于统一部署前提、支持的拓扑、运维检查项与回滚注意事项。
|
- 运行拓扑: `Caddy + Docker Compose`
|
||||||
|
- 目标主机: `47.120.61.35`
|
||||||
|
- 域名:
|
||||||
|
- `cn.svc.plus`
|
||||||
|
- `cn.onwalk.net`
|
||||||
|
- 前端独立发布流水线: `.github/workflows/service_release_frontend-deploy.yml`
|
||||||
|
|
||||||
## 与当前代码对齐的说明
|
## 运行方式
|
||||||
|
|
||||||
- 文档目标仓库: `console.svc.plus`
|
前端镜像在 GitHub Actions 中完成构建并推送到镜像仓库,目标主机只负责拉取镜像和启动容器,不在机器上本地构建。
|
||||||
- 仓库类型: `frontend`
|
|
||||||
- 构建与运行依据: package.json (`dashboard`)
|
|
||||||
- 主要实现与运维目录: `src/`, `scripts/`, `tests/`, `config/`, `public/`
|
|
||||||
- `package.json` 脚本快照: `dev`, `prebuild`, `build`, `build:static`, `start`, `lint`
|
|
||||||
|
|
||||||
## 需要继续归并的现有文档
|
当前方案尽量以静态模式运行:
|
||||||
|
|
||||||
|
- Caddy 直接服务 `/_next/static/*` 与 `public/` 里的静态资源。
|
||||||
|
- Next.js standalone 容器只承接动态页面、认证接口和代理接口。
|
||||||
|
- `knowledge/` 在 CI 阶段拉取,并在 Docker 打包时直接写入镜像。
|
||||||
|
|
||||||
|
这是针对当前单机弱 IO 环境的权衡。后续如果 `docs.svc.plus` 被拆成独立 API 服务,需要同步调整这里和 `docs/usage/deployment.md` 的镜像内容与路由职责。
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
- `development/dev-setup.md`
|
|
||||||
- `getting-started/installation.md`
|
|
||||||
- `getting-started/quickstart.md`
|
|
||||||
- `governance/release-process.md`
|
|
||||||
- `operations/runbooks/README.md`
|
|
||||||
- `operations/runbooks/rag-server.md`
|
|
||||||
- `usage/deployment.md`
|
- `usage/deployment.md`
|
||||||
- `zh/development/dev-setup.md`
|
- `governance/release-process.md`
|
||||||
|
- `development/dev-setup.md`
|
||||||
## 本页下一步应补充的内容
|
|
||||||
|
|
||||||
- 先描述当前已落地实现,再补充未来规划,避免只写愿景不写现状。
|
|
||||||
- 术语需要与仓库根 README、构建清单和实际目录保持一致。
|
|
||||||
- 将上方列出的历史 runbook、spec、子系统说明逐步链接并归并到本页。
|
|
||||||
- 每次发布前,依据当前脚本、清单、CI/CD 流程和环境契约重新核对部署步骤。
|
|
||||||
|
|||||||
88
scripts/github-actions/deploy-frontend-single-node.sh
Executable file
88
scripts/github-actions/deploy-frontend-single-node.sh
Executable file
@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||||
|
DEPLOY_SOURCE_DIR="${REPO_ROOT}/deploy/single-node"
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
local key="$1"
|
||||||
|
local value="${!key-}"
|
||||||
|
if [[ -z "${value}" ]]; then
|
||||||
|
echo "Missing required environment variable: ${key}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env DEPLOY_HOST
|
||||||
|
require_env DEPLOY_USER
|
||||||
|
require_env DEPLOY_DIR
|
||||||
|
require_env SSH_PRIVATE_KEY
|
||||||
|
require_env GHCR_USERNAME
|
||||||
|
require_env GHCR_PASSWORD
|
||||||
|
require_env FRONTEND_IMAGE
|
||||||
|
require_env PRIMARY_DOMAIN
|
||||||
|
require_env SECONDARY_DOMAIN
|
||||||
|
|
||||||
|
WORK_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${WORK_DIR}"' EXIT
|
||||||
|
|
||||||
|
RUNTIME_ENV_FILE="${WORK_DIR}/.env.runtime"
|
||||||
|
RELEASE_ARCHIVE="${WORK_DIR}/console-svc-plus-release.tgz"
|
||||||
|
REMOTE_ARCHIVE="/tmp/console-svc-plus-release-${GITHUB_SHA:-manual}.tgz"
|
||||||
|
SSH_KEY_FILE="${WORK_DIR}/deploy.key"
|
||||||
|
KNOWN_HOSTS_FILE="${WORK_DIR}/known_hosts"
|
||||||
|
|
||||||
|
bash "${SCRIPT_DIR}/render-frontend-runtime-env.sh" "${RUNTIME_ENV_FILE}"
|
||||||
|
|
||||||
|
cp "${DEPLOY_SOURCE_DIR}/docker-compose.yml" "${WORK_DIR}/docker-compose.yml"
|
||||||
|
cp "${DEPLOY_SOURCE_DIR}/Caddyfile" "${WORK_DIR}/Caddyfile"
|
||||||
|
|
||||||
|
tar -C "${WORK_DIR}" -czf "${RELEASE_ARCHIVE}" \
|
||||||
|
docker-compose.yml \
|
||||||
|
Caddyfile \
|
||||||
|
.env.runtime
|
||||||
|
|
||||||
|
printf '%s\n' "${SSH_PRIVATE_KEY}" > "${SSH_KEY_FILE}"
|
||||||
|
chmod 600 "${SSH_KEY_FILE}"
|
||||||
|
ssh-keyscan -H "${DEPLOY_HOST}" > "${KNOWN_HOSTS_FILE}"
|
||||||
|
|
||||||
|
SSH_BASE=(
|
||||||
|
ssh
|
||||||
|
-i "${SSH_KEY_FILE}"
|
||||||
|
-o StrictHostKeyChecking=yes
|
||||||
|
-o UserKnownHostsFile="${KNOWN_HOSTS_FILE}"
|
||||||
|
"${DEPLOY_USER}@${DEPLOY_HOST}"
|
||||||
|
)
|
||||||
|
|
||||||
|
SCP_BASE=(
|
||||||
|
scp
|
||||||
|
-i "${SSH_KEY_FILE}"
|
||||||
|
-o StrictHostKeyChecking=yes
|
||||||
|
-o UserKnownHostsFile="${KNOWN_HOSTS_FILE}"
|
||||||
|
)
|
||||||
|
|
||||||
|
printf '%s' "${GHCR_PASSWORD}" | "${SSH_BASE[@]}" "docker login ghcr.io -u '${GHCR_USERNAME}' --password-stdin"
|
||||||
|
|
||||||
|
"${SCP_BASE[@]}" "${RELEASE_ARCHIVE}" "${DEPLOY_USER}@${DEPLOY_HOST}:${REMOTE_ARCHIVE}"
|
||||||
|
|
||||||
|
"${SSH_BASE[@]}" \
|
||||||
|
"DEPLOY_DIR='${DEPLOY_DIR}' REMOTE_ARCHIVE='${REMOTE_ARCHIVE}' PROJECT_NAME='console-svc-plus' bash -s" <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
tmp_dir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${tmp_dir}" "${REMOTE_ARCHIVE}"' EXIT
|
||||||
|
|
||||||
|
mkdir -p "${DEPLOY_DIR}"
|
||||||
|
tar -xzf "${REMOTE_ARCHIVE}" -C "${tmp_dir}"
|
||||||
|
|
||||||
|
install -m 0644 "${tmp_dir}/docker-compose.yml" "${DEPLOY_DIR}/docker-compose.yml"
|
||||||
|
install -m 0644 "${tmp_dir}/Caddyfile" "${DEPLOY_DIR}/Caddyfile"
|
||||||
|
install -m 0600 "${tmp_dir}/.env.runtime" "${DEPLOY_DIR}/.env.runtime"
|
||||||
|
|
||||||
|
cd "${DEPLOY_DIR}"
|
||||||
|
docker compose --project-name "${PROJECT_NAME}" --env-file .env.runtime pull dashboard caddy
|
||||||
|
docker compose --project-name "${PROJECT_NAME}" --env-file .env.runtime run --rm frontend-assets
|
||||||
|
docker compose --project-name "${PROJECT_NAME}" --env-file .env.runtime up -d --remove-orphans dashboard caddy
|
||||||
|
docker compose --project-name "${PROJECT_NAME}" --env-file .env.runtime ps
|
||||||
|
EOF
|
||||||
76
scripts/github-actions/render-frontend-runtime-env.sh
Executable file
76
scripts/github-actions/render-frontend-runtime-env.sh
Executable file
@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OUTPUT_PATH="${1:?usage: render-frontend-runtime-env.sh <output-path>}"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "${OUTPUT_PATH}")"
|
||||||
|
: > "${OUTPUT_PATH}"
|
||||||
|
|
||||||
|
append_env() {
|
||||||
|
local key="$1"
|
||||||
|
local value="${2-}"
|
||||||
|
printf '%s=%s\n' "${key}" "${value}" >> "${OUTPUT_PATH}"
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
local key="$1"
|
||||||
|
local value="${!key-}"
|
||||||
|
if [[ -z "${value}" ]]; then
|
||||||
|
echo "Missing required environment variable: ${key}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env FRONTEND_IMAGE
|
||||||
|
require_env PRIMARY_DOMAIN
|
||||||
|
require_env SECONDARY_DOMAIN
|
||||||
|
|
||||||
|
append_env FRONTEND_IMAGE "${FRONTEND_IMAGE}"
|
||||||
|
append_env PRIMARY_DOMAIN "${PRIMARY_DOMAIN}"
|
||||||
|
append_env SECONDARY_DOMAIN "${SECONDARY_DOMAIN}"
|
||||||
|
|
||||||
|
append_env NODE_ENV "production"
|
||||||
|
append_env PORT "${PORT:-3000}"
|
||||||
|
append_env RUNTIME_ENV "${RUNTIME_ENV:-prod}"
|
||||||
|
append_env REGION "${REGION:-cn}"
|
||||||
|
append_env APP_BASE_URL "${APP_BASE_URL:-https://${PRIMARY_DOMAIN}}"
|
||||||
|
append_env NEXT_PUBLIC_APP_BASE_URL "${NEXT_PUBLIC_APP_BASE_URL:-https://${PRIMARY_DOMAIN}}"
|
||||||
|
append_env NEXT_PUBLIC_SITE_URL "${NEXT_PUBLIC_SITE_URL:-https://${PRIMARY_DOMAIN}}"
|
||||||
|
append_env NEXT_PUBLIC_LOGIN_URL "${NEXT_PUBLIC_LOGIN_URL:-https://${PRIMARY_DOMAIN}/login}"
|
||||||
|
append_env NEXT_PUBLIC_DOCS_BASE_URL "${NEXT_PUBLIC_DOCS_BASE_URL:-https://${PRIMARY_DOMAIN}/docs}"
|
||||||
|
append_env SESSION_COOKIE_SECURE "${SESSION_COOKIE_SECURE:-true}"
|
||||||
|
append_env NEXT_PUBLIC_SESSION_COOKIE_SECURE "${NEXT_PUBLIC_SESSION_COOKIE_SECURE:-true}"
|
||||||
|
append_env RUNTIME_HOSTNAME "${RUNTIME_HOSTNAME:-${PRIMARY_DOMAIN}}"
|
||||||
|
append_env NEXT_RUNTIME_HOSTNAME "${NEXT_RUNTIME_HOSTNAME:-${PRIMARY_DOMAIN}}"
|
||||||
|
append_env DEPLOYMENT_HOSTNAME "${DEPLOYMENT_HOSTNAME:-${PRIMARY_DOMAIN}}"
|
||||||
|
append_env NEXT_PUBLIC_RUNTIME_ENVIRONMENT "${NEXT_PUBLIC_RUNTIME_ENVIRONMENT:-prod}"
|
||||||
|
append_env NEXT_PUBLIC_RUNTIME_REGION "${NEXT_PUBLIC_RUNTIME_REGION:-cn}"
|
||||||
|
append_env ACCOUNT_SERVICE_URL "${ACCOUNT_SERVICE_URL:-https://accounts.svc.plus}"
|
||||||
|
append_env NEXT_PUBLIC_ACCOUNT_SERVICE_URL "${NEXT_PUBLIC_ACCOUNT_SERVICE_URL:-${ACCOUNT_SERVICE_URL:-https://accounts.svc.plus}}"
|
||||||
|
append_env SERVER_SERVICE_URL "${SERVER_SERVICE_URL:-https://api.svc.plus}"
|
||||||
|
append_env NEXT_PUBLIC_SERVER_SERVICE_URL "${NEXT_PUBLIC_SERVER_SERVICE_URL:-${SERVER_SERVICE_URL:-https://api.svc.plus}}"
|
||||||
|
append_env SERVER_SERVICE_INTERNAL_URL "${SERVER_SERVICE_INTERNAL_URL-}"
|
||||||
|
append_env OPENCLAW_GATEWAY_REMOTE_URL "${OPENCLAW_GATEWAY_REMOTE_URL-}"
|
||||||
|
append_env OPENCLAW_GATEWAY_TOKEN "${OPENCLAW_GATEWAY_TOKEN-}"
|
||||||
|
append_env VAULT_SERVER_URL "${VAULT_SERVER_URL-}"
|
||||||
|
append_env VAULT_NAMESPACE "${VAULT_NAMESPACE-}"
|
||||||
|
append_env VAULT_TOKEN "${VAULT_TOKEN-}"
|
||||||
|
append_env APISIX_AI_GATEWAY_URL "${APISIX_AI_GATEWAY_URL-}"
|
||||||
|
append_env AI_GATEWAY_ACCESS_TOKEN "${AI_GATEWAY_ACCESS_TOKEN-}"
|
||||||
|
append_env INTERNAL_SERVICE_TOKEN "${INTERNAL_SERVICE_TOKEN-}"
|
||||||
|
append_env CLOUDFLARE_API_TOKEN "${CLOUDFLARE_API_TOKEN-}"
|
||||||
|
append_env CLOUDFLARE_ACCOUNT_ID "${CLOUDFLARE_ACCOUNT_ID-}"
|
||||||
|
append_env CLOUDFLARE_WEB_ANALYTICS_SITE_TAG "${CLOUDFLARE_WEB_ANALYTICS_SITE_TAG-}"
|
||||||
|
append_env CLOUDFLARE_ZONE_TAG "${CLOUDFLARE_ZONE_TAG-}"
|
||||||
|
append_env ROOT_EMAIL_WHITELIST "${ROOT_EMAIL_WHITELIST:-admin@svc.plus}"
|
||||||
|
append_env NEXT_PUBLIC_PAYPAL_CLIENT_ID "${NEXT_PUBLIC_PAYPAL_CLIENT_ID-}"
|
||||||
|
append_env NEXT_PUBLIC_GISCUS_REPO "${NEXT_PUBLIC_GISCUS_REPO:-cloud-neutral-toolkit/console.svc.plus}"
|
||||||
|
append_env NEXT_PUBLIC_GISCUS_REPO_ID "${NEXT_PUBLIC_GISCUS_REPO_ID-}"
|
||||||
|
append_env NEXT_PUBLIC_GISCUS_CATEGORY "${NEXT_PUBLIC_GISCUS_CATEGORY:-General}"
|
||||||
|
append_env NEXT_PUBLIC_GISCUS_CATEGORY_ID "${NEXT_PUBLIC_GISCUS_CATEGORY_ID-}"
|
||||||
|
append_env NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO "${NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_PAYGO-}"
|
||||||
|
append_env NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION "${NEXT_PUBLIC_STRIPE_PRICE_XSTREAM_SUBSCRIPTION-}"
|
||||||
|
append_env NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO "${NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO-}"
|
||||||
|
append_env NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION "${NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION-}"
|
||||||
|
append_env NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO "${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO-}"
|
||||||
|
append_env NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION "${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION-}"
|
||||||
Loading…
Reference in New Issue
Block a user