Align frontend release contract across www and console domains

This commit is contained in:
Haitao Pan 2026-04-12 17:55:14 +08:00
parent 22e95e5bcb
commit d054b35116
23 changed files with 261 additions and 111 deletions

View File

@ -7,12 +7,15 @@ on:
paths: paths:
- ".github/workflows/pipeline.yaml" - ".github/workflows/pipeline.yaml"
- "Dockerfile" - "Dockerfile"
- "deploy/single-node/**"
- "package.json" - "package.json"
- "yarn.lock" - "yarn.lock"
- "scripts/github-actions/build-and-push-frontend-image.sh" - "scripts/github-actions/build-and-push-frontend-image.sh"
- "scripts/github-actions/compute-frontend-release-metadata.sh" - "scripts/github-actions/compute-frontend-release-metadata.sh"
- "scripts/github-actions/render-frontend-build-args.sh" - "scripts/github-actions/render-frontend-build-args.sh"
- "scripts/github-actions/render-frontend-runtime-env.sh"
- "scripts/github-actions/prepare-frontend-build-context.sh" - "scripts/github-actions/prepare-frontend-build-context.sh"
- "scripts/github-actions/run-console-deploy-playbook.sh"
- "scripts/github-actions/verify-frontend-release.sh" - "scripts/github-actions/verify-frontend-release.sh"
- "scripts/prebuild.sh" - "scripts/prebuild.sh"
- "contentlayer.config.ts" - "contentlayer.config.ts"
@ -42,7 +45,8 @@ concurrency:
cancel-in-progress: false cancel-in-progress: false
env: env:
PRIMARY_DOMAIN: console.svc.plus CANONICAL_DOMAIN: www.svc.plus
SERVED_DOMAINS: www.svc.plus,console.svc.plus
NEXT_PUBLIC_RUNTIME_ENVIRONMENT: prod NEXT_PUBLIC_RUNTIME_ENVIRONMENT: prod
NEXT_PUBLIC_RUNTIME_REGION: cn NEXT_PUBLIC_RUNTIME_REGION: cn
ACCOUNT_SERVICE_URL: https://accounts.svc.plus ACCOUNT_SERVICE_URL: https://accounts.svc.plus
@ -176,4 +180,5 @@ jobs:
- name: Verify Frontend Release - name: Verify Frontend Release
run: | run: |
bash scripts/github-actions/verify-frontend-release.sh \ bash scripts/github-actions/verify-frontend-release.sh \
"${PRIMARY_DOMAIN}" "${CANONICAL_DOMAIN}" \
"${SERVED_DOMAINS}"

View File

@ -1,21 +1,22 @@
# Compose settings # Compose settings
FRONTEND_IMAGE=ghcr.io/cloud-neutral-toolkit/dashboard:replace-me FRONTEND_IMAGE=ghcr.io/cloud-neutral-toolkit/dashboard:replace-me
PRIMARY_DOMAIN=cn-console.svc.plus CANONICAL_DOMAIN=www.svc.plus
SERVED_DOMAINS=www.svc.plus,console.svc.plus
# Frontend runtime # Frontend runtime
NODE_ENV=production NODE_ENV=production
PORT=3000 PORT=3000
RUNTIME_ENV=prod RUNTIME_ENV=prod
REGION=cn REGION=cn
APP_BASE_URL=https://cn-console.svc.plus APP_BASE_URL=https://www.svc.plus
NEXT_PUBLIC_APP_BASE_URL=https://cn-console.svc.plus NEXT_PUBLIC_APP_BASE_URL=https://www.svc.plus
NEXT_PUBLIC_SITE_URL=https://cn-console.svc.plus NEXT_PUBLIC_SITE_URL=https://www.svc.plus
NEXT_PUBLIC_LOGIN_URL=https://cn-console.svc.plus/login NEXT_PUBLIC_LOGIN_URL=https://www.svc.plus/login
NEXT_PUBLIC_DOCS_BASE_URL=https://cn-console.svc.plus/docs NEXT_PUBLIC_DOCS_BASE_URL=https://www.svc.plus/docs
SESSION_COOKIE_SECURE=true SESSION_COOKIE_SECURE=true
NEXT_PUBLIC_SESSION_COOKIE_SECURE=true NEXT_PUBLIC_SESSION_COOKIE_SECURE=true
RUNTIME_HOSTNAME=cn-console.svc.plus RUNTIME_HOSTNAME=www.svc.plus
DEPLOYMENT_HOSTNAME=cn-console.svc.plus DEPLOYMENT_HOSTNAME=www.svc.plus
NEXT_PUBLIC_RUNTIME_ENVIRONMENT=prod NEXT_PUBLIC_RUNTIME_ENVIRONMENT=prod
NEXT_PUBLIC_RUNTIME_REGION=cn NEXT_PUBLIC_RUNTIME_REGION=cn

View File

