diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 228275d..3339a31 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -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}" diff --git a/deploy/single-node/.env.runtime.example b/deploy/single-node/.env.runtime.example index 3758562..0f0f36c 100644 --- a/deploy/single-node/.env.runtime.example +++ b/deploy/single-node/.env.runtime.example @@ -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 diff --git a/deploy/single-node/Caddyfile b/deploy/single-node/Caddyfile index 78fcf10..8e9aea1 100644 --- a/deploy/single-node/Caddyfile +++ b/deploy/single-node/Caddyfile @@ -1,4 +1,4 @@ -{$PRIMARY_DOMAIN} { +{$SERVED_DOMAINS} { encode zstd gzip handle_path /_next/static/* { diff --git a/deploy/single-node/docker-compose.yml b/deploy/single-node/docker-compose.yml index 45d47f2..d6e7be9 100644 --- a/deploy/single-node/docker-compose.yml +++ b/deploy/single-node/docker-compose.yml @@ -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 diff --git a/docs/SEO-AUDIT-REPORT.md b/docs/SEO-AUDIT-REPORT.md index 0c56956..9065b5d 100644 --- a/docs/SEO-AUDIT-REPORT.md +++ b/docs/SEO-AUDIT-REPORT.md @@ -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 }) - + {/* ... rest of 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', }, } diff --git a/docs/en/deployment.md b/docs/en/deployment.md index 9a5fafd..e2d7add 100644 --- a/docs/en/deployment.md +++ b/docs/en/deployment.md @@ -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`. diff --git a/docs/governance/release-process.md b/docs/governance/release-process.md index bdd030d..8dc6945 100644 --- a/docs/governance/release-process.md +++ b/docs/governance/release-process.md @@ -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. diff --git a/docs/integrations/cloudflare-web-analytics.md b/docs/integrations/cloudflare-web-analytics.md index a8d612a..29057bd 100644 --- a/docs/integrations/cloudflare-web-analytics.md +++ b/docs/integrations/cloudflare-web-analytics.md @@ -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. 环境变量是否已在当前运行实例生效(重启/重新部署后再测) - diff --git a/docs/integrations/oidc-auth.md b/docs/integrations/oidc-auth.md index 841658f..349edb6 100644 --- a/docs/integrations/oidc-auth.md +++ b/docs/integrations/oidc-auth.md @@ -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= # ── 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" diff --git a/docs/usage/deployment.md b/docs/usage/deployment.md index f2f98d1..195118b 100644 --- a/docs/usage/deployment.md +++ b/docs/usage/deployment.md @@ -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. diff --git a/docs/zh/deployment.md b/docs/zh/deployment.md index 70c42be..39e3305 100644 --- a/docs/zh/deployment.md +++ b/docs/zh/deployment.md @@ -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` diff --git a/docs/zh/governance/release-process.md b/docs/zh/governance/release-process.md index eb812d6..d50219a 100644 --- a/docs/zh/governance/release-process.md +++ b/docs/zh/governance/release-process.md @@ -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` diff --git a/scripts/github-actions/build-and-push-frontend-image.sh b/scripts/github-actions/build-and-push-frontend-image.sh index c135598..926367c 100755 --- a/scripts/github-actions/build-and-push-frontend-image.sh +++ b/scripts/github-actions/build-and-push-frontend-image.sh @@ -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 diff --git a/scripts/github-actions/deploy-frontend-single-node.sh b/scripts/github-actions/deploy-frontend-single-node.sh index 2e0c66b..5ef6609 100755 --- a/scripts/github-actions/deploy-frontend-single-node.sh +++ b/scripts/github-actions/deploy-frontend-single-node.sh @@ -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}" diff --git a/scripts/github-actions/render-frontend-build-args.sh b/scripts/github-actions/render-frontend-build-args.sh index b3c589d..549caf2 100755 --- a/scripts/github-actions/render-frontend-build-args.sh +++ b/scripts/github-actions/render-frontend-build-args.sh @@ -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}" diff --git a/scripts/github-actions/render-frontend-runtime-env.sh b/scripts/github-actions/render-frontend-runtime-env.sh index 0bb29df..103576b 100755 --- a/scripts/github-actions/render-frontend-runtime-env.sh +++ b/scripts/github-actions/render-frontend-runtime-env.sh @@ -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}" diff --git a/scripts/github-actions/run-console-deploy-playbook.sh b/scripts/github-actions/run-console-deploy-playbook.sh index 9947a4f..4eaa4b6 100644 --- a/scripts/github-actions/run-console-deploy-playbook.sh +++ b/scripts/github-actions/run-console-deploy-playbook.sh @@ -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}" diff --git a/scripts/github-actions/verify-frontend-release.sh b/scripts/github-actions/verify-frontend-release.sh index 41bdda7..642b956 100755 --- a/scripts/github-actions/verify-frontend-release.sh +++ b/scripts/github-actions/verify-frontend-release.sh @@ -1,25 +1,26 @@ #!/usr/bin/env bash set -euo pipefail -PRIMARY_DOMAIN="${1:?usage: verify-frontend-release.sh }" +CANONICAL_DOMAIN="${1:?usage: verify-frontend-release.sh }" +SERVED_DOMAINS="${2:?usage: verify-frontend-release.sh }" +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 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5934bf1..96da0b7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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(/ { 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"); }); });