diff --git a/.gitea/workflows/docker-image.yml b/.gitea/workflows/docker-image.yml new file mode 100644 index 00000000..34ed1555 --- /dev/null +++ b/.gitea/workflows/docker-image.yml @@ -0,0 +1,133 @@ +name: Docker Image + +on: + push: + branches: + - main + - test + workflow_dispatch: + +jobs: + publish: + name: Build and Push Docker Image + runs-on: gitea-label + env: + NEXT_TELEMETRY_DISABLED: "1" + SENTRY_UPLOAD_SOURCEMAPS: "0" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Verify Docker + run: | + docker version + docker info + + - name: Prepare image metadata + env: + REGISTRY_IMAGE: ${{ secrets.REGISTRY_IMAGE }} + run: | + set -eu + + BRANCH_NAME="${GITHUB_REF_NAME:-}" + if [ -z "$BRANCH_NAME" ] && [ -n "${GITHUB_REF:-}" ]; then + BRANCH_NAME="${GITHUB_REF#refs/heads/}" + fi + if [ -z "$BRANCH_NAME" ]; then + BRANCH_NAME="$(git branch --show-current)" + fi + + SHORT_SHA="$(git rev-parse --short HEAD)" + case "$BRANCH_NAME" in + main) + DEPLOY_ENV="prod" + NEXT_ENV_FILE=".env.production" + ;; + test) + DEPLOY_ENV="test" + NEXT_ENV_FILE=".env.local" + ;; + *) + echo "Unsupported branch for image publishing: $BRANCH_NAME" >&2 + exit 1 + ;; + esac + + if [ -z "$REGISTRY_IMAGE" ]; then + echo "Missing secret: REGISTRY_IMAGE" >&2 + exit 1 + fi + + { + echo "BRANCH_NAME=$BRANCH_NAME" + echo "DEPLOY_ENV=$DEPLOY_ENV" + echo "NEXT_ENV_FILE=$NEXT_ENV_FILE" + echo "SHORT_SHA=$SHORT_SHA" + echo "IMAGE=$REGISTRY_IMAGE" + echo "IMAGE_VERSION_TAG=$REGISTRY_IMAGE:$DEPLOY_ENV-$SHORT_SHA" + echo "IMAGE_LATEST_TAG=$REGISTRY_IMAGE:$DEPLOY_ENV-latest" + } >> "$GITHUB_ENV" + + - name: Prepare build env file + env: + TEST_ENV_FILE_CONTENT: ${{ secrets.TEST_ENV_FILE }} + PRODUCTION_ENV_FILE_CONTENT: ${{ secrets.PRODUCTION_ENV_FILE }} + run: | + set -eu + + case "$DEPLOY_ENV" in + prod) + if [ -z "$PRODUCTION_ENV_FILE_CONTENT" ]; then + echo "Missing secret: PRODUCTION_ENV_FILE" >&2 + exit 1 + fi + printf '%s\n' "$PRODUCTION_ENV_FILE_CONTENT" > "$NEXT_ENV_FILE" + ;; + test) + if [ -z "$TEST_ENV_FILE_CONTENT" ]; then + echo "Missing secret: TEST_ENV_FILE" >&2 + exit 1 + fi + printf '%s\n' "$TEST_ENV_FILE_CONTENT" > "$NEXT_ENV_FILE" + ;; + esac + + - name: Login Docker registry + env: + REGISTRY_HOST: ${{ secrets.REGISTRY_HOST }} + REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + run: | + set -eu + + if [ -z "$REGISTRY_HOST" ] || [ -z "$REGISTRY_USERNAME" ] || [ -z "$REGISTRY_PASSWORD" ]; then + echo "Missing registry login secrets" >&2 + exit 1 + fi + + printf '%s' "$REGISTRY_PASSWORD" \ + | docker login "$REGISTRY_HOST" -u "$REGISTRY_USERNAME" --password-stdin + + - name: Build Docker image + run: | + set -eu + + DOCKER_BUILDKIT=1 docker build \ + --secret id=next_env,src="$NEXT_ENV_FILE" \ + --build-arg NEXT_ENV_FILE="$NEXT_ENV_FILE" \ + -t "$IMAGE_VERSION_TAG" \ + -t "$IMAGE_LATEST_TAG" \ + . + + - name: Push Docker image + run: | + set -eu + + docker push "$IMAGE_VERSION_TAG" + docker push "$IMAGE_LATEST_TAG" + + - name: Cleanup build env file + if: always() + run: | + rm -f .env.local .env.production diff --git a/docs/docker-image-ci.md b/docs/docker-image-ci.md new file mode 100644 index 00000000..2fcc1c96 --- /dev/null +++ b/docs/docker-image-ci.md @@ -0,0 +1,81 @@ +# Docker 镜像持续构建与发布 + +本文档说明 Gitea Actions 中 Docker 镜像构建与发布 workflow 的使用方式。对应脚本为 [.gitea/workflows/docker-image.yml](../.gitea/workflows/docker-image.yml)。 + +## 职责划分 + +当前持续集成拆成两条链路: + +| Workflow | 触发时机 | 职责 | +| --- | --- | --- | +| `.gitea/workflows/ci.yml` | `dev`、`main`、`test` push / PR | 安装依赖、Lint、单元测试、Next.js 构建 | +| `.gitea/workflows/docker-image.yml` | `main`、`test` push / 手动触发 | 构建 Docker 镜像并推送到镜像仓库 | + +这样可以避免所有开发分支都发布镜像,也能让代码检查失败时不污染镜像仓库。 + +## 必需 Secrets + +在 Gitea 仓库的 Actions Secrets 中配置以下变量: + +| Secret | 示例 | 说明 | +| --- | --- | --- | +| `REGISTRY_HOST` | `registry.example.com` | `docker login` 使用的镜像仓库地址 | +| `REGISTRY_IMAGE` | `registry.example.com/cozsweet/cozsweet-web` | 完整镜像仓库名,不包含 tag | +| `REGISTRY_USERNAME` | `cozsweet-bot` | 镜像仓库用户名 | +| `REGISTRY_PASSWORD` | `***` | 镜像仓库密码或访问令牌 | +| `TEST_ENV_FILE` | `.env.local` 的完整内容 | 测试环境构建期环境变量 | +| `PRODUCTION_ENV_FILE` | `.env.production` 的完整内容 | 生产环境构建期环境变量 | + +Next.js 的 `NEXT_PUBLIC_*` 会在构建期固化到产物中,因此测试环境和生产环境会分别构建镜像。 + +## 镜像 Tag 规则 + +`test` 分支发布: + +```bash +REGISTRY_IMAGE:test- +REGISTRY_IMAGE:test-latest +``` + +`main` 分支发布: + +```bash +REGISTRY_IMAGE:prod- +REGISTRY_IMAGE:prod-latest +``` + +`` 用于精确回滚,`*-latest` 用于普通部署拉取最新版本。 + +## Runner 要求 + +当前 workflow 使用: + +```yaml +runs-on: gitea-label +``` + +自托管 runner 需要满足: + +1. 安装 Node.js,确保 `node` 在 runner 执行环境的 `PATH` 中。 +2. 安装 Docker CLI。 +3. 当前 runner 用户可以访问 Docker daemon。 +4. 能访问配置的镜像仓库。 +5. 支持 Docker BuildKit secret,即 `DOCKER_BUILDKIT=1 docker build --secret ...`。 + +## 验证方式 + +推送到 `test` 分支后,检查 Actions 日志中是否出现: + +```text +Build Docker image +Push Docker image +``` + +服务器上可查看镜像: + +```bash +docker pull REGISTRY_IMAGE:test-latest +docker image ls | grep cozsweet-web +``` + +如果使用 Gitea 自带 Container Registry,需要确认 Gitea 服务已开启 packages / container registry,并且 `REGISTRY_HOST` 与 Docker 登录地址一致。