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:
- ".github/workflows/pipeline.yaml"
- "Dockerfile"
- "deploy/single-node/**"
- "package.json"
- "yarn.lock"
- "scripts/github-actions/build-and-push-frontend-image.sh"
- "scripts/github-actions/compute-frontend-release-metadata.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/run-console-deploy-playbook.sh"
- "scripts/github-actions/verify-frontend-release.sh"
- "scripts/prebuild.sh"
- "contentlayer.config.ts"
@ -42,7 +45,8 @@ concurrency:
cancel-in-progress: false
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_REGION: cn
ACCOUNT_SERVICE_URL: https://accounts.svc.plus
@ -176,4 +180,5 @@ jobs:
- name: Verify Frontend Release
run: |
bash scripts/github-actions/verify-frontend-release.sh \
"${PRIMARY_DOMAIN}"
"${CANONICAL_DOMAIN}" \
"${SERVED_DOMAINS}"

View File

@ -1,21 +1,22 @@
# Compose settings
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
NODE_ENV=production
PORT=3000
RUNTIME_ENV=prod
REGION=cn
APP_BASE_URL=https://cn-console.svc.plus
NEXT_PUBLIC_APP_BASE_URL=https://cn-console.svc.plus
NEXT_PUBLIC_SITE_URL=https://cn-console.svc.plus
NEXT_PUBLIC_LOGIN_URL=https://cn-console.svc.plus/login
NEXT_PUBLIC_DOCS_BASE_URL=https://cn-console.svc.plus/docs
APP_BASE_URL=https://www.svc.plus
NEXT_PUBLIC_APP_BASE_URL=https://www.svc.plus
NEXT_PUBLIC_SITE_URL=https://www.svc.plus
NEXT_PUBLIC_LOGIN_URL=https://www.svc.plus/login
NEXT_PUBLIC_DOCS_BASE_URL=https://www.svc.plus/docs
SESSION_COOKIE_SECURE=true
NEXT_PUBLIC_SESSION_COOKIE_SECURE=true
RUNTIME_HOSTNAME=cn-console.svc.plus
DEPLOYMENT_HOSTNAME=cn-console.svc.plus
RUNTIME_HOSTNAME=www.svc.plus
DEPLOYMENT_HOSTNAME=www.svc.plus
NEXT_PUBLIC_RUNTIME_ENVIRONMENT=prod
NEXT_PUBLIC_RUNTIME_REGION=cn

View File

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

View File

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

View File

