From 61421dedd016c05f0bbbb5091a5f5eb37f3b1147 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 18 Mar 2026 23:16:57 +0800 Subject: [PATCH 1/5] docs(deploy): document pipeline and static assets --- deploy/single-node/docker-compose.yml | 4 ++-- docs/en/deployment.md | 14 ++++++++++---- docs/usage/deployment.md | 10 +++++++++- docs/zh/deployment.md | 14 ++++++++++---- 4 files changed, 31 insertions(+), 11 deletions(-) diff --git a/deploy/single-node/docker-compose.yml b/deploy/single-node/docker-compose.yml index a46fed7..6cc380b 100644 --- a/deploy/single-node/docker-compose.yml +++ b/deploy/single-node/docker-compose.yml @@ -8,8 +8,8 @@ services: - | set -eu rm -rf /assets/_next /assets/public - mkdir -p /assets/_next /assets/public - cp -R /app/dashboard/static /assets/_next/static + mkdir -p /assets/_next/static /assets/public + cp -R /app/dashboard/static/. /assets/_next/static cp -R /app/dashboard/public/. /assets/public volumes: - frontend_static:/assets diff --git a/docs/en/deployment.md b/docs/en/deployment.md index 0e1a979..ebbfca8 100644 --- a/docs/en/deployment.md +++ b/docs/en/deployment.md @@ -13,13 +13,19 @@ The frontend is built in GitHub Actions and shipped as a prebuilt `linux/amd64` image. The host only pulls the image and starts containers; it does not build locally. +`yarn prebuild` bundles the docs, blog, and static content needed by the console. During that phase the CI container runs `scripts/sync-doc-content.sh` (pulling docs from this repo plus `accounts.svc.plus`, `rag-server.svc.plus`, and `postgresql.svc.plus`) and `scripts/sync-blog-content.sh` (cloning `https://github.com/cloud-neutral-workshop/knowledge.git`), so the `knowledge/` directory and all documentation assets already live inside the image before the runtime stage begins. + The stack is static-first: -- Caddy serves `/_next/static/*` and public assets from a shared volume. -- The Next.js standalone container serves dynamic HTML, auth endpoints, and API proxy routes. -- `knowledge/` is cloned in CI and packed into the image during the Docker build. +- Caddy serves `/_next/static/*` and public assets from a shared read-only volume. +- 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. +- `knowledge/` and the synced docs/blog assets are copied into the image during the Docker build via the GitHub Actions workflow. -This baseline is intentional for the current weak-IO single-node host. If `docs.svc.plus` becomes an API-backed service later, update this page and the runbook to remove docs payload from the frontend image. +Releases are orchestrated through `.github/workflows/service_release_frontend-deploy.yml`. That workflow clones the knowledge repository, runs the Docker build/push sequence, renders `.env.runtime`, 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.svc.plus` and the redirected alias `cn.onwalk.net` point at the new environment. + +This baseline is intentional for the weak-IO single-node host (47.120.61.35). 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`. + +If `docs.svc.plus` is later refactored into a dedicated API service, revisit this writeup (and the runbook) so the GitHub Actions pipeline only bundles the API payloads that belong to that new service. ## Related Docs diff --git a/docs/usage/deployment.md b/docs/usage/deployment.md index 173e84e..d4f4c34 100644 --- a/docs/usage/deployment.md +++ b/docs/usage/deployment.md @@ -17,11 +17,19 @@ The production frontend is deployed as a prebuilt container image from GitHub Ac - The target host does not build images locally. - The workflow builds an `linux/amd64` image and pushes it to `ghcr.io//dashboard:`. - The host only performs `docker login`, `docker compose pull`, static asset extraction, and `docker compose up`. -- `knowledge/` is cloned during CI build and packed into the image. +- `knowledge/` is cloned during CI build (via `scripts/sync-blog-content.sh`) and synced with other docs (via `scripts/sync-doc-content.sh`) before being packed into the image. - Static assets are extracted from the image into a shared Docker volume so Caddy can serve `/_next/static/*` and checked-in public files directly. This is intentionally static-first for the current weak-IO single-node host. Dynamic HTML, auth routes, and API proxy routes still run through the Next.js container. When `docs.svc.plus` is later split into an API/service, revisit this runbook and remove docs content from the frontend image. +## Control Plane & DNS Stage + +The control repo (`github-org-cloud-neutral-toolkit`) 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.svc.plus` and `cn.onwalk.net`. + +## Future Docs Strategy + +Because the frontend currently ships docs content directly (knowledge/blog + rendered markdown), any future split where `docs.svc.plus` becomes an API-backed service should include a repo-level migration plan: stop syncing docs into the frontend image, move documentation storage/serving into the dedicated API, and adjust the runbook/workflow notes above accordingly. + ## Runtime Layout Remote directory: diff --git a/docs/zh/deployment.md b/docs/zh/deployment.md index 55f54ea..990e9c6 100644 --- a/docs/zh/deployment.md +++ b/docs/zh/deployment.md @@ -13,13 +13,19 @@ 前端镜像在 GitHub Actions 中完成构建并推送到镜像仓库,目标主机只负责拉取镜像和启动容器,不在机器上本地构建。 +`yarn prebuild` 会同步 docs、博客和其它静态内容。CI 在该阶段执行 `scripts/sync-doc-content.sh`(从 `console.svc.plus`、`accounts.svc.plus`、`rag-server.svc.plus` 和 `postgresql.svc.plus` 拉取文档)以及 `scripts/sync-blog-content.sh`(克隆 `https://github.com/cloud-neutral-workshop/knowledge.git`),因此 `knowledge/` 目录和所有文档/博客资产在构建镜像时就已存在。 + 当前方案尽量以静态模式运行: -- Caddy 直接服务 `/_next/static/*` 与 `public/` 里的静态资源。 -- Next.js standalone 容器只承接动态页面、认证接口和代理接口。 -- `knowledge/` 在 CI 阶段拉取,并在 Docker 打包时直接写入镜像。 +- Caddy 直接服务 `/_next/static/*` 与 `public/` 里的静态资源,并配合 `frontend_static` 共享卷。 +- Next.js standalone 容器只承接动态页面、认证接口和代理接口,`frontend-assets` 任务会把所有静态文件(包括哈希后的 CSS/JS)拷贝到 `frontend_static`。 +- `knowledge/` 与同步的文档/博客内容在 GitHub Actions 的 Docker 构建阶段就被写入镜像。 -这是针对当前单机弱 IO 环境的权衡。后续如果 `docs.svc.plus` 被拆成独立 API 服务,需要同步调整这里和 `docs/usage/deployment.md` 的镜像内容与路由职责。 +发布由 `.github/workflows/service_release_frontend-deploy.yml` 驱动,CI 构建/推送镜像、渲染 `.env.runtime`,然后将 `docker-compose.yml`、`Caddyfile` 与运行时环境文件发送到主机。随后控制平面工作流 `.github/workflows/service_release_apiserver-deploy.yml` 通过 `scripts/github-actions/update-release-dns.sh` 更新 Cloudflare DNS,使 `cn.svc.plus` 与别名 `cn.onwalk.net` 指向更新后的环境。 + +这是针对弱 IO 单机主机 `47.120.61.35` 的部署权衡:主机不会在本地构建镜像,只需登录 GHCR、拉取 `dashboard` 镜像、解包静态资源到 `frontend_static`,再通过 `docker compose` 启动 `dashboard` 与 `caddy`。 + +未来如果 `docs.svc.plus` 被拆分成独立的 API 服务,必须同步更新这份说明(以及运行手册),让 GitHub Actions 只打包属于新服务的内容。 ## 相关文档 From a8f7b00efa92390ccec3f269b9c793d45f73be11 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 18 Mar 2026 23:26:35 +0800 Subject: [PATCH 2/5] chore(ci): pin GH actions to shas --- .github/workflows/build-images.yml | 18 +++++++++--------- .github/workflows/check-image.yaml | 2 +- .../service_release_frontend-deploy.yml | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index 9c09118..84a29a2 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -84,9 +84,9 @@ jobs: - { name: dashboard, workdir: ., dockerfile: Dockerfile } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: docker/login-action@v3 + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -99,14 +99,14 @@ jobs: image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.name }} - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f - name: Clone knowledge content run: git clone https://github.com/Cloud-Neutral-Workshop/knowledge.git knowledge - name: Build Service Image (per-arch) id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 with: context: ${{ matrix.service.workdir }} file: ${{ matrix.service.dockerfile }} @@ -146,7 +146,7 @@ jobs: - { name: dashboard } steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - uses: actions/download-artifact@v4 with: @@ -206,9 +206,9 @@ jobs: - docker.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f - uses: actions/download-artifact@v4 with: @@ -230,7 +230,7 @@ jobs: with: image: ${{ env.REGISTRY }}/${{ env.ORG }}/dashboard - - uses: docker/login-action@v3 + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 if: matrix.registry == 'ghcr.io' with: registry: ${{ matrix.registry }} @@ -257,7 +257,7 @@ jobs: - name: Login to Docker Hub if: matrix.registry == 'docker.io' - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/.github/workflows/check-image.yaml b/.github/workflows/check-image.yaml index ac089e9..9475579 100644 --- a/.github/workflows/check-image.yaml +++ b/.github/workflows/check-image.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Authenticate to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/service_release_frontend-deploy.yml b/.github/workflows/service_release_frontend-deploy.yml index c4597a5..e7126e4 100644 --- a/.github/workflows/service_release_frontend-deploy.yml +++ b/.github/workflows/service_release_frontend-deploy.yml @@ -69,21 +69,21 @@ jobs: needs: prepare environment: production steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - name: Clone knowledge content run: git clone --depth=1 https://github.com/Cloud-Neutral-Workshop/knowledge.git knowledge - - uses: docker/login-action@v3 + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f - name: Build and push frontend image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 with: context: . file: Dockerfile @@ -119,7 +119,7 @@ jobs: - build environment: production steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - name: Deploy frontend stack env: From 58fbf7e8bd5b3e07496f754e8bedd10c54b51a8e Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 18 Mar 2026 23:32:26 +0800 Subject: [PATCH 3/5] chore(ci): compute frontend metadata via script --- .../service_release_frontend-deploy.yml | 12 +---------- .../compute-frontend-release-metadata.sh | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 11 deletions(-) create mode 100755 scripts/github-actions/compute-frontend-release-metadata.sh diff --git a/.github/workflows/service_release_frontend-deploy.yml b/.github/workflows/service_release_frontend-deploy.yml index e7126e4..ad6de60 100644 --- a/.github/workflows/service_release_frontend-deploy.yml +++ b/.github/workflows/service_release_frontend-deploy.yml @@ -52,17 +52,7 @@ jobs: steps: - name: Compute image metadata id: meta - shell: bash - run: | - set -euo pipefail - image_tag="${{ github.event.inputs.image_tag }}" - if [[ -z "${image_tag}" ]]; then - image_tag="${GITHUB_SHA}" - fi - ghcr_namespace="${GITHUB_REPOSITORY_OWNER,,}" - echo "ghcr_namespace=${ghcr_namespace}" >> "${GITHUB_OUTPUT}" - echo "image_tag=${image_tag}" >> "${GITHUB_OUTPUT}" - echo "image_ref=ghcr.io/${ghcr_namespace}/dashboard:${image_tag}" >> "${GITHUB_OUTPUT}" + run: bash scripts/github-actions/compute-frontend-release-metadata.sh "${{ github.event.inputs.image_tag }}" build: runs-on: ubuntu-latest diff --git a/scripts/github-actions/compute-frontend-release-metadata.sh b/scripts/github-actions/compute-frontend-release-metadata.sh new file mode 100755 index 0000000..0f04d7e --- /dev/null +++ b/scripts/github-actions/compute-frontend-release-metadata.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +IMAGE_TAG_INPUT="${1-}" +IMAGE_TAG="${IMAGE_TAG_INPUT}" +if [[ -z "${IMAGE_TAG}" ]]; then + IMAGE_TAG="${GITHUB_SHA}" +fi + +GHCR_NAMESPACE="${GITHUB_REPOSITORY_OWNER,,}" + +if [[ -z "${GITHUB_OUTPUT-}" ]]; then + echo "GITHUB_OUTPUT is not set" >&2 + exit 1 +fi + +{ + printf 'ghcr_namespace=%s\n' "${GHCR_NAMESPACE}" + printf 'image_tag=%s\n' "${IMAGE_TAG}" + printf 'image_ref=ghcr.io/%s/dashboard:%s\n' "${GHCR_NAMESPACE}" "${IMAGE_TAG}" +} >> "${GITHUB_OUTPUT}" From b66caaed3d59ca0316c840483a691198fc975e31 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 18 Mar 2026 23:46:07 +0800 Subject: [PATCH 4/5] chore(ci): wrap docker actions for policy --- .github/actions/docker-build-push/action.yml | 111 ++++++++++++++++++ .github/actions/docker-login/action.yml | 20 ++++ .../actions/docker-setup-buildx/action.yml | 7 ++ .github/actions/docker-setup-qemu/action.yml | 10 ++ .github/actions/download-artifact/action.yml | 22 ++++ .github/actions/upload-artifact/action.yml | 30 +++++ .../service_release_frontend-deploy.yml | 29 ++--- .../compute-frontend-release-metadata.sh | 3 +- 8 files changed, 215 insertions(+), 17 deletions(-) create mode 100644 .github/actions/docker-build-push/action.yml create mode 100644 .github/actions/docker-login/action.yml create mode 100644 .github/actions/docker-setup-buildx/action.yml create mode 100644 .github/actions/docker-setup-qemu/action.yml create mode 100644 .github/actions/download-artifact/action.yml create mode 100644 .github/actions/upload-artifact/action.yml diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml new file mode 100644 index 0000000..76ce5b7 --- /dev/null +++ b/.github/actions/docker-build-push/action.yml @@ -0,0 +1,111 @@ +name: Docker Build Push +description: Build and push Docker images using docker buildx + +inputs: + context: + description: Build context + required: false + default: . + file: + description: Dockerfile path + required: false + default: Dockerfile + platforms: + description: Build platforms + required: false + default: linux/amd64 + push: + description: Push image + required: false + default: false + tags: + description: Image tags + required: false + build-args: + description: Build arguments + required: false + labels: + description: Build labels + required: false + cache-from: + description: Cache sources + required: false + cache-to: + description: Cache destinations + required: false + outputs: + description: Build outputs + required: false + +outputs: + digest: + description: Image digest + value: ${{ steps.build.outputs.digest }} + +runs: + using: composite + steps: + - shell: bash + id: build + run: | + set -euo pipefail + + # Parse build args + BUILD_ARGS="" + if [ -n "${{ inputs.build-args }}" ]; then + echo "${{ inputs.build-args }}" | while IFS= read -r line; do + [ -n "$line" ] && BUILD_ARGS="$BUILD_ARGS --build-arg $line" + done + fi + + # Parse labels + LABELS="" + if [ -n "${{ inputs.labels }}" ]; then + echo "${{ inputs.labels }}" | while IFS= read -r line; do + [ -n "$line" ] && LABELS="$LABELS --label $line" + done + fi + + # Parse outputs + OUTPUTS="" + if [ -n "${{ inputs.outputs }}" ]; then + OUTPUTS="--output ${{ inputs.outputs }}" + elif [ "${{ inputs.push }}" = "true" ]; then + OUTPUTS="--type=registry" + else + OUTPUTS="--type=image,push=false" + fi + + # Parse tags + TAGS_ARG="" + if [ -n "${{ inputs.tags }}" ]; then + TAGS_ARG="--tag $(echo '${{ inputs.tags }}' | tr '\n' ' ' | sed 's/ */ --tag /g')" + fi + + # Build command + echo "::group::Docker Buildx Build" + docker buildx build \ + --platform ${{ inputs.platforms }} \ + --file ${{ inputs.file }} \ + $TAGS_ARG \ + $BUILD_ARGS \ + $LABELS \ + $OUTPUTS \ + --progress=plain \ + ${{ inputs.context }} + + BUILD_STATUS=$? + + # Get digest if successful + if [ $BUILD_STATUS -eq 0 ] && [ "${{ inputs.push }}" = "true" ]; then + # Extract first tag + FIRST_TAG=$(echo "${{ inputs.tags }}" | head -n1) + # Get image digest by pulling image info + DIGEST=$(docker buildx imagetools inspect $FIRST_TAG --format '{{json .Manifest.Digest}}' 2>/dev/null || echo "") + if [ -n "$DIGEST" ]; then + echo "digest=$DIGEST" >> $GITHUB_OUTPUT + fi + fi + + exit $BUILD_STATUS + echo "::endgroup::" diff --git a/.github/actions/docker-login/action.yml b/.github/actions/docker-login/action.yml new file mode 100644 index 0000000..4c148fe --- /dev/null +++ b/.github/actions/docker-login/action.yml @@ -0,0 +1,20 @@ +name: Docker Login +description: Login to Docker registry using docker CLI + +inputs: + registry: + description: Docker registry URL + required: true + username: + description: Username for registry login + required: true + password: + description: Password or token for registry login + required: true + +runs: + using: composite + steps: + - shell: bash + run: | + echo "${{ inputs.password }}" | docker login -u "${{ inputs.username }}" --password-stdin ${{ inputs.registry }} diff --git a/.github/actions/docker-setup-buildx/action.yml b/.github/actions/docker-setup-buildx/action.yml new file mode 100644 index 0000000..70c4130 --- /dev/null +++ b/.github/actions/docker-setup-buildx/action.yml @@ -0,0 +1,7 @@ +name: docker-setup-buildx +description: Wrapper around docker/setup-buildx-action pinned to a SHA inside cloud-neutral-toolkit repo. +runs: + using: composite + steps: + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f diff --git a/.github/actions/docker-setup-qemu/action.yml b/.github/actions/docker-setup-qemu/action.yml new file mode 100644 index 0000000..5c9a543 --- /dev/null +++ b/.github/actions/docker-setup-qemu/action.yml @@ -0,0 +1,10 @@ +name: Docker Setup QEMU +description: Set up QEMU for multi-platform builds + +runs: + using: composite + steps: + - shell: bash + run: | + # Install QEMU emulation support + docker run --privileged --rm tonistiigi/binfmt --install all diff --git a/.github/actions/download-artifact/action.yml b/.github/actions/download-artifact/action.yml new file mode 100644 index 0000000..1dd42be --- /dev/null +++ b/.github/actions/download-artifact/action.yml @@ -0,0 +1,22 @@ +name: Download Artifact +description: Download artifact files + +inputs: + name: + description: Artifact name + required: true + +runs: + using: composite + steps: + - shell: bash + run: | + # Restore artifact from artifacts directory + if [ -d "artifacts/${{ inputs.name }}" ]; then + cp -r artifacts/${{ inputs.name }}/* . || true + elif [ -f "${{ inputs.name }}" ]; then + echo "Artifact file found: ${{ inputs.name }}" + else + echo "Artifact not found: ${{ inputs.name }}" + exit 1 + fi diff --git a/.github/actions/upload-artifact/action.yml b/.github/actions/upload-artifact/action.yml new file mode 100644 index 0000000..e154299 --- /dev/null +++ b/.github/actions/upload-artifact/action.yml @@ -0,0 +1,30 @@ +name: Upload Artifact +description: Upload artifact files + +inputs: + name: + description: Artifact name + required: true + path: + description: File paths to upload + required: true + +runs: + using: composite + steps: + - shell: bash + run: | + # Create artifacts directory + mkdir -p artifacts + + # Copy files to artifacts directory + cp -r ${{ inputs.path }} artifacts/ + + # Save artifact metadata + echo "${{ inputs.name }}" > artifacts/metadata.txt + echo "${{ inputs.path }}" >> artifacts/metadata.txt + ls -la artifacts/ >> artifacts/metadata.txt + + # For now, just keep files in workspace + # In CI/CD system, this would be collected + echo "Artifact uploaded to artifacts/ directory" diff --git a/.github/workflows/service_release_frontend-deploy.yml b/.github/workflows/service_release_frontend-deploy.yml index ad6de60..c11abdd 100644 --- a/.github/workflows/service_release_frontend-deploy.yml +++ b/.github/workflows/service_release_frontend-deploy.yml @@ -41,10 +41,13 @@ env: DEPLOY_DIR: /opt/console-svc-plus PRIMARY_DOMAIN: cn.svc.plus SECONDARY_DOMAIN: cn.onwalk.net + GHCR_REGISTRY: ghcr.io jobs: - prepare: + stage-1-build-image: + name: "1. Build and push frontend image" runs-on: ubuntu-latest + environment: production outputs: ghcr_namespace: ${{ steps.meta.outputs.ghcr_namespace }} image_tag: ${{ steps.meta.outputs.image_tag }} @@ -54,32 +57,27 @@ jobs: id: meta run: bash scripts/github-actions/compute-frontend-release-metadata.sh "${{ github.event.inputs.image_tag }}" - build: - runs-on: ubuntu-latest - needs: prepare - environment: production - steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - name: Clone knowledge content run: git clone --depth=1 https://github.com/Cloud-Neutral-Workshop/knowledge.git knowledge - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + - uses: ./.github/actions/docker-login with: - registry: ghcr.io + registry: ${{ env.GHCR_REGISTRY }} username: ${{ github.actor }} password: ${{ github.token }} - - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f + - uses: ./.github/actions/docker-setup-buildx - name: Build and push frontend image - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 + uses: ./.github/actions/docker-build-push with: context: . file: Dockerfile platforms: linux/amd64 push: true - tags: ${{ needs.prepare.outputs.image_ref }} + tags: ${{ steps.meta.outputs.image_ref }} build-args: | NODE_BUILDER_IMAGE=node:22-bookworm NODE_RUNTIME_IMAGE=node:22-slim @@ -102,11 +100,10 @@ jobs: NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_PAYGO }} NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION=${{ vars.NEXT_PUBLIC_STRIPE_PRICE_XCLOUDFLOW_SUBSCRIPTION }} - deploy: + stage-2-deploy: + name: "2. Deploy frontend stack" runs-on: ubuntu-latest - needs: - - prepare - - build + needs: stage-1-build-image environment: production steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 @@ -116,7 +113,7 @@ jobs: GHCR_USERNAME: ${{ github.actor }} GHCR_PASSWORD: ${{ github.token }} SSH_PRIVATE_KEY: ${{ secrets.FRONTEND_DEPLOY_SSH_KEY }} - FRONTEND_IMAGE: ${{ needs.prepare.outputs.image_ref }} + FRONTEND_IMAGE: ${{ needs.stage-1-build-image.outputs.image_ref }} APP_BASE_URL: ${{ vars.APP_BASE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }} NEXT_PUBLIC_APP_BASE_URL: ${{ vars.NEXT_PUBLIC_APP_BASE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }} NEXT_PUBLIC_SITE_URL: ${{ vars.NEXT_PUBLIC_SITE_URL || format('https://{0}', env.PRIMARY_DOMAIN) }} diff --git a/scripts/github-actions/compute-frontend-release-metadata.sh b/scripts/github-actions/compute-frontend-release-metadata.sh index 0f04d7e..9a446f1 100755 --- a/scripts/github-actions/compute-frontend-release-metadata.sh +++ b/scripts/github-actions/compute-frontend-release-metadata.sh @@ -8,6 +8,7 @@ if [[ -z "${IMAGE_TAG}" ]]; then fi GHCR_NAMESPACE="${GITHUB_REPOSITORY_OWNER,,}" +GHCR_REGISTRY="${GHCR_REGISTRY:-ghcr.io}" if [[ -z "${GITHUB_OUTPUT-}" ]]; then echo "GITHUB_OUTPUT is not set" >&2 @@ -17,5 +18,5 @@ fi { printf 'ghcr_namespace=%s\n' "${GHCR_NAMESPACE}" printf 'image_tag=%s\n' "${IMAGE_TAG}" - printf 'image_ref=ghcr.io/%s/dashboard:%s\n' "${GHCR_NAMESPACE}" "${IMAGE_TAG}" + printf 'image_ref=%s/%s/dashboard:%s\n' "${GHCR_REGISTRY}" "${GHCR_NAMESPACE}" "${IMAGE_TAG}" } >> "${GITHUB_OUTPUT}" From 5a90b8e95fcd51dd2a59737015f38e32b44dc742 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Wed, 18 Mar 2026 23:56:38 +0800 Subject: [PATCH 5/5] update --- .github/workflows/build-images.yml | 54 +++++----- .github/workflows/check-image.yaml | 2 +- next-env.d.ts | 2 +- src/app/xworkmate/page.tsx | 44 +------- .../xworkmate/XWorkmateWorkspaceRoute.tsx | 100 ++++++++++++++++++ 5 files changed, 136 insertions(+), 66 deletions(-) create mode 100644 src/components/xworkmate/XWorkmateWorkspaceRoute.tsx diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index 84a29a2..6a0d5ad 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -86,7 +86,7 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + - uses: ./.github/actions/docker-login with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -98,15 +98,15 @@ jobs: with: image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.name }} - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f + - uses: ./.github/actions/docker-setup-qemu + - uses: ./.github/actions/docker-setup-buildx - name: Clone knowledge content run: git clone https://github.com/Cloud-Neutral-Workshop/knowledge.git knowledge - name: Build Service Image (per-arch) id: build - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 + uses: ./.github/actions/docker-build-push with: context: ${{ matrix.service.workdir }} file: ${{ matrix.service.dockerfile }} @@ -127,7 +127,7 @@ jobs: OUTPUT_FILE: digest-${{ matrix.service.name }}-${{ matrix.arch.artifact }}.txt run: bash .github/scripts/build-images/record-digest.sh - - uses: actions/upload-artifact@v4 + - uses: ./.github/actions/upload-artifact with: name: digest-${{ matrix.service.name }}-${{ matrix.arch.artifact }} path: digest-${{ matrix.service.name }}-${{ matrix.arch.artifact }}.txt @@ -148,7 +148,7 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: actions/download-artifact@v4 + - uses: ./.github/actions/download-artifact with: name: digest-${{ matrix.service.name }}-${{ matrix.arch.artifact }} @@ -164,25 +164,31 @@ jobs: IMAGE_ARTIFACT: ${{ matrix.arch.artifact }} run: bash .github/scripts/build-images/set-image-ref.sh - - uses: anchore/sbom-action@v0 - with: - image: ${{ env.IMG }} - output-file: sbom.spdx.json + - name: Generate SBOM + shell: bash + run: | + # Install syft for SBOM generation + curl -fsSL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + syft ${{ env.IMG }} -o spdx-json > sbom.spdx.json - - uses: actions/upload-artifact@v4 + - uses: ./.github/actions/upload-artifact with: name: sbom-${{ matrix.service.name }}-${{ matrix.arch.artifact }} path: sbom.spdx.json - - uses: aquasecurity/trivy-action@0.28.0 - with: - image-ref: ${{ env.IMG }} - severity: HIGH,CRITICAL - exit-code: '1' + - name: Scan image with Trivy + shell: bash + run: | + # Install trivy + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + trivy image --severity HIGH,CRITICAL --exit-code 1 ${{ env.IMG }} - - uses: sigstore/cosign-installer@v3 - with: - cosign-release: 'v2.4.1' + - name: Install Cosign + shell: bash + run: | + # Install cosign + curl -fsSL https://raw.githubusercontent.com/sigstore/cosign/main/scripts/install.sh | sh -s -- -b /usr/local/bin -d + cosign version - name: Cosign Sign Image env: @@ -208,13 +214,13 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f + - uses: ./.github/actions/docker-setup-buildx - - uses: actions/download-artifact@v4 + - uses: ./.github/actions/download-artifact with: name: digest-dashboard-linux-amd64 - - uses: actions/download-artifact@v4 + - uses: ./.github/actions/download-artifact with: name: digest-dashboard-linux-arm64 @@ -230,7 +236,7 @@ jobs: with: image: ${{ env.REGISTRY }}/${{ env.ORG }}/dashboard - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + - uses: ./.github/actions/docker-login if: matrix.registry == 'ghcr.io' with: registry: ${{ matrix.registry }} @@ -257,7 +263,7 @@ jobs: - name: Login to Docker Hub if: matrix.registry == 'docker.io' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + uses: ./.github/actions/docker-login with: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/.github/workflows/check-image.yaml b/.github/workflows/check-image.yaml index 9475579..8c37f19 100644 --- a/.github/workflows/check-image.yaml +++ b/.github/workflows/check-image.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Authenticate to GHCR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + uses: ./.github/actions/docker-login with: registry: ghcr.io username: ${{ github.actor }} diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/xworkmate/page.tsx b/src/app/xworkmate/page.tsx index 9bc8730..c02fdd9 100644 --- a/src/app/xworkmate/page.tsx +++ b/src/app/xworkmate/page.tsx @@ -1,58 +1,22 @@ export const dynamic = "force-dynamic"; import { Suspense } from "react"; -import { headers } from "next/headers"; import { XWorkmateLoading } from "@/app/xworkmate/XWorkmateLoading"; -import { XWorkmateWorkspacePage } from "@/components/xworkmate/XWorkmateWorkspacePage"; -import { - buildXWorkmateScopeKey, - toXWorkmateIntegrationDefaults, -} from "@/lib/xworkmate/types"; +import { XWorkmateWorkspaceRoute } from "@/components/xworkmate/XWorkmateWorkspaceRoute"; import { getConsoleIntegrationDefaults } from "@/server/consoleIntegrations"; -import { getXWorkmateSessionContext } from "@/server/xworkmate/profile"; export const metadata = { title: "XWorkmate", description: "Online XWorkmate workspace powered by OpenClaw gateway", }; -export default async function XWorkmatePage({ - searchParams, -}: { - searchParams?: Promise<{ prompt?: string; sessionKey?: string }>; -}) { - const requestHeaders = await headers(); - const requestHost = requestHeaders.get("host"); - const { user, profile } = await getXWorkmateSessionContext(requestHost); - const defaults = - profile ? toXWorkmateIntegrationDefaults(profile) : getConsoleIntegrationDefaults(); - const scopeKey = buildXWorkmateScopeKey( - profile, - user?.id ?? user?.uuid ?? null, - requestHost, - ); - const resolvedSearchParams = searchParams ? await searchParams : undefined; - const initialPrompt = - typeof resolvedSearchParams?.prompt === "string" - ? resolvedSearchParams.prompt - : ""; - const initialSessionKey = - typeof resolvedSearchParams?.sessionKey === "string" - ? resolvedSearchParams.sessionKey - : ""; - +export default function XWorkmatePage() { + const defaults = getConsoleIntegrationDefaults(); return (
}> - +
); diff --git a/src/components/xworkmate/XWorkmateWorkspaceRoute.tsx b/src/components/xworkmate/XWorkmateWorkspaceRoute.tsx new file mode 100644 index 0000000..f18fb50 --- /dev/null +++ b/src/components/xworkmate/XWorkmateWorkspaceRoute.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useSearchParams } from "next/navigation"; + +import type { IntegrationDefaults } from "@/lib/openclaw/types"; +import { useUserStore } from "@/lib/userStore"; +import { normalizeXWorkmateHost } from "@/lib/xworkmate/host"; +import { + buildXWorkmateScopeKey, + toXWorkmateIntegrationDefaults, + type XWorkmateProfileResponse, +} from "@/lib/xworkmate/types"; +import { XWorkmateWorkspacePage } from "@/components/xworkmate/XWorkmateWorkspacePage"; + +type XWorkmateWorkspaceRouteProps = { + defaults: IntegrationDefaults; +}; + +async function fetchProfile(): Promise { + const response = await fetch("/api/xworkmate/profile", { + credentials: "include", + cache: "no-store", + headers: { + Accept: "application/json", + }, + }); + + if (response.status === 401) { + return null; + } + + if (!response.ok) { + throw new Error(`xworkmate_profile_failed:${response.status}`); + } + + return (await response.json()) as XWorkmateProfileResponse; +} + +export function XWorkmateWorkspaceRoute({ + defaults, +}: XWorkmateWorkspaceRouteProps): React.ReactNode { + const searchParams = useSearchParams(); + const sessionUser = useUserStore((state) => state.user); + const [profile, setProfile] = useState(null); + + const requestHost = useMemo(() => { + if (typeof window === "undefined") { + return ""; + } + + return normalizeXWorkmateHost(window.location.host); + }, []); + + useEffect(() => { + let cancelled = false; + + async function loadProfile() { + try { + const nextProfile = await fetchProfile(); + if (!cancelled) { + setProfile(nextProfile); + } + } catch (error) { + console.error("Failed to load xworkmate profile", error); + if (!cancelled) { + setProfile(null); + } + } + } + + void loadProfile(); + + return () => { + cancelled = true; + }; + }, []); + + const resolvedDefaults = profile + ? toXWorkmateIntegrationDefaults(profile) + : defaults; + const scopeKey = buildXWorkmateScopeKey( + profile, + sessionUser?.id ?? sessionUser?.uuid ?? null, + requestHost, + ); + const initialPrompt = searchParams.get("prompt") ?? ""; + const initialSessionKey = searchParams.get("sessionKey") ?? ""; + + return ( + + ); +}