fix(ci): release only from manually pushed tags, never from merged PRs

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jejs2dffkscAbhZkcjZ1oK
This commit is contained in:
Estrella Pan
2026-07-08 16:25:09 +02:00
parent 277f3d740c
commit 122cf68eac
2 changed files with 57 additions and 56 deletions

View File

@@ -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: |

View File

@@ -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 `<version>` + `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 <merge-commit-on-main>
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 `<version>` + `latest`) and creates the GitHub release with notes from `docs/changelog/<X.Y>.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"`.