From 122cf68eac162417f5d80249b08f0240ff4b05e8 Mon Sep 17 00:00:00 2001 From: Estrella Pan Date: Wed, 8 Jul 2026 16:25:09 +0200 Subject: [PATCH] fix(ci): release only from manually pushed tags, never from merged PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merging PR #1063 ("Doc update", 3.3-dev -> main) triggered the stable release path, which fell back to the raw PR title as the version string and shipped a Docker image tagged Doc-update as :latest — every container pulling latest then crashed on SemVer parsing (#1065). The release trigger is now explicit: - bare X.Y.Z tag -> stable release, refused unless the tagged commit is an ancestor of main - tag containing alpha/beta -> pre-release (unchanged) - PR merges only run tests and a docker build test Also pass head.ref through an env var instead of inlining it in the script (injection hardening), and take release notes solely from the changelog file since tag pushes carry no PR body. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Jejs2dffkscAbhZkcjZ1oK --- .github/workflows/build.yml | 95 ++++++++++++++++--------------------- CLAUDE.md | 18 ++++++- 2 files changed, 57 insertions(+), 56 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 937480ef..315513c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,6 @@ on: types: - opened - synchronize - - closed branches: - main - "3.*-dev" @@ -82,66 +81,54 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: If release - id: release + with: + fetch-depth: 0 + # 发布只由手动推送 tag 触发: + # - 稳定版:语义化版本号 tag(如 3.3.2),且必须打在 main 的提交上 + # - 预发布:包含 alpha/beta 的 tag(如 3.4.0-beta.1),在 dev 分支上打 + # 合并 PR 不再触发发布(防止 PR 标题被当作版本号,见 #1065)。 + - name: Classify trigger + id: classify + env: + TAG: ${{ github.ref_name }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | - if [[ '${{ github.event_name }}' == 'pull_request' && '${{ github.event.pull_request.head.ref }}' == *'dev'* ]]; then - if [ ${{ github.event.pull_request.merged }} == true ]; then - echo "release=1" >> $GITHUB_OUTPUT - else - echo "release=0" >> $GITHUB_OUTPUT - fi - elif [[ '${{ github.event_name }}' == 'push' && (${{ github.ref }} == *'alpha'* || ${{ github.ref }} == *'beta'*) ]]; then - echo "release=1" >> $GITHUB_OUTPUT - else - echo "release=0" >> $GITHUB_OUTPUT - fi - - name: If dev - id: dev - run: | - if [[ '${{ github.event_name }}' == 'push' && (${{ github.ref }} == *'alpha'* || ${{ github.ref }} == *'beta'*) ]]; then - echo "dev=1" >> $GITHUB_OUTPUT - else - echo "dev=0" >> $GITHUB_OUTPUT - fi - - name: Check version - id: version - run: | - if [ '${{ github.event_name }}' == 'pull_request' ]; then - if [ ${{ github.event.pull_request.merged }} == true ]; then - # Extract version from PR title (handles "Release X.Y.Z", "vX.Y.Z", or "X.Y.Z") - PR_TITLE="${{ github.event.pull_request.title }}" - VERSION=$(echo "$PR_TITLE" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?' | head -1) - if [ -n "$VERSION" ]; then - echo "version=$VERSION" >> $GITHUB_OUTPUT + RELEASE=0 + DEV=0 + BUILD_TEST=0 + VERSION=Test + if [[ '${{ github.ref_type }}' == 'tag' ]]; then + if [[ "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + if git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then + RELEASE=1 + VERSION="$TAG" else - echo "version=$PR_TITLE" >> $GITHUB_OUTPUT + echo "::error::Stable tag $TAG does not point to a commit on main; refusing to release" + exit 1 fi + elif [[ "$TAG" == *'alpha'* || "$TAG" == *'beta'* ]]; then + RELEASE=1 + DEV=1 + VERSION="$TAG" fi - elif [[ ${{ github.event_name }} == 'push' && (${{ github.ref }} == *'alpha'* || ${{ github.ref }} == *'beta'*) ]]; then - echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT - else - echo "version=Test" >> $GITHUB_OUTPUT - fi - - name: If build test - id: build_test - run: | - if [[ '${{ github.event_name }}' == 'pull_request' && '${{ github.event.pull_request.merged }}' != 'true' && '${{ github.event.pull_request.head.ref }}' == *'dev'* ]]; then - echo "build_test=1" >> $GITHUB_OUTPUT - else - echo "build_test=0" >> $GITHUB_OUTPUT + elif [[ '${{ github.event_name }}' == 'pull_request' && "$HEAD_REF" == *'dev'* ]]; then + BUILD_TEST=1 fi + echo "release=$RELEASE" >> $GITHUB_OUTPUT + echo "dev=$DEV" >> $GITHUB_OUTPUT + echo "build_test=$BUILD_TEST" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Check result run: | - echo "release: ${{ steps.release.outputs.release }}" - echo "dev: ${{ steps.dev.outputs.dev }}" - echo "build_test: ${{ steps.build_test.outputs.build_test }}" - echo "version: ${{ steps.version.outputs.version }}" + echo "release: ${{ steps.classify.outputs.release }}" + echo "dev: ${{ steps.classify.outputs.dev }}" + echo "build_test: ${{ steps.classify.outputs.build_test }}" + echo "version: ${{ steps.classify.outputs.version }}" outputs: - release: ${{ steps.release.outputs.release }} - dev: ${{ steps.dev.outputs.dev }} - build_test: ${{ steps.build_test.outputs.build_test }} - version: ${{ steps.version.outputs.version }} + release: ${{ steps.classify.outputs.release }} + dev: ${{ steps.classify.outputs.dev }} + build_test: ${{ steps.classify.outputs.build_test }} + version: ${{ steps.classify.outputs.version }} build-webui: runs-on: ubuntu-latest @@ -422,7 +409,7 @@ jobs: with: tag_name: ${{ needs.version-info.outputs.version }} name: ${{ steps.release-info.outputs.version }} - body: ${{ github.event.pull_request.body || steps.changelog.outputs.body }} + body: ${{ steps.changelog.outputs.body }} draft: false prerelease: ${{ steps.release-info.outputs.pre_release == 'true' }} files: | diff --git a/CLAUDE.md b/CLAUDE.md index bf5c5779..bc632d4d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,7 +137,11 @@ webui/src/ - Bug fixes → PR to current released version's `-dev` branch - New features → PR to next version's `-dev` branch -## Releasing a Beta Version +## Releasing + +All releases are triggered by manually pushing a tag — merging a PR never releases. + +### Beta Version 1. Update version in `backend/pyproject.toml` 2. Update `CHANGELOG.md` with the new version heading @@ -147,7 +151,17 @@ webui/src/ git tag 3.2.0-beta.4 git push origin 3.2.0-beta.4 ``` -5. The CI/CD workflow (`.github/workflows/build.yml`) detects the tag contains "beta", uses the tag name as the VERSION string, generates `module/__version__.py`, and builds the Docker image +5. The CI/CD workflow (`.github/workflows/build.yml`) detects the tag contains "beta", uses the tag name as the VERSION string, generates `module/__version__.py`, and builds the Docker image (tagged `` + `dev-latest`) + +### Stable Version + +1. Merge the dev branch into `main` via PR (this only runs tests and a build test) +2. Tag the merge commit on `main` with the bare semver version and push: + ```bash + git tag 3.3.2 + git push origin 3.3.2 + ``` +3. CI validates the tag is `X.Y.Z` **and** points to a commit on `main` (refuses otherwise), then builds and pushes Docker images (tagged `` + `latest`) and creates the GitHub release with notes from `docs/changelog/.md` The VERSION is injected at build time via CI — `module/__version__.py` does not exist in the repo. At runtime, `module/conf/config.py` imports it or falls back to `"DEV_VERSION"`.