Align console release verification with build image ref
This commit is contained in:
parent
5f1b59be70
commit
318f407222
6
.github/workflows/pipeline.yaml
vendored
6
.github/workflows/pipeline.yaml
vendored
@ -186,8 +186,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- prep
|
- prep
|
||||||
|
- build
|
||||||
- deploy
|
- deploy
|
||||||
if: ${{ always() && needs.deploy.result == 'success' }}
|
if: ${{ always() && needs.deploy.result == 'success' }}
|
||||||
|
env:
|
||||||
|
EXPECTED_FRONTEND_IMAGE: ${{ needs.build.outputs.image_ref }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check Out Repository
|
- name: Check Out Repository
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@ -196,4 +199,5 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
bash scripts/github-actions/verify-frontend-release.sh \
|
bash scripts/github-actions/verify-frontend-release.sh \
|
||||||
"${CANONICAL_DOMAIN}" \
|
"${CANONICAL_DOMAIN}" \
|
||||||
"${SERVED_DOMAINS}"
|
"${SERVED_DOMAINS}" \
|
||||||
|
"${EXPECTED_FRONTEND_IMAGE}"
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
## Production Baseline
|
## Production Baseline
|
||||||
|
|
||||||
- Runtime: `Caddy + Docker Compose`
|
- Runtime: `Caddy + Docker Compose`
|
||||||
- Deploy host: `root@cn-console.svc.plus`
|
- Deploy host: `root@jp-xhttp-contabo.svc.plus`
|
||||||
- Public domains:
|
- Public domains:
|
||||||
- `www.svc.plus`
|
- `www.svc.plus`
|
||||||
- `console.svc.plus`
|
- `console.svc.plus`
|
||||||
@ -31,7 +31,7 @@ The release contract now uses:
|
|||||||
|
|
||||||
Validation must pass for both domains. A release is incomplete if either host serves a different runtime version, static asset family, or `dashboardUrl`.
|
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@jp-xhttp-contabo.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`.
|
||||||
|
|
||||||
`docs.svc.plus` is now the dedicated docs/blog service for the frontend delivery path.
|
`docs.svc.plus` is now the dedicated docs/blog service for the frontend delivery path.
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- Runtime: `console.svc.plus`
|
- Runtime: `console.svc.plus`
|
||||||
- Topology: `Caddy + Docker Compose + GitHub Actions`
|
- Topology: `Caddy + Docker Compose + GitHub Actions`
|
||||||
- Deploy host: `root@cn-console.svc.plus`
|
- Deploy host: `root@jp-xhttp-contabo.svc.plus`
|
||||||
- Public domains:
|
- Public domains:
|
||||||
- `https://www.svc.plus`
|
- `https://www.svc.plus`
|
||||||
- `https://console.svc.plus`
|
- `https://console.svc.plus`
|
||||||
@ -24,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/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`.
|
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 through the current production host `jp-xhttp-contabo.svc.plus`.
|
||||||
|
|
||||||
## Runtime Layout
|
## Runtime Layout
|
||||||
|
|
||||||
@ -124,9 +124,9 @@ PY
|
|||||||
Remote checks:
|
Remote checks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime ps"
|
ssh root@jp-xhttp-contabo.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime ps"
|
||||||
ssh root@cn-console.svc.plus "curl -fsSI -H 'Host: www.svc.plus' http://127.0.0.1/"
|
ssh root@jp-xhttp-contabo.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/"
|
ssh root@jp-xhttp-contabo.svc.plus "curl -fsSI -H 'Host: console.svc.plus' http://127.0.0.1/"
|
||||||
curl -fsSIL https://www.svc.plus
|
curl -fsSIL https://www.svc.plus
|
||||||
curl -fsSIL https://console.svc.plus
|
curl -fsSIL https://console.svc.plus
|
||||||
```
|
```
|
||||||
@ -147,8 +147,8 @@ curl -fsSIL https://console.svc.plus
|
|||||||
3. Restart the services:
|
3. Restart the services:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime run --rm frontend-assets"
|
ssh root@jp-xhttp-contabo.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime run --rm frontend-assets"
|
||||||
ssh root@cn-console.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime up -d dashboard caddy"
|
ssh root@jp-xhttp-contabo.svc.plus "cd /opt/console-svc-plus && docker compose --env-file .env.runtime up -d dashboard caddy"
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Verify `https://www.svc.plus` and `https://console.svc.plus` again before closing the incident.
|
4. Verify `https://www.svc.plus` and `https://console.svc.plus` again before closing the incident.
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
## 生产基线
|
## 生产基线
|
||||||
|
|
||||||
- 运行拓扑: `Caddy + Docker Compose`
|
- 运行拓扑: `Caddy + Docker Compose`
|
||||||
- 目标主机: `root@cn-console.svc.plus`
|
- 目标主机: `root@jp-xhttp-contabo.svc.plus`
|
||||||
- 域名:
|
- 域名:
|
||||||
- `www.svc.plus`
|
- `www.svc.plus`
|
||||||
- `console.svc.plus`
|
- `console.svc.plus`
|
||||||
@ -22,9 +22,9 @@
|
|||||||
- 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/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` 指向更新后的环境。
|
发布由 `.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,使当前生产主机 `jp-xhttp-contabo.svc.plus` 承载更新后的环境。
|
||||||
|
|
||||||
这是针对弱 IO 单机主机 `root@cn-console.svc.plus` 的部署权衡:主机不会在本地构建镜像,只需登录 GHCR、拉取 `dashboard` 镜像、解包静态资源到 `frontend_static`,再通过 `docker compose` 启动 `dashboard` 与 `caddy`。
|
这是针对弱 IO 单机主机 `root@jp-xhttp-contabo.svc.plus` 的部署权衡:主机不会在本地构建镜像,只需登录 GHCR、拉取 `dashboard` 镜像、解包静态资源到 `frontend_static`,再通过 `docker compose` 启动 `dashboard` 与 `caddy`。
|
||||||
|
|
||||||
`docs.svc.plus` 已经是前端 docs/blog 内容的独立服务。
|
`docs.svc.plus` 已经是前端 docs/blog 内容的独立服务。
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ set -euo pipefail
|
|||||||
IMAGE_TAG_INPUT="${1-}"
|
IMAGE_TAG_INPUT="${1-}"
|
||||||
IMAGE_TAG="${IMAGE_TAG_INPUT}"
|
IMAGE_TAG="${IMAGE_TAG_INPUT}"
|
||||||
if [[ -z "${IMAGE_TAG}" ]]; then
|
if [[ -z "${IMAGE_TAG}" ]]; then
|
||||||
IMAGE_TAG="${GITHUB_SHA::7}"
|
IMAGE_TAG="${GITHUB_SHA:?GITHUB_SHA is required}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
GHCR_NAMESPACE="${GITHUB_REPOSITORY_OWNER,,}"
|
GHCR_NAMESPACE="${GITHUB_REPOSITORY_OWNER,,}"
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
CANONICAL_DOMAIN="${1:?usage: verify-frontend-release.sh <canonical-domain> <served-domains>}"
|
CANONICAL_DOMAIN="${1:?usage: verify-frontend-release.sh <canonical-domain> <served-domains> <expected-image-ref>}"
|
||||||
SERVED_DOMAINS="${2:?usage: verify-frontend-release.sh <canonical-domain> <served-domains>}"
|
SERVED_DOMAINS="${2:?usage: verify-frontend-release.sh <canonical-domain> <served-domains> <expected-image-ref>}"
|
||||||
|
EXPECTED_IMAGE_REF="${3:?usage: verify-frontend-release.sh <canonical-domain> <served-domains> <expected-image-ref>}"
|
||||||
EXPECTED_DASHBOARD_URL="https://${CANONICAL_DOMAIN}"
|
EXPECTED_DASHBOARD_URL="https://${CANONICAL_DOMAIN}"
|
||||||
|
|
||||||
curl_headers=(
|
curl_headers=(
|
||||||
@ -18,6 +19,35 @@ trim() {
|
|||||||
printf '%s' "${value}"
|
printf '%s' "${value}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parse_image_ref() {
|
||||||
|
local image_ref="$1"
|
||||||
|
|
||||||
|
IMAGE_REF="${image_ref}" python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
image_ref = os.environ["IMAGE_REF"].strip()
|
||||||
|
match = re.search(r":([^:@]+)$", image_ref)
|
||||||
|
tag = match.group(1) if match else ""
|
||||||
|
commit = ""
|
||||||
|
|
||||||
|
if re.fullmatch(r"[0-9a-f]{7,40}", tag, flags=re.IGNORECASE):
|
||||||
|
commit = tag
|
||||||
|
else:
|
||||||
|
prefixed_match = re.fullmatch(r"sha-([0-9a-f]{7,40})", tag, flags=re.IGNORECASE)
|
||||||
|
if prefixed_match:
|
||||||
|
commit = prefixed_match.group(1)
|
||||||
|
|
||||||
|
if not image_ref or not tag or not commit:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(image_ref)
|
||||||
|
print(tag)
|
||||||
|
print(commit)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
parse_release_metadata() {
|
parse_release_metadata() {
|
||||||
local payload="$1"
|
local payload="$1"
|
||||||
RELEASE_PAYLOAD="${payload}" python3 - <<'PY'
|
RELEASE_PAYLOAD="${payload}" python3 - <<'PY'
|
||||||
@ -101,6 +131,16 @@ verify_domain() {
|
|||||||
printf '%s\t%s\t%s\t%s\t%s\n' "${domain}" "${actual_image_ref}" "${actual_image_tag}" "${actual_release_commit}" "${actual_dashboard_url}"
|
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 expected_release_lines < <(parse_image_ref "${EXPECTED_IMAGE_REF}")
|
||||||
|
EXPECTED_RELEASE_IMAGE_REF="${expected_release_lines[0]-}"
|
||||||
|
EXPECTED_RELEASE_IMAGE_TAG="${expected_release_lines[1]-}"
|
||||||
|
EXPECTED_RELEASE_COMMIT="${expected_release_lines[2]-}"
|
||||||
|
|
||||||
|
if [[ -z "${EXPECTED_RELEASE_IMAGE_REF}" || -z "${EXPECTED_RELEASE_IMAGE_TAG}" || -z "${EXPECTED_RELEASE_COMMIT}" ]]; then
|
||||||
|
echo "Expected image ref is invalid: ${EXPECTED_IMAGE_REF}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
mapfile -t served_domains < <(
|
mapfile -t served_domains < <(
|
||||||
printf '%s' "${SERVED_DOMAINS}" | tr ',' '\n' | while IFS= read -r domain; do
|
printf '%s' "${SERVED_DOMAINS}" | tr ',' '\n' | while IFS= read -r domain; do
|
||||||
domain="$(trim "${domain}")"
|
domain="$(trim "${domain}")"
|
||||||
@ -138,6 +178,21 @@ reference_dashboard_url=""
|
|||||||
for row in "${verification_rows[@]}"; do
|
for row in "${verification_rows[@]}"; do
|
||||||
IFS=$'\t' read -r domain actual_image_ref actual_image_tag actual_release_commit actual_dashboard_url <<< "${row}"
|
IFS=$'\t' read -r domain actual_image_ref actual_image_tag actual_release_commit actual_dashboard_url <<< "${row}"
|
||||||
|
|
||||||
|
if [[ "${actual_image_ref}" != "${EXPECTED_RELEASE_IMAGE_REF}" ]]; then
|
||||||
|
echo "Release image mismatch for ${domain}: expected ${EXPECTED_RELEASE_IMAGE_REF}, got ${actual_image_ref}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${actual_image_tag}" != "${EXPECTED_RELEASE_IMAGE_TAG}" ]]; then
|
||||||
|
echo "Release tag mismatch for ${domain}: expected ${EXPECTED_RELEASE_IMAGE_TAG}, got ${actual_image_tag}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${actual_release_commit}" != "${EXPECTED_RELEASE_COMMIT}" ]]; then
|
||||||
|
echo "Release commit mismatch for ${domain}: expected ${EXPECTED_RELEASE_COMMIT}, got ${actual_release_commit}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "${reference_image_ref}" ]]; then
|
if [[ -z "${reference_image_ref}" ]]; then
|
||||||
reference_image_ref="${actual_image_ref}"
|
reference_image_ref="${actual_image_ref}"
|
||||||
reference_image_tag="${actual_image_tag}"
|
reference_image_tag="${actual_image_tag}"
|
||||||
@ -145,24 +200,4 @@ for row in "${verification_rows[@]}"; do
|
|||||||
reference_dashboard_url="${actual_dashboard_url}"
|
reference_dashboard_url="${actual_dashboard_url}"
|
||||||
continue
|
continue
|
||||||
fi
|
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
|
done
|
||||||
|
|||||||
@ -2,11 +2,31 @@ import { NextResponse } from 'next/server'
|
|||||||
|
|
||||||
import { loadRuntimeConfig } from '@server/runtime-loader'
|
import { loadRuntimeConfig } from '@server/runtime-loader'
|
||||||
|
|
||||||
function resolveReleaseImageMetadata(frontendImage: string | undefined) {
|
type TReleaseImageMetadata = {
|
||||||
|
releaseImageRef: string | null
|
||||||
|
releaseImageTag: string | null
|
||||||
|
releaseCommit: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveReleaseCommit(releaseImageTag: string | null): string | null {
|
||||||
|
if (!releaseImageTag) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedTag = releaseImageTag.trim()
|
||||||
|
const prefixedShaMatch = normalizedTag.match(/^sha-([0-9a-f]{7,40})$/i)
|
||||||
|
if (prefixedShaMatch) {
|
||||||
|
return prefixedShaMatch[1] ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
return /^[0-9a-f]{7,40}$/i.test(normalizedTag) ? normalizedTag : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveReleaseImageMetadata(frontendImage: string | undefined): TReleaseImageMetadata {
|
||||||
const releaseImageRef = frontendImage?.trim() || null
|
const releaseImageRef = frontendImage?.trim() || null
|
||||||
const releaseImageTagMatch = releaseImageRef?.match(/:([^:@]+)$/)
|
const releaseImageTagMatch = releaseImageRef?.match(/:([^:@]+)$/)
|
||||||
const releaseImageTag = releaseImageTagMatch?.[1] ?? null
|
const releaseImageTag = releaseImageTagMatch?.[1] ?? null
|
||||||
const releaseCommit = releaseImageTag && /^[0-9a-f]{7,40}$/i.test(releaseImageTag) ? releaseImageTag : null
|
const releaseCommit = resolveReleaseCommit(releaseImageTag)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
releaseImageRef,
|
releaseImageRef,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user