@ -1,4 +1,4 @@
{$PRIMARY_DOMAIN} { {$SERVED_DOMAINS} {
encode zstd gzip encode zstd gzip
handle_path /_next/static/* { handle_path /_next/static/* {

View File

@ -36,7 +36,7 @@ services:
- "80:80" - "80:80"
- "443:443" - "443:443"
environment: environment:
PRIMARY_DOMAIN: ${PRIMARY_DOMAIN:?set PRIMARY_DOMAIN in .env.runtime} SERVED_DOMAINS: ${SERVED_DOMAINS:?set SERVED_DOMAINS in .env.runtime}
volumes: volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro - ./Caddyfile:/etc/caddy/Caddyfile:ro
- frontend_static:/srv:ro - frontend_static:/srv:ro

View File

@ -214,7 +214,7 @@ export default function NotFound() {
import type { Metadata } from 'next' import type { Metadata } from 'next'
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL('https://console.svc.plus'), metadataBase: new URL('https://www.svc.plus'),
title: { title: {
default: 'Cloud-Neutral | Unified Cloud Native Tools', default: 'Cloud-Neutral | Unified Cloud Native Tools',
template: '%s | Cloud-Neutral', template: '%s | Cloud-Neutral',
@ -232,7 +232,7 @@ export const metadata: Metadata = {
openGraph: { openGraph: {
type: 'website', type: 'website',
locale: 'en_US', locale: 'en_US',
url: 'https://console.svc.plus', url: 'https://www.svc.plus',
title: 'Cloud-Neutral | Unified Cloud Native Tools', title: 'Cloud-Neutral | Unified Cloud Native Tools',
description: 'Unified tools for your cloud native stack', description: 'Unified tools for your cloud native stack',
siteName: 'Cloud-Neutral', siteName: 'Cloud-Neutral',
@ -274,7 +274,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#6366f1" /> <meta name="theme-color" content="#6366f1" />
<link rel="canonical" href="https://console.svc.plus" /> <link rel="canonical" href="https://www.svc.plus" />
{/* ... rest of head */} {/* ... rest of head */}
</head> </head>
{/* ... rest of layout */} {/* ... rest of layout */}
@ -343,7 +343,7 @@ Disallow: /admin/
Disallow: /api/ Disallow: /api/
Disallow: /internal/ Disallow: /internal/
Sitemap: https://console.svc.plus/sitemap.xml Sitemap: https://www.svc.plus/sitemap.xml
``` ```
--- ---
@ -360,8 +360,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'Organization', '@type': 'Organization',
name: 'Cloud-Neutral', name: 'Cloud-Neutral',
url: 'https://console.svc.plus', url: 'https://www.svc.plus',
logo: 'https://console.svc.plus/logo.png', logo: 'https://www.svc.plus/logo.png',
sameAs: [ sameAs: [
'https://twitter.com/cloudneutral', 'https://twitter.com/cloudneutral',
'https://github.com/x-evor', 'https://github.com/x-evor',
@ -372,10 +372,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'WebSite', '@type': 'WebSite',
name: 'Cloud-Neutral', name: 'Cloud-Neutral',
url: 'https://console.svc.plus', url: 'https://www.svc.plus',
potentialAction: { potentialAction: {
'@type': 'SearchAction', '@type': 'SearchAction',
target: 'https://console.svc.plus/search?q={search_term_string}', target: 'https://www.svc.plus/search?q={search_term_string}',
'query-input': 'required name=search_term_string', 'query-input': 'required name=search_term_string',
}, },
} }

View File

@ -4,9 +4,11 @@
- Runtime: `Caddy + Docker Compose` - Runtime: `Caddy + Docker Compose`
- Deploy host: `root@cn-console.svc.plus` - Deploy host: `root@cn-console.svc.plus`
- Domains: - Public domains:
- `cn-console.svc.plus` - `www.svc.plus`
- Frontend release workflow: `.github/workflows/service_release_frontend-deploy.yml` - `console.svc.plus`
- Canonical public origin: `https://www.svc.plus`
- Frontend release workflow: `.github/workflows/pipeline.yaml`
## Operating Model ## Operating Model
@ -20,7 +22,14 @@ The stack is static-first:
- The Next.js standalone container serves dynamic HTML, auth endpoints, and API proxy routes. Static assets and hashed CSS/JS files are extracted by the `frontend-assets` helper task, so the runtime no longer needs to compile anything on the single-node host. - The Next.js standalone container serves dynamic HTML, auth endpoints, and API proxy routes. Static assets and hashed CSS/JS files are extracted by the `frontend-assets` helper task, so the runtime no longer needs to compile anything on the single-node host.
- `docs.svc.plus` is the source of truth for rendered docs/blog pages; the browser does not call it directly. - `docs.svc.plus` is the source of truth for rendered docs/blog pages; the browser does not call it directly.
Releases are orchestrated through `.github/workflows/service_release_frontend-deploy.yml`. That workflow builds/pushes the image, renders `.env.runtime` including `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL`, and ships `docker-compose.yml`, `Caddyfile`, and the runtime env file to the host. The control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` then updates Cloudflare DNS for the release domain (via `scripts/github-actions/update-release-dns.sh`) so `cn-console.svc.plus` points at the new environment. Releases are orchestrated through `.github/workflows/pipeline.yaml`. That workflow builds/pushes the image, renders `.env.runtime` including `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL`, and ships `docker-compose.yml`, `Caddyfile`, and the runtime env file to the host. The control-plane DNS automation then updates Cloudflare DNS for the release domains (via `scripts/github-actions/update-release-dns.sh`) so both `www.svc.plus` and `console.svc.plus` resolve to the same environment.
The release contract now uses:
- `CANONICAL_DOMAIN=www.svc.plus`
- `SERVED_DOMAINS=www.svc.plus,console.svc.plus`
Validation must pass for both domains. A release is incomplete if either host serves a different runtime version, static asset family, or `dashboardUrl`.
This baseline is intentional for the weak-IO single-node host `root@cn-console.svc.plus`. No images are built on the target machine, keeping the deployment lightweight: the host only logs into GHCR, pulls the `dashboard` image, extracts assets into `frontend_static`, and starts `dashboard` plus `caddy` containers via `docker compose`. This baseline is intentional for the weak-IO single-node host `root@cn-console.svc.plus`. No images are built on the target machine, keeping the deployment lightweight: the host only logs into GHCR, pulls the `dashboard` image, extracts assets into `frontend_static`, and starts `dashboard` plus `caddy` containers via `docker compose`.

View File

@ -1,6 +1,6 @@
# Release Process # Release Process
This page tracks release summaries for published versions of `console.svc.plus`. This page tracks release summaries for published versions of the public web console served under `www.svc.plus` and `console.svc.plus`.
## Current Release ## Current Release
@ -49,3 +49,5 @@ Published commit: `0fab89e`
- GitHub Release: `https://github.com/x-evor/console.svc.plus/releases/tag/v0.2` - GitHub Release: `https://github.com/x-evor/console.svc.plus/releases/tag/v0.2`
- Related docs: `docs/README.md`, `docs/en/README.md`, `docs/zh/README.md` - Related docs: `docs/README.md`, `docs/en/README.md`, `docs/zh/README.md`
- Release validation must verify both `www.svc.plus` and `console.svc.plus` against the same `releaseImageRef`, `releaseImageTag`, and `releaseCommit`.
- `www.svc.plus` is the canonical public domain for metadata, sitemap, `dashboardUrl`, and shared links.

View File

@ -48,7 +48,7 @@ CLOUDFLARE_WEB_ANALYTICS_SITE_TAG=
### 本地开发 ### 本地开发
写入 `console.svc.plus/.env.local` 写入当前前端仓库的 `.env.local`
```bash ```bash
CLOUDFLARE_API_TOKEN=... CLOUDFLARE_API_TOKEN=...
@ -58,7 +58,7 @@ CLOUDFLARE_WEB_ANALYTICS_SITE_TAG=...
### 线上部署 ### 线上部署
把同名变量写入 `console.svc.plus` 的部署环境(例如 Vercel/Cloud Run 的环境变量配置) 把同名变量写入前端部署环境
> 注意:这些变量属于服务端密钥,不要暴露到 `NEXT_PUBLIC_*` > 注意:这些变量属于服务端密钥,不要暴露到 `NEXT_PUBLIC_*`
@ -67,7 +67,7 @@ CLOUDFLARE_WEB_ANALYTICS_SITE_TAG=...
部署后访问: 部署后访问:
```bash ```bash
curl -fsSL https://console.svc.plus/api/marketing/home-stats curl -fsSL https://www.svc.plus/api/marketing/home-stats
``` ```
期望返回中 `visits.daily/weekly/monthly` 为数字(非 `null`)。 期望返回中 `visits.daily/weekly/monthly` 为数字(非 `null`)。
@ -77,4 +77,3 @@ curl -fsSL https://console.svc.plus/api/marketing/home-stats
1. token 权限是否包含 Analytics Read 1. token 权限是否包含 Analytics Read
2. Account ID 是否与 siteTag 属于同一账号 2. Account ID 是否与 siteTag 属于同一账号
3. 环境变量是否已在当前运行实例生效(重启/重新部署后再测) 3. 环境变量是否已在当前运行实例生效(重启/重新部署后再测)

View File

@ -6,7 +6,7 @@ This guide describes how to configure GitHub and Google OAuth login for the Clou
``` ```
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Browser │ │ console.svc.plus │ │accounts.svc.plus │ │ Browser │ │ www.svc.plus │ │accounts.svc.plus │
│ (User) │ │ (Frontend) │ │ (Backend) │ │ (User) │ │ (Frontend) │ │ (Backend) │
└──────┬───────┘ └────────┬─────────┘ └────────┬─────────┘ └──────┬───────┘ └────────┬─────────┘ └────────┬─────────┘
│ 1. Click "Login │ │ │ 1. Click "Login │ │
@ -40,7 +40,7 @@ This guide describes how to configure GitHub and Google OAuth login for the Clou
- A GitHub account with access to **Settings > Developer Settings** - A GitHub account with access to **Settings > Developer Settings**
- A Google account with access to [Google Cloud Console](https://console.cloud.google.com/) - A Google account with access to [Google Cloud Console](https://console.cloud.google.com/)
- Running `accounts.svc.plus` and `console.svc.plus` services - Running `accounts.svc.plus` and the frontend served under `www.svc.plus` / `console.svc.plus`
--- ---
@ -55,7 +55,7 @@ This guide describes how to configure GitHub and Google OAuth login for the Clou
| Field | Value | | Field | Value |
|---|---| |---|---|
| **Application name** | `Cloud Neutral Console` | | **Application name** | `Cloud Neutral Console` |
| **Homepage URL** | `https://console.svc.plus` | | **Homepage URL** | `https://www.svc.plus` |
| **Authorization callback URL** | `https://accounts.svc.plus/api/auth/oauth/callback/github` | | **Authorization callback URL** | `https://accounts.svc.plus/api/auth/oauth/callback/github` |
| **Enable Device Flow** | ☐ (unchecked) | | **Enable Device Flow** | ☐ (unchecked) |
@ -119,7 +119,7 @@ No additional GitHub permissions are required.
|---|---| |---|---|
| **Application type** | `Web application` | | **Application type** | `Web application` |
| **Name** | `Cloud Neutral Console` | | **Name** | `Cloud Neutral Console` |
| **Authorized JavaScript origins** | `https://console.svc.plus` | | **Authorized JavaScript origins** | `https://www.svc.plus` |
| **Authorized redirect URIs** | `https://accounts.svc.plus/api/auth/oauth/callback/google` | | **Authorized redirect URIs** | `https://accounts.svc.plus/api/auth/oauth/callback/google` |
4. Click **"Create"** 4. Click **"Create"**
@ -150,7 +150,7 @@ GOOGLE_CLIENT_SECRET=<your_google_client_secret>
# ── General OAuth ── # ── General OAuth ──
OAUTH_REDIRECT_URL=https://accounts.svc.plus/api/auth/oauth/callback OAUTH_REDIRECT_URL=https://accounts.svc.plus/api/auth/oauth/callback
OAUTH_FRONTEND_URL=https://console.svc.plus OAUTH_FRONTEND_URL=https://www.svc.plus
``` ```
These variables are referenced in `config/account.yaml`: These variables are referenced in `config/account.yaml`:
@ -159,7 +159,7 @@ These variables are referenced in `config/account.yaml`:
auth: auth:
oauth: oauth:
redirectUrl: "${OAUTH_REDIRECT_URL}" redirectUrl: "${OAUTH_REDIRECT_URL}"
frontendUrl: "${OAUTH_FRONTEND_URL:-https://console.svc.plus}" frontendUrl: "${OAUTH_FRONTEND_URL:-https://www.svc.plus}"
github: github:
clientId: "${GITHUB_CLIENT_ID}" clientId: "${GITHUB_CLIENT_ID}"
clientSecret: "${GITHUB_CLIENT_SECRET}" clientSecret: "${GITHUB_CLIENT_SECRET}"
@ -172,7 +172,7 @@ auth:
--- ---
## 4. Frontend Configuration (console.svc.plus) ## 4. Frontend Configuration (`www.svc.plus` canonical, `console.svc.plus` secondary)
The frontend resolves the accounts service URL **server-side** via `getAccountServiceBaseUrl()`, which reads: The frontend resolves the accounts service URL **server-side** via `getAccountServiceBaseUrl()`, which reads:
@ -209,7 +209,7 @@ If not set, the function falls back to a runtime default. **No `NEXT_PUBLIC_*` e
### OAuth login redirects to wrong domain ### OAuth login redirects to wrong domain
Check that `OAUTH_FRONTEND_URL` in accounts.svc.plus matches the console domain where users should be redirected after authentication. Check that `OAUTH_FRONTEND_URL` in accounts.svc.plus matches the canonical public domain where users should be redirected after authentication. The current default is `https://www.svc.plus`.
### Google "Access blocked: This app's request is invalid" ### Google "Access blocked: This app's request is invalid"

View File

@ -6,8 +6,9 @@
- Topology: `Caddy + Docker Compose + GitHub Actions` - Topology: `Caddy + Docker Compose + GitHub Actions`
- Deploy host: `root@cn-console.svc.plus` - Deploy host: `root@cn-console.svc.plus`
- Public domains: - Public domains:
- `https://cn-console.svc.plus` - `https://www.svc.plus`
- Primary origin: `https://cn-console.svc.plus` - `https://console.svc.plus`
- Canonical public origin: `https://www.svc.plus`
## Current Delivery Model ## Current Delivery Model
@ -23,7 +24,7 @@ This is intentionally static-first for the current weak-IO single-node host. Dyn
## Control Plane & DNS Stage ## Control Plane & DNS Stage
The control repo (`github-org-x-evor`) tracks `console.svc.plus` through `console.svc.plus.code-workspace` and keeps the `subrepos/accounts.svc.plus` pointer in sync via `skills/cross-repo-upstream-submodule-sync`. Releases resolve metadata with that workspace and the `config/single-node-release` manifests. After `.github/workflows/service_release_frontend-deploy.yml` finishes pushing the new image, the control-plane workflow `.github/workflows/service_release_apiserver-deploy.yml` calls `scripts/github-actions/update-release-dns.sh` to update Cloudflare DNS so the new endpoint is reachable under `cn-console.svc.plus`. The control repo (`github-org-x-evor`) tracks `console.svc.plus` through `console.svc.plus.code-workspace` and keeps the `subrepos/accounts.svc.plus` pointer in sync via `skills/cross-repo-upstream-submodule-sync`. Releases resolve metadata with that workspace and the `config/single-node-release` manifests. After `.github/workflows/pipeline.yaml` finishes pushing the new image, the control-plane DNS automation calls `scripts/github-actions/update-release-dns.sh` to update Cloudflare DNS so the new endpoint is reachable under `cn-console.svc.plus`.
## Runtime Layout ## Runtime Layout
@ -52,7 +53,7 @@ Containers:
Workflow: Workflow:
```text ```text
.github/workflows/service_release_frontend-deploy.yml .github/workflows/pipeline.yaml
``` ```
Secrets required: Secrets required:
@ -67,6 +68,8 @@ Secrets required:
Repository/environment variables recommended: Repository/environment variables recommended:
- `CANONICAL_DOMAIN`
- `SERVED_DOMAINS`
- `APP_BASE_URL` - `APP_BASE_URL`
- `NEXT_PUBLIC_APP_BASE_URL` - `NEXT_PUBLIC_APP_BASE_URL`
- `NEXT_PUBLIC_SITE_URL` - `NEXT_PUBLIC_SITE_URL`
@ -94,10 +97,10 @@ Repository/environment variables recommended:
2. Docker builds the frontend image with the public `NEXT_PUBLIC_*` values needed at build time. 2. Docker builds the frontend image with the public `NEXT_PUBLIC_*` values needed at build time.
3. The image is pushed to GHCR. 3. The image is pushed to GHCR.
4. The workflow updates Cloudflare DNS for the release domain. 4. The workflow updates Cloudflare DNS for the release domain.
5. The workflow renders `.env.runtime`, including docs service runtime endpoints and the `cn-console` origin settings. 5. The workflow renders `.env.runtime`, including the canonical public origin and both served frontend domains.
6. The workflow uploads `docker-compose.yml`, `Caddyfile`, and `.env.runtime` to the host. 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`. 7. The host pulls the new image, refreshes the static asset volume, and starts `dashboard + caddy`.
8. The workflow verifies `cn-console.svc.plus`. 8. The workflow verifies both `www.svc.plus` and `console.svc.plus`, and fails the release if either domain serves a different runtime version.
## Verification Commands ## Verification Commands
@ -113,7 +116,7 @@ rm -f deploy/single-node/.env.runtime
python3 - <<'PY' python3 - <<'PY'
from pathlib import Path from pathlib import Path
import yaml import yaml
yaml.safe_load(Path('.github/workflows/service_release_frontend-deploy.yml').read_text()) yaml.safe_load(Path('.github/workflows/pipeline.yaml').read_text())
print('workflow yaml ok') print('workflow yaml ok')
PY PY
``` ```
@ -122,8 +125,10 @@ Remote checks:
```bash ```bash
ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime ps" ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime ps"
ssh root@cn-console.svc.plus "curl -fsSI -H 'Host: cn-console.svc.plus' http://127.0.0.1/" ssh root@cn-console.svc.plus "curl -fsSI -H 'Host: www.svc.plus' http://127.0.0.1/"
curl -fsSIL https://cn-console.svc.plus ssh root@cn-console.svc.plus "curl -fsSI -H 'Host: console.svc.plus' http://127.0.0.1/"
curl -fsSIL https://www.svc.plus
curl -fsSIL https://console.svc.plus
``` ```
## Failure Signatures ## Failure Signatures
@ -132,7 +137,7 @@ curl -fsSIL https://cn-console.svc.plus
The workflow token or package visibility is wrong. The workflow token or package visibility is wrong.
- `frontend-assets` fails - `frontend-assets` fails
The image layout changed and no longer contains `/app/dashboard/static` or `/app/dashboard/public`. The image layout changed and no longer contains `/app/dashboard/static` or `/app/dashboard/public`.
- `cn-console.svc.plus` returns `502` - `www.svc.plus` or `console.svc.plus` returns `502`
Caddy is up, but the `dashboard` container failed or is not reachable on port `3000`. Caddy is up, but the `dashboard` container failed or is not reachable on port `3000`.
## Rollback ## Rollback
@ -146,4 +151,4 @@ ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-f
ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime up -d dashboard caddy" ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime up -d dashboard caddy"
``` ```
4. Verify `https://cn-console.svc.plus` again before closing the incident. 4. Verify `https://www.svc.plus` and `https://console.svc.plus` again before closing the incident.

View File

@ -5,8 +5,10 @@
- 运行拓扑: `Caddy + Docker Compose` - 运行拓扑: `Caddy + Docker Compose`
- 目标主机: `root@cn-console.svc.plus` - 目标主机: `root@cn-console.svc.plus`
- 域名: - 域名:
- `cn-console.svc.plus` - `www.svc.plus`
- 前端独立发布流水线: `.github/workflows/service_release_frontend-deploy.yml` - `console.svc.plus`
- 公开首选域名: `www.svc.plus`
- 前端独立发布流水线: `.github/workflows/pipeline.yaml`
## 运行方式 ## 运行方式
@ -20,12 +22,19 @@
- Next.js standalone 容器只承接动态页面、认证接口和代理接口,`frontend-assets` 任务会把所有静态文件(包括哈希后的 CSS/JS拷贝到 `frontend_static` - Next.js standalone 容器只承接动态页面、认证接口和代理接口,`frontend-assets` 任务会把所有静态文件(包括哈希后的 CSS/JS拷贝到 `frontend_static`
- `docs.svc.plus` 是 docs/blog 的运行时内容源,浏览器不会直接访问它。 - `docs.svc.plus` 是 docs/blog 的运行时内容源,浏览器不会直接访问它。
发布由 `.github/workflows/service_release_frontend-deploy.yml` 驱动CI 构建/推送镜像、渲染包含 `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL``.env.runtime`,然后将 `docker-compose.yml`、`Caddyfile` 与运行时环境文件发送到主机。随后控制平面工作流 `.github/workflows/service_release_apiserver-deploy.yml` 通过 `scripts/github-actions/update-release-dns.sh` 更新 Cloudflare DNS使 `cn-console.svc.plus` 指向更新后的环境。 发布由 `.github/workflows/pipeline.yaml` 驱动CI 构建/推送镜像、渲染包含 `DOCS_SERVICE_URL` / `DOCS_SERVICE_INTERNAL_URL``.env.runtime`,然后将 `docker-compose.yml`、`Caddyfile` 与运行时环境文件发送到主机。随后控制平面 DNS 自动化会通过 `scripts/github-actions/update-release-dns.sh` 更新 Cloudflare DNS使 `cn-console.svc.plus` 指向更新后的环境。
这是针对弱 IO 单机主机 `root@cn-console.svc.plus` 的部署权衡:主机不会在本地构建镜像,只需登录 GHCR、拉取 `dashboard` 镜像、解包静态资源到 `frontend_static`,再通过 `docker compose` 启动 `dashboard``caddy` 这是针对弱 IO 单机主机 `root@cn-console.svc.plus` 的部署权衡:主机不会在本地构建镜像,只需登录 GHCR、拉取 `dashboard` 镜像、解包静态资源到 `frontend_static`,再通过 `docker compose` 启动 `dashboard``caddy`
`docs.svc.plus` 已经是前端 docs/blog 内容的独立服务。 `docs.svc.plus` 已经是前端 docs/blog 内容的独立服务。
当前发布合同要求:
- `CANONICAL_DOMAIN=www.svc.plus`
- `SERVED_DOMAINS=www.svc.plus,console.svc.plus`
- 流水线必须同时校验两个域名的首页静态资源与 `/api/ping` 版本元数据完全一致
- `dashboardUrl`、canonical、structured data 与 sitemap 默认统一输出 `https://www.svc.plus`
## 相关文档 ## 相关文档
- `usage/deployment.md` - `usage/deployment.md`

View File

@ -2,7 +2,7 @@
> English: `../../governance/release-process.md` > English: `../../governance/release-process.md`
本页用于记录 `console.svc.plus` 已发布版本的发布说明与变更摘要。 本页用于记录公开控制台在 `www.svc.plus``console.svc.plus` 下发布版本的说明与变更摘要。
## 当前版本 ## 当前版本
@ -50,4 +50,6 @@
## 备注 ## 备注
- GitHub Release`https://github.com/x-evor/console.svc.plus/releases/tag/v0.2` - GitHub Release`https://github.com/x-evor/console.svc.plus/releases/tag/v0.2`
- 发布校验必须同时验证 `www.svc.plus``console.svc.plus``releaseImageRef`、`releaseImageTag`、`releaseCommit` 完全一致。
- `www.svc.plus` 是 metadata、sitemap、`dashboardUrl` 与公开分享链接的首选域名。
- 相关文档:`docs/README.md`、`docs/en/README.md`、`docs/zh/README.md` - 相关文档:`docs/README.md`、`docs/en/README.md`、`docs/zh/README.md`

View File

@ -14,7 +14,7 @@ require_env() {
} }
require_env IMAGE_REF require_env IMAGE_REF
require_env PRIMARY_DOMAIN require_env CANONICAL_DOMAIN
BUILD_ARGS_FILE="$(mktemp)" BUILD_ARGS_FILE="$(mktemp)"
trap 'rm -f "${BUILD_ARGS_FILE}"' EXIT trap 'rm -f "${BUILD_ARGS_FILE}"' EXIT

View File

@ -21,7 +21,8 @@ require_env SINGLE_NODE_VPS_SSH_PRIVATE_KEY
require_env GHCR_USERNAME require_env GHCR_USERNAME
require_env GHCR_PASSWORD require_env GHCR_PASSWORD
require_env FRONTEND_IMAGE require_env FRONTEND_IMAGE
require_env PRIMARY_DOMAIN require_env CANONICAL_DOMAIN
require_env SERVED_DOMAINS
GHCR_REGISTRY="${GHCR_REGISTRY:-ghcr.io}" GHCR_REGISTRY="${GHCR_REGISTRY:-ghcr.io}"

View File

@ -13,16 +13,16 @@ require_env() {
} }
emit_lines() { emit_lines() {
require_env PRIMARY_DOMAIN require_env CANONICAL_DOMAIN
local primary_domain="${PRIMARY_DOMAIN}" local canonical_domain="${CANONICAL_DOMAIN}"
printf 'NODE_BUILDER_IMAGE=%s\n' "${NODE_BUILDER_IMAGE:-node:22-bookworm}" printf 'NODE_BUILDER_IMAGE=%s\n' "${NODE_BUILDER_IMAGE:-node:22-bookworm}"
printf 'NODE_RUNTIME_IMAGE=%s\n' "${NODE_RUNTIME_IMAGE:-node:22-slim}" printf 'NODE_RUNTIME_IMAGE=%s\n' "${NODE_RUNTIME_IMAGE:-node:22-slim}"
printf 'CONTENTLAYER_BUILD=%s\n' "${CONTENTLAYER_BUILD:-true}" printf 'CONTENTLAYER_BUILD=%s\n' "${CONTENTLAYER_BUILD:-true}"
printf 'NEXT_PUBLIC_APP_BASE_URL=%s\n' "${NEXT_PUBLIC_APP_BASE_URL:-https://${primary_domain}}" printf 'NEXT_PUBLIC_APP_BASE_URL=%s\n' "${NEXT_PUBLIC_APP_BASE_URL:-https://${canonical_domain}}"
printf 'NEXT_PUBLIC_SITE_URL=%s\n' "${NEXT_PUBLIC_SITE_URL:-https://${primary_domain}}" printf 'NEXT_PUBLIC_SITE_URL=%s\n' "${NEXT_PUBLIC_SITE_URL:-https://${canonical_domain}}"
printf 'NEXT_PUBLIC_LOGIN_URL=%s\n' "${NEXT_PUBLIC_LOGIN_URL:-https://${primary_domain}/login}" printf 'NEXT_PUBLIC_LOGIN_URL=%s\n' "${NEXT_PUBLIC_LOGIN_URL:-https://${canonical_domain}/login}"
printf 'NEXT_PUBLIC_DOCS_BASE_URL=%s\n' "${NEXT_PUBLIC_DOCS_BASE_URL:-https://${primary_domain}/docs}" printf 'NEXT_PUBLIC_DOCS_BASE_URL=%s\n' "${NEXT_PUBLIC_DOCS_BASE_URL:-https://${canonical_domain}/docs}"
printf 'NEXT_PUBLIC_RUNTIME_ENVIRONMENT=%s\n' "${NEXT_PUBLIC_RUNTIME_ENVIRONMENT:-prod}" printf 'NEXT_PUBLIC_RUNTIME_ENVIRONMENT=%s\n' "${NEXT_PUBLIC_RUNTIME_ENVIRONMENT:-prod}"
printf 'NEXT_PUBLIC_RUNTIME_REGION=%s\n' "${NEXT_PUBLIC_RUNTIME_REGION:-cn}" printf 'NEXT_PUBLIC_RUNTIME_REGION=%s\n' "${NEXT_PUBLIC_RUNTIME_REGION:-cn}"
printf 'NEXT_PUBLIC_GISCUS_REPO=%s\n' "${NEXT_PUBLIC_GISCUS_REPO:-x-evor/console.svc.plus}" printf 'NEXT_PUBLIC_GISCUS_REPO=%s\n' "${NEXT_PUBLIC_GISCUS_REPO:-x-evor/console.svc.plus}"

View File

@ -22,25 +22,27 @@ require_env() {
} }
require_env FRONTEND_IMAGE require_env FRONTEND_IMAGE
require_env PRIMARY_DOMAIN require_env CANONICAL_DOMAIN
require_env SERVED_DOMAINS
append_env FRONTEND_IMAGE "${FRONTEND_IMAGE}" append_env FRONTEND_IMAGE "${FRONTEND_IMAGE}"
append_env PRIMARY_DOMAIN "${PRIMARY_DOMAIN}" append_env CANONICAL_DOMAIN "${CANONICAL_DOMAIN}"
append_env SERVED_DOMAINS "${SERVED_DOMAINS}"
append_env NODE_ENV "production" append_env NODE_ENV "production"
append_env PORT "${PORT:-3000}" append_env PORT "${PORT:-3000}"
append_env RUNTIME_ENV "${RUNTIME_ENV:-prod}" append_env RUNTIME_ENV "${RUNTIME_ENV:-prod}"
append_env REGION "${REGION:-cn}" append_env REGION "${REGION:-cn}"
append_env APP_BASE_URL "${APP_BASE_URL:-https://${PRIMARY_DOMAIN}}" append_env APP_BASE_URL "${APP_BASE_URL:-https://${CANONICAL_DOMAIN}}"
append_env NEXT_PUBLIC_APP_BASE_URL "${NEXT_PUBLIC_APP_BASE_URL:-https://${PRIMARY_DOMAIN}}" append_env NEXT_PUBLIC_APP_BASE_URL "${NEXT_PUBLIC_APP_BASE_URL:-https://${CANONICAL_DOMAIN}}"
append_env NEXT_PUBLIC_SITE_URL "${NEXT_PUBLIC_SITE_URL:-https://${PRIMARY_DOMAIN}}" append_env NEXT_PUBLIC_SITE_URL "${NEXT_PUBLIC_SITE_URL:-https://${CANONICAL_DOMAIN}}"
append_env NEXT_PUBLIC_LOGIN_URL "${NEXT_PUBLIC_LOGIN_URL:-https://${PRIMARY_DOMAIN}/login}" append_env NEXT_PUBLIC_LOGIN_URL "${NEXT_PUBLIC_LOGIN_URL:-https://${CANONICAL_DOMAIN}/login}"
append_env NEXT_PUBLIC_DOCS_BASE_URL "${NEXT_PUBLIC_DOCS_BASE_URL:-https://${PRIMARY_DOMAIN}/docs}" append_env NEXT_PUBLIC_DOCS_BASE_URL "${NEXT_PUBLIC_DOCS_BASE_URL:-https://${CANONICAL_DOMAIN}/docs}"
append_env SESSION_COOKIE_SECURE "${SESSION_COOKIE_SECURE:-true}" append_env SESSION_COOKIE_SECURE "${SESSION_COOKIE_SECURE:-true}"
append_env NEXT_PUBLIC_SESSION_COOKIE_SECURE "${NEXT_PUBLIC_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 RUNTIME_HOSTNAME "${RUNTIME_HOSTNAME:-${CANONICAL_DOMAIN}}"
append_env NEXT_RUNTIME_HOSTNAME "${NEXT_RUNTIME_HOSTNAME:-${PRIMARY_DOMAIN}}" append_env NEXT_RUNTIME_HOSTNAME "${NEXT_RUNTIME_HOSTNAME:-${CANONICAL_DOMAIN}}"
append_env DEPLOYMENT_HOSTNAME "${DEPLOYMENT_HOSTNAME:-${PRIMARY_DOMAIN}}" append_env DEPLOYMENT_HOSTNAME "${DEPLOYMENT_HOSTNAME:-${CANONICAL_DOMAIN}}"
append_env NEXT_PUBLIC_RUNTIME_ENVIRONMENT "${NEXT_PUBLIC_RUNTIME_ENVIRONMENT:-prod}" append_env NEXT_PUBLIC_RUNTIME_ENVIRONMENT "${NEXT_PUBLIC_RUNTIME_ENVIRONMENT:-prod}"
append_env NEXT_PUBLIC_RUNTIME_REGION "${NEXT_PUBLIC_RUNTIME_REGION:-cn}" 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 ACCOUNT_SERVICE_URL "${ACCOUNT_SERVICE_URL:-https://accounts.svc.plus}"

View File

@ -15,7 +15,8 @@ ansible_args=(
-e "GHCR_PASSWORD=${GHCR_PASSWORD:?GHCR_PASSWORD is required}" -e "GHCR_PASSWORD=${GHCR_PASSWORD:?GHCR_PASSWORD is required}"
-e "INTERNAL_SERVICE_TOKEN=${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}" -e "INTERNAL_SERVICE_TOKEN=${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN is required}"
-e "ACCOUNT_SERVICE_URL=${ACCOUNT_SERVICE_URL:?ACCOUNT_SERVICE_URL is required}" -e "ACCOUNT_SERVICE_URL=${ACCOUNT_SERVICE_URL:?ACCOUNT_SERVICE_URL is required}"
-e "PRIMARY_DOMAIN=${PRIMARY_DOMAIN:?PRIMARY_DOMAIN is required}" -e "CANONICAL_DOMAIN=${CANONICAL_DOMAIN:?CANONICAL_DOMAIN is required}"
-e "SERVED_DOMAINS=${SERVED_DOMAINS:?SERVED_DOMAINS is required}"
-e "NEXT_PUBLIC_RUNTIME_ENVIRONMENT=${NEXT_PUBLIC_RUNTIME_ENVIRONMENT:?NEXT_PUBLIC_RUNTIME_ENVIRONMENT is required}" -e "NEXT_PUBLIC_RUNTIME_ENVIRONMENT=${NEXT_PUBLIC_RUNTIME_ENVIRONMENT:?NEXT_PUBLIC_RUNTIME_ENVIRONMENT is required}"
-e "NEXT_PUBLIC_RUNTIME_REGION=${NEXT_PUBLIC_RUNTIME_REGION:?NEXT_PUBLIC_RUNTIME_REGION is required}" -e "NEXT_PUBLIC_RUNTIME_REGION=${NEXT_PUBLIC_RUNTIME_REGION:?NEXT_PUBLIC_RUNTIME_REGION is required}"
-e "CLOUDFLARE_ZONE_TAG=${CLOUDFLARE_ZONE_TAG:?CLOUDFLARE_ZONE_TAG is required}" -e "CLOUDFLARE_ZONE_TAG=${CLOUDFLARE_ZONE_TAG:?CLOUDFLARE_ZONE_TAG is required}"

View File

@ -1,25 +1,26 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
PRIMARY_DOMAIN="${1:?usage: verify-frontend-release.sh <primary-domain>}" CANONICAL_DOMAIN="${1:?usage: verify-frontend-release.sh <canonical-domain> <served-domains>}"
SERVED_DOMAINS="${2:?usage: verify-frontend-release.sh <canonical-domain> <served-domains>}"
EXPECTED_DASHBOARD_URL="https://${CANONICAL_DOMAIN}"
primary_url="https://${PRIMARY_DOMAIN}" curl_headers=(
-H 'user-agent: Mozilla/5.0 (compatible; console-release-validator/1.0; +https://www.svc.plus)'
-H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
-H 'accept-language: en-US,en;q=0.9'
)
curl -fsSIL "${primary_url}" >/dev/null trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "${value}"
}
homepage_html="$(curl -fsSL "${primary_url}")" parse_release_metadata() {
asset_path="$(printf '%s' "${homepage_html}" | grep -Eo '/_next/static/[^"'"'"' ]+\.(css|js)' | head -n 1)" local payload="$1"
if [[ -z "${asset_path}" ]]; then RELEASE_PAYLOAD="${payload}" python3 - <<'PY'
echo "Could not find a _next/static asset on the homepage" >&2
exit 1
fi
curl -fsSIL "${primary_url}${asset_path}" >/dev/null
printf 'verified static asset: %s%s\n' "${primary_url}" "${asset_path}"
release_payload="$(curl -fsSL "${primary_url}/api/ping")"
release_metadata="$(
RELEASE_PAYLOAD="${release_payload}" python3 - <<'PY'
import json import json
import os import os
@ -27,28 +28,141 @@ payload = json.loads(os.environ["RELEASE_PAYLOAD"])
print(payload.get("releaseImageRef", "")) print(payload.get("releaseImageRef", ""))
print(payload.get("releaseImageTag", "")) print(payload.get("releaseImageTag", ""))
print(payload.get("releaseCommit", "")) print(payload.get("releaseCommit", ""))
print(payload.get("dashboardUrl", ""))
PY PY
)" }
mapfile -t release_lines <<< "${release_metadata}" require_http_200() {
actual_image_ref="${release_lines[0]-}" local url="$1"
actual_image_tag="${release_lines[1]-}" shift
actual_release_commit="${release_lines[2]-}"
if [[ -z "${actual_image_ref}" || -z "${actual_image_tag}" || -z "${actual_release_commit}" ]]; then local http_code
echo "Remote release metadata is incomplete: ${release_payload}" >&2 http_code="$(curl -sS -o /dev/null -w '%{http_code}' "$@" "${url}")"
if [[ "${http_code}" != "200" ]]; then
echo "Expected HTTP 200 from ${url}, got ${http_code}" >&2
exit 1
fi
}
verify_domain() {
local domain="$1"
local url="https://${domain}"
local homepage_html asset_path release_payload release_metadata
local actual_image_ref actual_image_tag actual_release_commit actual_dashboard_url
local release_lines
require_http_200 "${url}" "${curl_headers[@]}"
printf 'verified homepage for %s: 200\n' "${domain}" >&2
homepage_html="$(curl -fsSL "${curl_headers[@]}" "${url}")"
asset_path="$(printf '%s' "${homepage_html}" | grep -Eo '/_next/static/[^"'"'"' ]+\.(css|js)' | head -n 1)"
if [[ -z "${asset_path}" ]]; then
echo "Could not find a _next/static asset on ${url}" >&2
exit 1
fi
require_http_200 "${url}${asset_path}" "${curl_headers[@]}"
printf 'verified static asset for %s: %s%s\n' "${domain}" "${url}" "${asset_path}" >&2
require_http_200 "${url}/api/ping" "${curl_headers[@]}"
release_payload="$(curl -fsSL "${curl_headers[@]}" "${url}/api/ping")"
release_metadata="$(parse_release_metadata "${release_payload}")"
mapfile -t release_lines <<< "${release_metadata}"
actual_image_ref="${release_lines[0]-}"
actual_image_tag="${release_lines[1]-}"
actual_release_commit="${release_lines[2]-}"
actual_dashboard_url="${release_lines[3]-}"
if [[ -z "${actual_image_ref}" || -z "${actual_image_tag}" || -z "${actual_release_commit}" ]]; then
echo "Remote release metadata is incomplete for ${domain}: ${release_payload}" >&2
exit 1
fi
if [[ ! "${actual_image_tag}" =~ ^[0-9a-f]{7,40}$ ]]; then
echo "Remote image tag must contain a commit id for ${domain}, got: ${actual_image_tag}" >&2
exit 1
fi
if [[ "${actual_release_commit}" != "${actual_image_tag}" ]]; then
echo "Remote release commit mismatch for ${domain}: expected ${actual_image_tag}, got ${actual_release_commit}" >&2
exit 1
fi
if [[ "${actual_dashboard_url}" != "${EXPECTED_DASHBOARD_URL}" ]]; then
echo "Remote dashboardUrl mismatch for ${domain}: expected ${EXPECTED_DASHBOARD_URL}, got ${actual_dashboard_url}" >&2
exit 1
fi
printf 'verified release image for %s: %s\n' "${domain}" "${actual_image_ref}" >&2
printf 'verified release commit for %s: %s\n' "${domain}" "${actual_release_commit}" >&2
printf 'verified dashboardUrl for %s: %s\n' "${domain}" "${actual_dashboard_url}" >&2
printf '%s\t%s\t%s\t%s\t%s\n' "${domain}" "${actual_image_ref}" "${actual_image_tag}" "${actual_release_commit}" "${actual_dashboard_url}"
}
mapfile -t served_domains < <(
printf '%s' "${SERVED_DOMAINS}" | tr ',' '\n' | while IFS= read -r domain; do
domain="$(trim "${domain}")"
if [[ -n "${domain}" ]]; then
printf '%s\n' "${domain}"
fi
done
)
if [[ "${#served_domains[@]}" -eq 0 ]]; then
echo "No served domains were provided." >&2
exit 1 exit 1
fi fi
if [[ ! "${actual_image_tag}" =~ ^[0-9a-f]{7,40}$ ]]; then canonical_found=false
echo "Remote image tag must contain a commit id, got: ${actual_image_tag}" >&2 declare -a verification_rows=()
for domain in "${served_domains[@]}"; do
if [[ "${domain}" == "${CANONICAL_DOMAIN}" ]]; then
canonical_found=true
fi
verification_rows+=("$(verify_domain "${domain}")")
done
if [[ "${canonical_found}" != "true" ]]; then
echo "Canonical domain ${CANONICAL_DOMAIN} must be included in served domains: ${SERVED_DOMAINS}" >&2
exit 1 exit 1
fi fi
if [[ "${actual_release_commit}" != "${actual_image_tag}" ]]; then reference_image_ref=""
echo "Remote release commit mismatch: expected ${actual_image_tag}, got ${actual_release_commit}" >&2 reference_image_tag=""
exit 1 reference_release_commit=""
fi reference_dashboard_url=""
printf 'verified release image: %s\n' "${actual_image_ref}" for row in "${verification_rows[@]}"; do
printf 'verified release commit: %s\n' "${actual_release_commit}" IFS=$'\t' read -r domain actual_image_ref actual_image_tag actual_release_commit actual_dashboard_url <<< "${row}"
if [[ -z "${reference_image_ref}" ]]; then
reference_image_ref="${actual_image_ref}"
reference_image_tag="${actual_image_tag}"
reference_release_commit="${actual_release_commit}"
reference_dashboard_url="${actual_dashboard_url}"
continue
fi
if [[ "${actual_image_ref}" != "${reference_image_ref}" ]]; then
echo "Release image mismatch across served domains: ${domain} returned ${actual_image_ref}, expected ${reference_image_ref}" >&2
exit 1
fi
if [[ "${actual_image_tag}" != "${reference_image_tag}" ]]; then
echo "Release tag mismatch across served domains: ${domain} returned ${actual_image_tag}, expected ${reference_image_tag}" >&2
exit 1
fi
if [[ "${actual_release_commit}" != "${reference_release_commit}" ]]; then
echo "Release commit mismatch across served domains: ${domain} returned ${actual_release_commit}, expected ${reference_release_commit}" >&2
exit 1
fi
if [[ "${actual_dashboard_url}" != "${reference_dashboard_url}" ]]; then
echo "dashboardUrl mismatch across served domains: ${domain} returned ${actual_dashboard_url}, expected ${reference_dashboard_url}" >&2
exit 1
fi
done

View File

@ -15,7 +15,7 @@ const DEFAULT_DESCRIPTION =
const DEFAULT_OG_IMAGE = '/icons/webchat.jpg' const DEFAULT_OG_IMAGE = '/icons/webchat.jpg'
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL('https://console.svc.plus'), metadataBase: new URL('https://www.svc.plus'),
title: { title: {
default: DEFAULT_TITLE, default: DEFAULT_TITLE,
template: '%s | Cloud-Neutral', template: '%s | Cloud-Neutral',
@ -93,8 +93,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'Organization', '@type': 'Organization',
name: 'Cloud-Neutral', name: 'Cloud-Neutral',
url: 'https://console.svc.plus', url: 'https://www.svc.plus',
logo: 'https://console.svc.plus/icons/cloudnative_32.png', logo: 'https://www.svc.plus/icons/cloudnative_32.png',
description: DEFAULT_DESCRIPTION, description: DEFAULT_DESCRIPTION,
}).replace(/</g, '\\u003c'), }).replace(/</g, '\\u003c'),
}} }}
@ -106,7 +106,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'WebSite', '@type': 'WebSite',
name: 'Cloud-Neutral', name: 'Cloud-Neutral',
url: 'https://console.svc.plus', url: 'https://www.svc.plus',
description: DEFAULT_DESCRIPTION, description: DEFAULT_DESCRIPTION,
}).replace(/</g, '\\u003c'), }).replace(/</g, '\\u003c'),
}} }}

