diff --git a/Dockerfile b/Dockerfile index 90ec776..9766a8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,10 @@ ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO= ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION= ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO= ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION= +ARG NEXT_PUBLIC_RELEASE_IMAGE= +ARG NEXT_PUBLIC_RELEASE_TAG= +ARG NEXT_PUBLIC_RELEASE_COMMIT= +ARG NEXT_PUBLIC_RELEASE_VERSION= # ------------------------------------------------------- # Stage 1 — Builder (Turbopack + standalone) @@ -46,6 +50,10 @@ ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO ARG NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO ARG NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION +ARG NEXT_PUBLIC_RELEASE_IMAGE +ARG NEXT_PUBLIC_RELEASE_TAG +ARG NEXT_PUBLIC_RELEASE_COMMIT +ARG NEXT_PUBLIC_RELEASE_VERSION ENV NEXT_TELEMETRY_DISABLED=1 \ NEXT_PRIVATE_TURBOPACK=1 \ @@ -65,7 +73,11 @@ ENV NEXT_TELEMETRY_DISABLED=1 \ NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO=${NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_PAYGO} \ NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION=${NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION} \ NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO=${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO} \ - NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION=${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION} + NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION=${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION} \ + NEXT_PUBLIC_RELEASE_IMAGE=${NEXT_PUBLIC_RELEASE_IMAGE} \ + NEXT_PUBLIC_RELEASE_TAG=${NEXT_PUBLIC_RELEASE_TAG} \ + NEXT_PUBLIC_RELEASE_COMMIT=${NEXT_PUBLIC_RELEASE_COMMIT} \ + NEXT_PUBLIC_RELEASE_VERSION=${NEXT_PUBLIC_RELEASE_VERSION} # --------------------------- # 基础镜像升级到最新 diff --git a/scripts/github-actions/render-frontend-build-args.sh b/scripts/github-actions/render-frontend-build-args.sh index 549caf2..e114df0 100755 --- a/scripts/github-actions/render-frontend-build-args.sh +++ b/scripts/github-actions/render-frontend-build-args.sh @@ -16,6 +16,22 @@ emit_lines() { require_env CANONICAL_DOMAIN local canonical_domain="${CANONICAL_DOMAIN}" + local release_image_ref="${IMAGE_REF-}" + local release_image_tag="" + local release_commit="" + local release_version="" + + if [[ -n "${release_image_ref}" ]]; then + release_image_tag="$(printf '%s' "${release_image_ref}" | sed -E 's#^.*:([^:@]+)$#\1#')" + release_version="${release_image_tag}" + + if [[ "${release_image_tag}" =~ ^sha-([0-9a-f]{7,40})$ ]]; then + release_commit="${BASH_REMATCH[1]}" + elif [[ "${release_image_tag}" =~ ^[0-9a-f]{7,40}$ ]]; then + release_commit="${release_image_tag}" + fi + fi + 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}" @@ -36,6 +52,10 @@ emit_lines() { printf 'NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION=%s\n' "${NEXT_PUBLIC_STRIPE_PRICE_XSCOPEHUB_SUBSCRIPTION-}" printf 'NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO=%s\n' "${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO-}" printf 'NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION=%s\n' "${NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION-}" + printf 'NEXT_PUBLIC_RELEASE_IMAGE=%s\n' "${release_image_ref}" + printf 'NEXT_PUBLIC_RELEASE_TAG=%s\n' "${release_image_tag}" + printf 'NEXT_PUBLIC_RELEASE_COMMIT=%s\n' "${release_commit}" + printf 'NEXT_PUBLIC_RELEASE_VERSION=%s\n' "${release_version}" } if [[ "${MODE}" == "--stdout" ]]; then diff --git a/scripts/github-actions/verify-frontend-release.sh b/scripts/github-actions/verify-frontend-release.sh index 4e70d6a..57f7548 100755 --- a/scripts/github-actions/verify-frontend-release.sh +++ b/scripts/github-actions/verify-frontend-release.sh @@ -5,7 +5,6 @@ CANONICAL_DOMAIN="${1:?usage: verify-frontend-release.sh str: + pattern = rf']+name=["\']{re.escape(name)}["\'][^>]+content=["\']([^"\']*)["\']' + match = re.search(pattern, html, flags=re.IGNORECASE) + return match.group(1).strip() if match else "" + +print(extract_meta("svc-plus-release-image")) +print(extract_meta("svc-plus-release-tag")) +print(extract_meta("svc-plus-release-commit")) +print(extract_meta("svc-plus-release-version")) PY } @@ -79,8 +86,8 @@ verify_domain() { local domain="$1" local request_base_url="${REQUEST_BASE_URL%/}" local request_headers=("${curl_headers[@]}" -H "host: ${domain}") - local homepage_html asset_path release_payload release_metadata - local actual_image_ref actual_image_tag actual_release_commit actual_dashboard_url + local homepage_html asset_path release_metadata + local actual_image_ref actual_image_tag actual_release_commit actual_release_version local release_lines require_http_200 "${request_base_url}" "${request_headers[@]}" @@ -96,49 +103,48 @@ verify_domain() { require_http_200 "${request_base_url}${asset_path}" "${request_headers[@]}" printf 'verified static asset for %s: %s%s\n' "${domain}" "${request_base_url}" "${asset_path}" >&2 - require_http_200 "${request_base_url}/api/ping" "${request_headers[@]}" - release_payload="$(curl -fsSL "${request_headers[@]}" "${request_base_url}/api/ping")" - release_metadata="$(parse_release_metadata "${release_payload}")" + release_metadata="$(parse_homepage_release_metadata "${homepage_html}")" 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]-}" + actual_release_version="${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 + if [[ -z "${actual_image_ref}" || -z "${actual_image_tag}" || -z "${actual_release_commit}" || -z "${actual_release_version}" ]]; then + echo "Homepage release metadata is incomplete for ${domain}" >&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 + if [[ ! "${actual_release_commit}" =~ ^[0-9a-f]{7,40}$ ]]; then + echo "Homepage release commit must contain a commit id for ${domain}, got: ${actual_release_commit}" >&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 + if [[ "${actual_release_version}" != "${actual_image_tag}" ]]; then + echo "Homepage release version mismatch for ${domain}: expected ${actual_image_tag}, got ${actual_release_version}" >&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 + if [[ "${actual_release_commit}" != "${EXPECTED_RELEASE_COMMIT}" ]]; then + echo "Homepage release commit mismatch for ${domain}: expected ${EXPECTED_RELEASE_COMMIT}, got ${actual_release_commit}" >&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 'verified homepage release image for %s: %s\n' "${domain}" "${actual_image_ref}" >&2 + printf 'verified homepage release commit for %s: %s\n' "${domain}" "${actual_release_commit}" >&2 + printf 'verified homepage release version for %s: %s\n' "${domain}" "${actual_release_version}" >&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}" + printf '%s\t%s\t%s\t%s\t%s\n' "${domain}" "${actual_image_ref}" "${actual_image_tag}" "${actual_release_commit}" "${actual_release_version}" } 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]-}" +EXPECTED_RELEASE_VERSION="${expected_release_lines[3]-}" -if [[ -z "${EXPECTED_RELEASE_IMAGE_REF}" || -z "${EXPECTED_RELEASE_IMAGE_TAG}" || -z "${EXPECTED_RELEASE_COMMIT}" ]]; then +if [[ -z "${EXPECTED_RELEASE_IMAGE_REF}" || -z "${EXPECTED_RELEASE_IMAGE_TAG}" || -z "${EXPECTED_RELEASE_COMMIT}" || -z "${EXPECTED_RELEASE_VERSION}" ]]; then echo "Expected image ref is invalid: ${EXPECTED_IMAGE_REF}" >&2 exit 1 fi @@ -175,10 +181,10 @@ fi reference_image_ref="" reference_image_tag="" reference_release_commit="" -reference_dashboard_url="" +reference_release_version="" 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_release_version <<< "${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 @@ -195,11 +201,21 @@ for row in "${verification_rows[@]}"; do exit 1 fi + if [[ "${actual_release_version}" != "${EXPECTED_RELEASE_VERSION}" ]]; then + echo "Release version mismatch for ${domain}: expected ${EXPECTED_RELEASE_VERSION}, got ${actual_release_version}" >&2 + exit 1 + fi + 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}" + reference_release_version="${actual_release_version}" continue fi + + if [[ "${actual_image_ref}" != "${reference_image_ref}" || "${actual_image_tag}" != "${reference_image_tag}" || "${actual_release_commit}" != "${reference_release_commit}" || "${actual_release_version}" != "${reference_release_version}" ]]; then + echo "Release metadata drift detected across served domains." >&2 + exit 1 + fi done diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 96da0b7..41e5116 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,6 +7,7 @@ import type { Metadata } from 'next' import Script from 'next/script' import { Analytics } from '@vercel/analytics/react' import { AppProviders } from './AppProviders' +import { resolveWebReleaseMetadata } from '@/lib/webReleaseMetadata' import { getConsoleIntegrationDefaults } from '@/server/consoleIntegrations' const DEFAULT_TITLE = 'Cloud-Neutral Console | Unified Cloud Native Tools' @@ -76,12 +77,17 @@ const GA_ID = 'G-T4VM8G4Q42' export default function RootLayout({ children }: { children: React.ReactNode }) { const assistantDefaults = getConsoleIntegrationDefaults() + const releaseMetadata = resolveWebReleaseMetadata() return ( + {releaseMetadata.image ? : null} + {releaseMetadata.tag ? : null} + {releaseMetadata.commit ? : null} + {releaseMetadata.version ? : null} { + it("returns trimmed public release metadata", () => { + expect( + resolveWebReleaseMetadata({ + NEXT_PUBLIC_RELEASE_IMAGE: " ghcr.io/x-evor/console:abc123 ", + NEXT_PUBLIC_RELEASE_TAG: " abc123 ", + NEXT_PUBLIC_RELEASE_COMMIT: " abc123 ", + NEXT_PUBLIC_RELEASE_VERSION: " sha-abc123 ", + }), + ).toEqual({ + image: "ghcr.io/x-evor/console:abc123", + tag: "abc123", + commit: "abc123", + version: "sha-abc123", + }); + }); + + it("normalizes empty public release metadata to null", () => { + expect( + resolveWebReleaseMetadata({ + NEXT_PUBLIC_RELEASE_IMAGE: " ", + NEXT_PUBLIC_RELEASE_TAG: "", + NEXT_PUBLIC_RELEASE_COMMIT: undefined, + NEXT_PUBLIC_RELEASE_VERSION: " ", + }), + ).toEqual({ + image: null, + tag: null, + commit: null, + version: null, + }); + }); +}); diff --git a/src/lib/webReleaseMetadata.ts b/src/lib/webReleaseMetadata.ts new file mode 100644 index 0000000..155ecde --- /dev/null +++ b/src/lib/webReleaseMetadata.ts @@ -0,0 +1,22 @@ +export type TWebReleaseMetadata = { + image: string | null + tag: string | null + commit: string | null + version: string | null +} + +type TReleaseMetadataEnv = Record + +function normalizeReleaseValue(value: string | undefined): string | null { + const normalizedValue = value?.trim() + return normalizedValue ? normalizedValue : null +} + +export function resolveWebReleaseMetadata(env: TReleaseMetadataEnv = process.env): TWebReleaseMetadata { + return { + image: normalizeReleaseValue(env.NEXT_PUBLIC_RELEASE_IMAGE), + tag: normalizeReleaseValue(env.NEXT_PUBLIC_RELEASE_TAG), + commit: normalizeReleaseValue(env.NEXT_PUBLIC_RELEASE_COMMIT), + version: normalizeReleaseValue(env.NEXT_PUBLIC_RELEASE_VERSION), + } +}