diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml new file mode 100644 index 00000000..1eb3186e --- /dev/null +++ b/.github/workflows/publish-images.yml @@ -0,0 +1,83 @@ +name: Publish Images + +on: + push: + branches: + - dev + - release + - release/** + - main + +permissions: + contents: read + +concurrency: + group: publish-images-${{ github.ref }} + cancel-in-progress: true + +jobs: + publish: + runs-on: ubuntu-latest + env: + REMOTE_BASE_DIR: /opt/ctms-build + SSH_KEY_PATH: /tmp/tcloud_ci_key + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Prepare remote path metadata + id: meta + shell: bash + run: | + echo "repo_name=${GITHUB_REPOSITORY#*/}" >> "$GITHUB_OUTPUT" + echo "branch_name=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + echo "short_sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" + + - name: Install SSH key + shell: bash + run: | + printf '%s\n' '${{ secrets.TCLOUD_SSH_KEY }}' > "${{ env.SSH_KEY_PATH }}" + chmod 600 "${{ env.SSH_KEY_PATH }}" + + - name: Sync repository to Tencent Cloud host + shell: bash + env: + TCLOUD_HOST: ${{ secrets.TCLOUD_HOST }} + TCLOUD_PORT: ${{ secrets.TCLOUD_PORT }} + TCLOUD_USER: ${{ secrets.TCLOUD_USER }} + REMOTE_REPO_DIR: ${{ env.REMOTE_BASE_DIR }}/${{ steps.meta.outputs.repo_name }} + run: | + ssh -i "${{ env.SSH_KEY_PATH }}" -p "${TCLOUD_PORT}" -o StrictHostKeyChecking=no "${TCLOUD_USER}@${TCLOUD_HOST}" \ + "mkdir -p '${REMOTE_REPO_DIR}' && find '${REMOTE_REPO_DIR}' -mindepth 1 -maxdepth 1 -exec rm -rf {} +" + tar --exclude=.git -cf - . | \ + ssh -i "${{ env.SSH_KEY_PATH }}" -p "${TCLOUD_PORT}" -o StrictHostKeyChecking=no "${TCLOUD_USER}@${TCLOUD_HOST}" \ + "tar -xf - -C '${REMOTE_REPO_DIR}'" + + - name: Build and push images on Tencent Cloud host + shell: bash + env: + TCLOUD_HOST: ${{ secrets.TCLOUD_HOST }} + TCLOUD_PORT: ${{ secrets.TCLOUD_PORT }} + TCLOUD_USER: ${{ secrets.TCLOUD_USER }} + BRANCH_NAME: ${{ steps.meta.outputs.branch_name }} + COMMIT_SHA: ${{ github.sha }} + REGISTRY_HOST: ${{ secrets.REGISTRY_HOST }} + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + REMOTE_REPO_DIR: ${{ env.REMOTE_BASE_DIR }}/${{ steps.meta.outputs.repo_name }} + run: | + BRANCH_NAME_B64="$(printf '%s' "${BRANCH_NAME}" | base64)" + COMMIT_SHA_B64="$(printf '%s' "${COMMIT_SHA}" | base64)" + REGISTRY_HOST_B64="$(printf '%s' "${REGISTRY_HOST}" | base64)" + REGISTRY_USERNAME_B64="$(printf '%s' "${REGISTRY_USERNAME}" | base64)" + REGISTRY_PASSWORD_B64="$(printf '%s' "${REGISTRY_PASSWORD}" | base64)" + ssh -i "${{ env.SSH_KEY_PATH }}" -p "${TCLOUD_PORT}" -o StrictHostKeyChecking=no "${TCLOUD_USER}@${TCLOUD_HOST}" " + cd '${REMOTE_REPO_DIR}' + chmod +x scripts/build-and-push-registry.sh + export BRANCH_NAME=\$(printf '%s' '${BRANCH_NAME_B64}' | base64 -d) + export COMMIT_SHA=\$(printf '%s' '${COMMIT_SHA_B64}' | base64 -d) + export REGISTRY_HOST=\$(printf '%s' '${REGISTRY_HOST_B64}' | base64 -d) + export REGISTRY_USERNAME=\$(printf '%s' '${REGISTRY_USERNAME_B64}' | base64 -d) + export REGISTRY_PASSWORD=\$(printf '%s' '${REGISTRY_PASSWORD_B64}' | base64 -d) + ./scripts/build-and-push-registry.sh + " diff --git a/README.md b/README.md index fe785080..b8ecf344 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,14 @@ - `curl -i http://127.0.0.1/` - `curl -i http://127.0.0.1/health` +## 腾讯云私有镜像仓库 +- GitHub Actions 会在推送到 `dev`、`release`、`release/*` 和 `main` 时,通过 SSH 连接腾讯云服务器,在服务器本机完成 `backend` 和 `nginx` 镜像的构建与推送。 +- 私有仓库地址默认使用 `${REGISTRY_HOST}`,仓库命名为 `${REGISTRY_HOST}/ctms/ctms-backend` 和 `${REGISTRY_HOST}/ctms/ctms-nginx`。 +- `dev` 分支发布 `dev-latest` 和 `dev-`,`release` 分支发布 `rc-release` 和 `rc-`,`release/*` 分支发布 `rc-` 和 `rc-`,`main` 分支发布 `latest` 和 `sha-`。 +- 部署机需要先 `docker login :5000`,并使用私有仓库的 `htpasswd` 账号密码完成认证。 +- `backend-init` 复用 `BACKEND_IMAGE`,所以生产机可在设置 `REGISTRY_HOST`,或直接指定 `BACKEND_IMAGE` 和 `NGINX_IMAGE` 后执行 `docker compose pull && docker compose run --rm backend-init && docker compose up -d`。 +- 本地开发仍可继续使用 `docker compose up -d --build` 走本地构建。 + ## 账号与注册 - 初始化管理员:`admin@huapont.cn / admin123`(通过生产初始化命令显式创建) - 自助注册:前端 `/register` 提交邮箱、密码、姓名、角色(CRA/PV/IMP/PM)、部门,状态为 PENDING。 diff --git a/docker-compose.yaml b/docker-compose.yaml index a37303b9..0cc84fcd 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -21,6 +21,7 @@ services: - ctms_net backend: + image: ${BACKEND_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-backend:latest} build: context: ./backend dockerfile: Dockerfile @@ -37,6 +38,7 @@ services: - ctms_net backend-init: + image: ${BACKEND_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-backend:latest} build: context: ./backend dockerfile: Dockerfile @@ -51,6 +53,7 @@ services: - ctms_net nginx: + image: ${NGINX_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-nginx:latest} build: context: . dockerfile: nginx/Dockerfile diff --git a/docs/guides/release-checklist.md b/docs/guides/release-checklist.md index 85b71223..a98a5efa 100644 --- a/docs/guides/release-checklist.md +++ b/docs/guides/release-checklist.md @@ -9,6 +9,14 @@ - [ ] `cd backend && python3 -m compileall app scripts` - [ ] `cd frontend && npm run build` +## 1.1 腾讯云私有镜像校验 +- [ ] `dev` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-nginx` 的 `dev-latest` / `dev-` 标签已出现在私有仓库 +- [ ] `release` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-nginx` 的 `rc-release` / `rc-` 标签已出现在私有仓库 +- [ ] `release/*` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-nginx` 的 `rc-` / `rc-` 标签已出现在私有仓库 +- [ ] `main` 推送后,`:5000/ctms/ctms-backend` 和 `ctms-nginx` 的 `latest` / `sha-` 标签已出现在私有仓库 +- [ ] GitHub Actions 已成功 SSH 登录腾讯云服务器并执行远程构建脚本 +- [ ] 部署机已执行 `docker login :5000`,且使用的是私有仓库 `htpasswd` 账号密码 + ## 2. 接口与权限回归 - [ ] ADMIN 账号可 `GET/PUT/PUBLISH/IMPORT-EXCEL/EXPORT-EXCEL` - [ ] PM 账号可 `GET/PUT/PUBLISH/IMPORT-EXCEL/EXPORT-EXCEL` diff --git a/docs/plans/2026-03-30-tcloud-private-registry-design.md b/docs/plans/2026-03-30-tcloud-private-registry-design.md new file mode 100644 index 00000000..8509bfc1 --- /dev/null +++ b/docs/plans/2026-03-30-tcloud-private-registry-design.md @@ -0,0 +1,82 @@ +# Tencent Cloud Private Registry Design + +**Goal:** Add CI that automatically builds and pushes private `backend` and `nginx` images to a self-hosted Docker Registry running on a Tencent Cloud CVM, with `dev` publishing test tags, `release` and `release/*` publishing release-candidate tags, and `main` publishing formal tags. + +**Decisions** +- Publish only two runtime images: `/ctms/ctms-backend` and `/ctms/ctms-nginx`. +- Keep the private registry on the Tencent Cloud CVM as a self-hosted `registry:2` instance protected by `htpasswd`. +- Trigger automation on pushes to `dev`, `release`, `release/*`, and `main`. +- Use GitHub Actions only as the orchestrator. Actual Docker build and push happen on the Tencent Cloud server over SSH. +- Keep `docker-compose.yaml` compatible with both local `build` workflows and private registry pulls. + +**Approaches Considered** +- Recommended: GitHub Actions connects to the Tencent Cloud server over SSH and runs a server-local build-and-push script. This avoids GitHub-hosted runner limitations around insecure or self-signed private registries and keeps registry access local to the server. +- Alternative: GitHub Actions builds and pushes directly to `:5000`. This is simpler on paper but is fragile when the registry is exposed only as an IP endpoint and may use insecure or non-public TLS. +- Alternative: move first to a domain-backed HTTPS registry and then push directly from GitHub Actions. This is the clean long-term path but adds infrastructure work that is not required for the current goal. + +**Architecture** +- A new GitHub Actions workflow triggers on pushes to `dev`, `release`, `release/*`, and `main`. +- The workflow authenticates to the Tencent Cloud server using SSH credentials stored in GitHub Secrets. +- The workflow syncs the repository contents to a fixed build directory such as `/opt/ctms-build/`. +- A server-local script logs in to the private registry, builds `backend` and `nginx`, applies branch-specific tags, and pushes the images to the registry. + +**Registry Naming** +- Backend image: `/ctms/ctms-backend` +- Nginx image: `/ctms/ctms-nginx` +- `frontend` is intentionally not published because the current runtime topology already builds the frontend assets into the `nginx` image. + +**Tagging Strategy** +- `dev` branch pushes publish: + - `/ctms/ctms-backend:dev-latest` + - `/ctms/ctms-backend:dev-` + - `/ctms/ctms-nginx:dev-latest` + - `/ctms/ctms-nginx:dev-` +- `release` branch pushes publish: + - `/ctms/ctms-backend:rc-release` + - `/ctms/ctms-backend:rc-` + - `/ctms/ctms-nginx:rc-release` + - `/ctms/ctms-nginx:rc-` +- `release/*` branch pushes publish: + - `/ctms/ctms-backend:rc-` + - `/ctms/ctms-backend:rc-` + - `/ctms/ctms-nginx:rc-` + - `/ctms/ctms-nginx:rc-` +- `main` branch pushes publish: + - `/ctms/ctms-backend:latest` + - `/ctms/ctms-backend:sha-` + - `/ctms/ctms-nginx:latest` + - `/ctms/ctms-nginx:sha-` + +**Server Requirements** +- Docker installed on the Tencent Cloud server. +- A private `registry:2` instance listening on `:5000`. +- `htpasswd` authentication configured for the registry. +- SSH access from GitHub Actions to the server. +- A writable build directory such as `/opt/ctms-build`. + +**GitHub Secrets** +- `TCLOUD_HOST` +- `TCLOUD_PORT` +- `TCLOUD_USER` +- `TCLOUD_SSH_KEY` +- `REGISTRY_HOST` +- `REGISTRY_USERNAME` +- `REGISTRY_PASSWORD` + +**Compose Integration** +- `docker-compose.yaml` should define `image:` for `backend`, `backend-init`, and `nginx`, with defaults based on private registry variables such as `REGISTRY_HOST`. +- Existing `build:` blocks stay in place so local `docker compose up -d --build` continues to work. +- Deployment hosts can export `REGISTRY_HOST`, then run `docker login`, `docker compose pull`, `docker compose run --rm backend-init`, and `docker compose up -d`. + +**Operational Notes** +- This design assumes the server-local script runs on the same machine that can log in to the private registry reliably. +- The GitHub workflow should not embed long shell logic inline; the repository should own a script under `scripts/` so the build logic stays versioned and testable. +- Direct verification of actual image publication depends on GitHub Actions reaching the Tencent Cloud server and cannot be proven locally without those secrets and network access. + +**Verification** +- Push to `dev` and confirm both images appear with `dev-latest` and `dev-`. +- Push to `release` and confirm both images appear with `rc-release` and `rc-`. +- Push to `release/*` and confirm both images appear with `rc-` and `rc-`. +- Push to `main` and confirm both images appear with `latest` and `sha-`. +- Run `docker compose config` after compose changes and confirm the private registry defaults render correctly. +- Parse the workflow YAML successfully and inspect the remote build script for the expected tag logic and registry login behavior. diff --git a/docs/plans/2026-03-30-tcloud-private-registry.md b/docs/plans/2026-03-30-tcloud-private-registry.md new file mode 100644 index 00000000..55a063c2 --- /dev/null +++ b/docs/plans/2026-03-30-tcloud-private-registry.md @@ -0,0 +1,146 @@ +# Tencent Cloud Private Registry Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add CI that triggers remote builds on a Tencent Cloud CVM and pushes private `backend` and `nginx` images into a self-hosted Docker Registry protected by `htpasswd`, for `dev`, `release`, `release/*`, and `main`. + +**Architecture:** GitHub Actions runs on pushes to `dev`, `release`, `release/*`, and `main`, connects to the Tencent Cloud server over SSH, syncs the repository into a fixed build directory, and invokes a repository-owned shell script that logs in to the private registry, builds the images locally on the server, tags them according to branch, and pushes them. + +**Tech Stack:** GitHub Actions, SSH, Docker, self-hosted `registry:2`, Docker Compose, FastAPI, Nginx + +--- + +### Task 1: Replace GHCR Workflow with Tencent Registry Publish Workflow + +**Files:** +- Modify: `.github/workflows/publish-images.yml` + +**Step 1: Define push triggers and remote execution secrets** + +Ensure the workflow triggers on pushes to `dev`, `release`, `release/*`, and `main`, and uses repository secrets for SSH host, port, user, SSH key, registry host, registry username, and registry password. + +**Step 2: Sync repository content to the Tencent Cloud server** + +Use an SSH-capable action or shell step to create the remote build directory and copy the current repository contents into `/opt/ctms-build/`. + +**Step 3: Invoke the remote publish script** + +Run the repository-owned remote script over SSH with environment values for: +- branch name +- commit SHA +- registry host +- registry credentials + +**Step 4: Verify workflow structure** + +Run: `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/publish-images.yml")'` +Expected: PASS + +### Task 2: Add Remote Build-And-Push Script + +**Files:** +- Create: `scripts/build-and-push-registry.sh` + +**Step 1: Implement branch-aware tag logic** + +For `dev`, compute: +- `dev-latest` +- `dev-` + +For `release`, compute: +- `rc-release` +- `rc-` + +For `release/*`, compute: +- `rc-` +- `rc-` + +For `main`, compute: +- `latest` +- `sha-` + +Fail fast for unsupported branches. + +**Step 2: Implement registry login and image builds** + +Log in to `${REGISTRY_HOST}` using the supplied username and password, then build: +- `${REGISTRY_HOST}/ctms/ctms-backend` +- `${REGISTRY_HOST}/ctms/ctms-nginx` + +**Step 3: Push both tags for both images** + +Push every computed tag explicitly so the branch and immutable tags are both published. + +**Step 4: Verify script syntax** + +Run: `bash -n scripts/build-and-push-registry.sh` +Expected: PASS + +### Task 3: Point Compose Defaults at the Private Registry + +**Files:** +- Modify: `docker-compose.yaml` + +**Step 1: Replace GHCR defaults with private registry defaults** + +Set: +- `backend.image` to `${BACKEND_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-backend:latest}` +- `backend-init.image` to `${BACKEND_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-backend:latest}` +- `nginx.image` to `${NGINX_IMAGE:-${REGISTRY_HOST:-127.0.0.1:5000}/ctms/ctms-nginx:latest}` + +Keep the current `build:` blocks intact. + +**Step 2: Verify rendered compose** + +Run: `docker compose config` +Expected: PASS with private registry image defaults rendered. + +### Task 4: Update Deployment Documentation + +**Files:** +- Modify: `README.md` +- Modify: `docs/guides/release-checklist.md` + +**Step 1: Document the Tencent registry flow** + +Explain that GitHub Actions triggers remote builds on the Tencent Cloud server and publishes to the private registry at `:5000`. + +**Step 2: Document deployment commands** + +Show that deployment hosts must run: +- `docker login :5000` +- `docker compose pull` +- `docker compose run --rm backend-init` +- `docker compose up -d` + +**Step 3: Update release checklist** + +Add checks for remote publish success on `dev`, `release`, `release/*`, and `main`, and for deployment-host registry authentication. + +### Task 5: End-To-End Local Verification + +**Files:** +- Verify only + +**Step 1: Parse the workflow YAML** + +Run: `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/publish-images.yml")'` +Expected: PASS + +**Step 2: Verify script syntax** + +Run: `bash -n scripts/build-and-push-registry.sh` +Expected: PASS + +**Step 3: Render compose** + +Run: `docker compose config` +Expected: PASS + +**Step 4: Inspect documentation** + +Run: `sed -n '1,120p' README.md` +Expected: PASS with Tencent private registry deployment steps present. + +Run: `sed -n '1,120p' docs/guides/release-checklist.md` +Expected: PASS with private registry release verification present. diff --git a/scripts/build-and-push-registry.sh b/scripts/build-and-push-registry.sh new file mode 100644 index 00000000..b38f945d --- /dev/null +++ b/scripts/build-and-push-registry.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +set -euo pipefail + +required_vars=( + BRANCH_NAME + COMMIT_SHA + REGISTRY_HOST + REGISTRY_USERNAME + REGISTRY_PASSWORD +) + +for var_name in "${required_vars[@]}"; do + if [[ -z "${!var_name:-}" ]]; then + echo "Missing required environment variable: ${var_name}" >&2 + exit 1 + fi +done + +short_sha="${COMMIT_SHA:0:7}" + +case "${BRANCH_NAME}" in + dev) + mutable_tag="dev-latest" + immutable_tag="dev-${short_sha}" + ;; + release) + mutable_tag="rc-release" + immutable_tag="rc-${short_sha}" + ;; + release/*) + release_name="${BRANCH_NAME#release/}" + release_slug="${release_name//\//-}" + mutable_tag="rc-${release_slug}" + immutable_tag="rc-${short_sha}" + ;; + main) + mutable_tag="latest" + immutable_tag="sha-${short_sha}" + ;; + *) + echo "Unsupported branch for publishing: ${BRANCH_NAME}" >&2 + exit 1 + ;; +esac + +backend_image="${REGISTRY_HOST}/ctms/ctms-backend" +nginx_image="${REGISTRY_HOST}/ctms/ctms-nginx" + +echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY_HOST}" --username "${REGISTRY_USERNAME}" --password-stdin + +docker build \ + -t "${backend_image}:${mutable_tag}" \ + -t "${backend_image}:${immutable_tag}" \ + ./backend + +docker build \ + -t "${nginx_image}:${mutable_tag}" \ + -t "${nginx_image}:${immutable_tag}" \ + -f ./nginx/Dockerfile \ + . + +docker push "${backend_image}:${mutable_tag}" +docker push "${backend_image}:${immutable_tag}" +docker push "${nginx_image}:${mutable_tag}" +docker push "${nginx_image}:${immutable_tag}"