View File

@ -3,7 +3,7 @@ import type { MetadataRoute } from 'next'
import { getBlogList } from '@/lib/docsServiceClient' import { getBlogList } from '@/lib/docsServiceClient'
import { PRODUCT_LIST } from '@/modules/products/registry' import { PRODUCT_LIST } from '@/modules/products/registry'
const baseUrl = 'https://console.svc.plus' const baseUrl = 'https://www.svc.plus'
export const dynamic = 'force-dynamic' export const dynamic = 'force-dynamic'
export const revalidate = 3600 // Revalidate every hour export const revalidate = 3600 // Revalidate every hour

View File

@ -1,6 +1,6 @@
# Base runtime configuration shared by all environments. # Base runtime configuration shared by all environments.
apiBaseUrl: https://rag-server-svc-plus-266500572462.asia-northeast1.run.app apiBaseUrl: https://rag-server-svc-plus-266500572462.asia-northeast1.run.app
authUrl: https://accounts.svc.plus authUrl: https://accounts.svc.plus
dashboardUrl: https://console.svc.plus dashboardUrl: https://www.svc.plus
docsServiceUrl: https://docs.svc.plus docsServiceUrl: https://docs.svc.plus
logLevel: info logLevel: info

View File

@ -4,10 +4,10 @@ regions:
cn: cn:
apiBaseUrl: https://rag-server-svc-plus-266500572462.asia-northeast1.run.app apiBaseUrl: https://rag-server-svc-plus-266500572462.asia-northeast1.run.app
authUrl: https://accounts.svc.plus authUrl: https://accounts.svc.plus
dashboardUrl: https://console.svc.plus dashboardUrl: https://www.svc.plus
docsServiceUrl: https://docs.svc.plus docsServiceUrl: https://docs.svc.plus
global: global:
apiBaseUrl: https://rag-server-svc-plus-266500572462.asia-northeast1.run.app apiBaseUrl: https://rag-server-svc-plus-266500572462.asia-northeast1.run.app
authUrl: https://accounts.svc.plus authUrl: https://accounts.svc.plus
dashboardUrl: https://console.svc.plus dashboardUrl: https://www.svc.plus
docsServiceUrl: https://docs.svc.plus docsServiceUrl: https://docs.svc.plus

View File

@ -24,6 +24,6 @@ describe("runtime-loader", () => {
const config = loadRuntimeConfig({ hostname: "console.svc.plus" }); const config = loadRuntimeConfig({ hostname: "console.svc.plus" });
expect(config.authUrl).toBe("https://accounts.svc.plus"); expect(config.authUrl).toBe("https://accounts.svc.plus");
expect(config.dashboardUrl).toBe("https://console.svc.plus"); expect(config.dashboardUrl).toBe("https://www.svc.plus");
}); });
}); });