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
|
||||
needs:
|
||||
- prep
|
||||
- build
|
||||
- deploy
|
||||
if: ${{ always() && needs.deploy.result == 'success' }}
|
||||
env:
|
||||
EXPECTED_FRONTEND_IMAGE: ${{ needs.build.outputs.image_ref }}
|
||||
steps:
|
||||
- name: Check Out Repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
@ -196,4 +199,5 @@ jobs:
|
||||
run: |
|
||||
bash scripts/github-actions/verify-frontend-release.sh \
|
||||
"${CANONICAL_DOMAIN}" \
|
||||
"${SERVED_DOMAINS}"
|
||||
"${SERVED_DOMAINS}" \
|
||||
"${EXPECTED_FRONTEND_IMAGE}"
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
## Production Baseline
|
||||
|
||||
- Runtime: `Caddy + Docker Compose`
|
||||
- Deploy host: `root@cn-console.svc.plus`
|
||||
- Deploy host: `root@jp-xhttp-contabo.svc.plus`
|
||||
- Public domains:
|
||||
- `www.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`.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
- Runtime: `console.svc.plus`
|
||||
- Topology: `Caddy + Docker Compose + GitHub Actions`
|
||||
- Deploy host: `root@cn-console.svc.plus`
|
||||
- Deploy host: `root@jp-xhttp-contabo.svc.plus`
|
||||
- Public domains:
|
||||
- `https://www.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
|
||||
|
||||
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
|
||||
|
||||
@ -124,9 +124,9 @@ PY
|
||||
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: 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 "cd /opt/console-svc-plus && docker compose --env-file .env.runtime ps"
|
||||
ssh root@jp-xhttp-contabo.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: console.svc.plus' http://127.0.0.1/"
|
||||
curl -fsSIL https://www.svc.plus
|
||||
curl -fsSIL https://console.svc.plus
|
||||
```
|
||||
@ -147,8 +147,8 @@ curl -fsSIL https://console.svc.plus
|
||||
3. Restart the services:
|
||||
|
||||
```bash
|
||||
ssh root@cn-console.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 run --rm frontend-assets"
|
||||
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.
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
## 生产基线
|
||||
|
||||
- 运行拓扑: `Caddy + Docker Compose`
|
||||
- 目标主机: `root@cn-console.svc.plus`
|
||||
- 目标主机: `root@jp-xhttp-contabo.svc.plus`
|
||||
- 域名:
|
||||
- `www.svc.plus`
|
||||
- `console.svc.plus`
|
||||
@ -22,9 +22,9 @@
|
||||
- Next.js standalone 容器只承接动态页面、认证接口和代理接口,`frontend-assets` 任务会把所有静态文件(包括哈希后的 CSS/JS)拷贝到 `frontend_static`。
|
||||
- `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 内容的独立服务。
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ set -euo pipefail
|
||||
IMAGE_TAG_INPUT="${1-}"
|
||||
IMAGE_TAG="${IMAGE_TAG_INPUT}"
|
||||
if [[ -z "${IMAGE_TAG}" ]]; then
|
||||
IMAGE_TAG="${GITHUB_SHA::7}"
|
||||
IMAGE_TAG="${GITHUB_SHA:?GITHUB_SHA is required}"
|
||||
fi
|
||||
|
||||
GHCR_NAMESPACE="${GITHUB_REPOSITORY_OWNER,,}"
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CANONICAL_DOMAIN="${1:?usage: verify-frontend-release.sh <canonical-domain> <served-domains>}"
|
||||
SERVED_DOMAINS="${2:?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> <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}"
|
||||
|
||||
curl_headers=(
|
||||
@ -18,6 +19,35 @@ trim() {
|
||||
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() {
|
||||
local payload="$1"
|
||||
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}"
|
||||
}
|
||||
|
||||
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 < <(
|
||||
printf '%s' "${SERVED_DOMAINS}" | tr ',' '\n' | while IFS= read -r domain; do
|
||||
domain="$(trim "${domain}")"
|
||||
@ -138,6 +178,21 @@ reference_dashboard_url=""
|
||||
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 [[ "${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
|
||||
reference_image_ref="${actual_image_ref}"
|
||||
reference_image_tag="${actual_image_tag}"
|
||||
@ -145,24 +200,4 @@ for row in "${verification_rows[@]}"; do
|
||||
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
|
||||
|
||||
@ -2,11 +2,31 @@ import { NextResponse } from 'next/server'
|
||||
|
||||
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 releaseImageTagMatch = releaseImageRef?.match(/:([^:@]+)$/)
|
||||
const releaseImageTag = releaseImageTagMatch?.[1] ?? null
|
||||
const releaseCommit = releaseImageTag && /^[0-9a-f]{7,40}$/i.test(releaseImageTag) ? releaseImageTag : null
|
||||
const releaseCommit = resolveReleaseCommit(releaseImageTag)
|
||||
|
||||
return {
|
||||
releaseImageRef,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user