@ -214,7 +214,7 @@ export default function NotFound() {
import type { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://console.svc.plus'),
metadataBase: new URL('https://www.svc.plus'),
title: {
default: 'Cloud-Neutral | Unified Cloud Native Tools',
template: '%s | Cloud-Neutral',
@ -232,7 +232,7 @@ export const metadata: Metadata = {
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://console.svc.plus',
url: 'https://www.svc.plus',
title: 'Cloud-Neutral | Unified Cloud Native Tools',
description: 'Unified tools for your cloud native stack',
siteName: 'Cloud-Neutral',
@ -274,7 +274,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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 */}
</head>
{/* ... rest of layout */}
@ -343,7 +343,7 @@ Disallow: /admin/
Disallow: /api/
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',
'@type': 'Organization',
name: 'Cloud-Neutral',
url: 'https://console.svc.plus',
logo: 'https://console.svc.plus/logo.png',
url: 'https://www.svc.plus',
logo: 'https://www.svc.plus/logo.png',
sameAs: [
'https://twitter.com/cloudneutral',
'https://github.com/x-evor',
@ -372,10 +372,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Cloud-Neutral',
url: 'https://console.svc.plus',
url: 'https://www.svc.plus',
potentialAction: {
'@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',
},
}

View File

@ -4,9 +4,11 @@
- Runtime: `Caddy + Docker Compose`
- Deploy host: `root@cn-console.svc.plus`
- Domains:
- `cn-console.svc.plus`
- Frontend release workflow: `.github/workflows/service_release_frontend-deploy.yml`
- Public domains:
- `www.svc.plus`
- `console.svc.plus`
- Canonical public origin: `https://www.svc.plus`
- Frontend release workflow: `.github/workflows/pipeline.yaml`
## 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.
- `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`.

View File

@ -1,6 +1,6 @@
# 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
@ -49,3 +49,5 @@ Published commit: `0fab89e`
- 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`
- 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
CLOUDFLARE_API_TOKEN=...
@ -58,7 +58,7 @@ CLOUDFLARE_WEB_ANALYTICS_SITE_TAG=...
### 线上部署
把同名变量写入 `console.svc.plus` 的部署环境(例如 Vercel/Cloud Run 的环境变量配置)
把同名变量写入前端部署环境
> 注意:这些变量属于服务端密钥,不要暴露到 `NEXT_PUBLIC_*`
@ -67,7 +67,7 @@ CLOUDFLARE_WEB_ANALYTICS_SITE_TAG=...
部署后访问:
```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`)。
@ -77,4 +77,3 @@ curl -fsSL https://console.svc.plus/api/marketing/home-stats
1. token 权限是否包含 Analytics Read
2. Account ID 是否与 siteTag 属于同一账号
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) │
└──────┬───────┘ └────────┬─────────┘ └────────┬─────────┘
│ 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 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 |
|---|---|
| **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` |
| **Enable Device Flow** | ☐ (unchecked) |
@ -119,7 +119,7 @@ No additional GitHub permissions are required.
|---|---|
| **Application type** | `Web application` |
| **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` |
4. Click **"Create"**
@ -150,7 +150,7 @@ GOOGLE_CLIENT_SECRET=<your_google_client_secret>
# ── General OAuth ──
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`:
@ -159,7 +159,7 @@ These variables are referenced in `config/account.yaml`:
auth:
oauth:
redirectUrl: "${OAUTH_REDIRECT_URL}"
frontendUrl: "${OAUTH_FRONTEND_URL:-https://console.svc.plus}"
frontendUrl: "${OAUTH_FRONTEND_URL:-https://www.svc.plus}"
github:
clientId: "${GITHUB_CLIENT_ID}"
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:
@ -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
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"

View File

@ -6,8 +6,9 @@
- Topology: `Caddy + Docker Compose + GitHub Actions`
- Deploy host: `root@cn-console.svc.plus`
- Public domains:
- `https://cn-console.svc.plus`
- Primary origin: `https://cn-console.svc.plus`
- `https://www.svc.plus`
- `https://console.svc.plus`
- Canonical public origin: `https://www.svc.plus`
## 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
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
@ -52,7 +53,7 @@ Containers:
Workflow:
```text
.github/workflows/service_release_frontend-deploy.yml
.github/workflows/pipeline.yaml
```
Secrets required:
@ -67,6 +68,8 @@ Secrets required:
Repository/environment variables recommended:
- `CANONICAL_DOMAIN`
- `SERVED_DOMAINS`
- `APP_BASE_URL`
- `NEXT_PUBLIC_APP_BASE_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.
3. The image is pushed to GHCR.
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.
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
@ -113,7 +116,7 @@ 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())
yaml.safe_load(Path('.github/workflows/pipeline.yaml').read_text())
print('workflow yaml ok')
PY
```
@ -122,8 +125,10 @@ Remote checks:
```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 "curl -fsSI -H 'Host: cn-console.svc.plus' http://127.0.0.1/"
curl -fsSIL https://cn-console.svc.plus
ssh root@cn-console.svc.plus "curl -fsSI -H 'Host: www.svc.plus' http://127.0.0.1/"
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
@ -132,7 +137,7 @@ curl -fsSIL https://cn-console.svc.plus
The workflow token or package visibility is wrong.
- `frontend-assets` fails
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`.
## 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"
```
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`
- 目标主机: `root@cn-console.svc.plus`
- 域名:
- `cn-console.svc.plus`
- 前端独立发布流水线: `.github/workflows/service_release_frontend-deploy.yml`
- `www.svc.plus`
- `console.svc.plus`
- 公开首选域名: `www.svc.plus`
- 前端独立发布流水线: `.github/workflows/pipeline.yaml`
## 运行方式
@ -20,12 +22,19 @@
- Next.js standalone 容器只承接动态页面、认证接口和代理接口,`frontend-assets` 任务会把所有静态文件(包括哈希后的 CSS/JS拷贝到 `frontend_static`
- `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`
`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`

View File

@ -2,7 +2,7 @@
> 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`
- 发布校验必须同时验证 `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`

View File

@ -14,7 +14,7 @@ require_env() {
}
require_env IMAGE_REF
require_env PRIMARY_DOMAIN
require_env CANONICAL_DOMAIN
BUILD_ARGS_FILE="$(mktemp)"
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_PASSWORD
require_env FRONTEND_IMAGE
require_env PRIMARY_DOMAIN
require_env CANONICAL_DOMAIN
require_env SERVED_DOMAINS
GHCR_REGISTRY="${GHCR_REGISTRY:-ghcr.io}"

View File

@ -13,16 +13,16 @@ require_env() {
}
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_RUNTIME_IMAGE=%s\n' "${NODE_RUNTIME_IMAGE:-node:22-slim}"
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_SITE_URL=%s\n' "${NEXT_PUBLIC_SITE_URL:-https://${primary_domain}}"
printf 'NEXT_PUBLIC_LOGIN_URL=%s\n' "${NEXT_PUBLIC_LOGIN_URL:-https://${primary_domain}/login}"
printf 'NEXT_PUBLIC_DOCS_BASE_URL=%s\n' "${NEXT_PUBLIC_DOCS_BASE_URL:-https://${primary_domain}/docs}"
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://${canonical_domain}}"
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://${canonical_domain}/docs}"
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_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 PRIMARY_DOMAIN
require_env CANONICAL_DOMAIN
require_env SERVED_DOMAINS
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 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 APP_BASE_URL "${APP_BASE_URL:-https://${CANONICAL_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://${CANONICAL_DOMAIN}}"
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://${CANONICAL_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 RUNTIME_HOSTNAME "${RUNTIME_HOSTNAME:-${CANONICAL_DOMAIN}}"
append_env NEXT_RUNTIME_HOSTNAME "${NEXT_RUNTIME_HOSTNAME:-${CANONICAL_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_REGION "${NEXT_PUBLIC_RUNTIME_REGION:-cn}"
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 "INTERNAL_SERVICE_TOKEN=${INTERNAL_SERVICE_TOKEN:?INTERNAL_SERVICE_TOKEN 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_REGION=${NEXT_PUBLIC_RUNTIME_REGION:?NEXT_PUBLIC_RUNTIME_REGION is required}"
-e "CLOUDFLARE_ZONE_TAG=${CLOUDFLARE_ZONE_TAG:?CLOUDFLARE_ZONE_TAG is required}"

View File

@ -1,25 +1,26 @@
#!/usr/bin/env bash
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}")"
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 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'
parse_release_metadata() {
local payload="$1"
RELEASE_PAYLOAD="${payload}" python3 - <<'PY'
import json
import os
@ -27,28 +28,141 @@ payload = json.loads(os.environ["RELEASE_PAYLOAD"])
print(payload.get("releaseImageRef", ""))
print(payload.get("releaseImageTag", ""))
print(payload.get("releaseCommit", ""))
print(payload.get("dashboardUrl", ""))
PY
)"
}
mapfile -t release_lines <<< "${release_metadata}"
actual_image_ref="${release_lines[0]-}"
actual_image_tag="${release_lines[1]-}"
actual_release_commit="${release_lines[2]-}"
require_http_200() {
local url="$1"
shift
if [[ -z "${actual_image_ref}" || -z "${actual_image_tag}" || -z "${actual_release_commit}" ]]; then
echo "Remote release metadata is incomplete: ${release_payload}" >&2
local http_code
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
fi
if [[ ! "${actual_image_tag}" =~ ^[0-9a-f]{7,40}$ ]]; then
echo "Remote image tag must contain a commit id, got: ${actual_image_tag}" >&2
canonical_found=false
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
fi
if [[ "${actual_release_commit}" != "${actual_image_tag}" ]]; then
echo "Remote release commit mismatch: expected ${actual_image_tag}, got ${actual_release_commit}" >&2
exit 1
fi
reference_image_ref=""
reference_image_tag=""
reference_release_commit=""
reference_dashboard_url=""
printf 'verified release image: %s\n' "${actual_image_ref}"
printf 'verified release commit: %s\n' "${actual_release_commit}"
for row in "${verification_rows[@]}"; do
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'
export const metadata: Metadata = {
metadataBase: new URL('https://console.svc.plus'),
metadataBase: new URL('https://www.svc.plus'),
title: {
default: DEFAULT_TITLE,
template: '%s | Cloud-Neutral',
@ -93,8 +93,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'Cloud-Neutral',
url: 'https://console.svc.plus',
logo: 'https://console.svc.plus/icons/cloudnative_32.png',
url: 'https://www.svc.plus',
logo: 'https://www.svc.plus/icons/cloudnative_32.png',
description: DEFAULT_DESCRIPTION,
}).replace(/</g, '\\u003c'),
}}
@ -106,7 +106,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
'@context': 'https://schema.org',
'@type': 'WebSite',
name: 'Cloud-Neutral',
url: 'https://console.svc.plus',
url: 'https://www.svc.plus',
description: DEFAULT_DESCRIPTION,
}).replace(/</g, '\\u003c'),
}}

View File

@ -3,7 +3,7 @@ import type { MetadataRoute } from 'next'
import { getBlogList } from '@/lib/docsServiceClient'
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 revalidate = 3600 // Revalidate every hour

View File

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

View File

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

View File

@ -24,6 +24,6 @@ describe("runtime-loader", () => {
const config = loadRuntimeConfig({ hostname: "console.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");
});
});