mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-02 21:55:22 +08:00
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
436 lines
15 KiB
YAML
436 lines
15 KiB
YAML
name: Build Docker
|
||
|
||
on:
|
||
pull_request:
|
||
types:
|
||
- opened
|
||
- synchronize
|
||
branches:
|
||
- main
|
||
- "3.*-dev"
|
||
push:
|
||
|
||
jobs:
|
||
test:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- name: Set up Python 3.13
|
||
uses: actions/setup-python@v5
|
||
with:
|
||
python-version: "3.13"
|
||
- uses: astral-sh/setup-uv@v4
|
||
with:
|
||
version: "latest"
|
||
- name: Install dependencies
|
||
run: cd backend && uv sync --group dev
|
||
- name: Lint
|
||
run: cd backend && uv run ruff check src
|
||
- name: Type check
|
||
run: cd backend && uv run mypy src
|
||
- name: Test
|
||
run: |
|
||
mkdir -p backend/config
|
||
cd backend && uv run pytest src/test -v
|
||
|
||
webui-test:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Install Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: 20
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
name: Install pnpm
|
||
with:
|
||
version: 9
|
||
run_install: false
|
||
|
||
- name: Get pnpm store directory
|
||
shell: bash
|
||
run: |
|
||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||
|
||
- uses: actions/cache@v4
|
||
name: Setup pnpm cache
|
||
with:
|
||
path: ${{ env.STORE_PATH }}
|
||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-pnpm-store-
|
||
|
||
- name: Install dependencies
|
||
run: cd webui && pnpm install
|
||
|
||
- name: Lint
|
||
run: cd webui && pnpm lint
|
||
|
||
- name: Unit test
|
||
run: cd webui && pnpm test
|
||
|
||
- name: build test
|
||
run: |
|
||
cd webui && pnpm test:build
|
||
|
||
version-info:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
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: |
|
||
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 "::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 }}' == '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.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.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
|
||
needs: [test, webui-test, version-info]
|
||
if: ${{ needs.version-info.outputs.release == 1 || needs.version-info.outputs.dev == 1 || needs.version-info.outputs.build_test == 1 }}
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Install Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: 20
|
||
|
||
- uses: pnpm/action-setup@v4
|
||
name: Install pnpm
|
||
with:
|
||
version: 9
|
||
run_install: false
|
||
|
||
- name: Get pnpm store directory
|
||
shell: bash
|
||
run: |
|
||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||
|
||
- uses: actions/cache@v4
|
||
name: Setup pnpm cache
|
||
with:
|
||
path: ${{ env.STORE_PATH }}
|
||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||
restore-keys: |
|
||
${{ runner.os }}-pnpm-store-
|
||
|
||
- name: Install dependencies
|
||
run: cd webui && pnpm install
|
||
|
||
- name: Build
|
||
run: |
|
||
cd webui && pnpm build
|
||
|
||
- name: Upload artifact
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: dist
|
||
path: webui/dist
|
||
|
||
build-docker:
|
||
runs-on: ubuntu-latest
|
||
needs: [build-webui, version-info]
|
||
if: ${{ needs.version-info.outputs.release == 1 || needs.version-info.outputs.dev == 1 || needs.version-info.outputs.build_test == 1 }}
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Create Version info via tag
|
||
working-directory: ./backend/src
|
||
run: |
|
||
echo ${{ needs.version-info.outputs.version }}
|
||
echo "VERSION='${{ needs.version-info.outputs.version }}'" >> module/__version__.py
|
||
|
||
- name: Set up QEMU
|
||
uses: docker/setup-qemu-action@v3
|
||
|
||
- name: Set up Docker Buildx
|
||
id: buildx
|
||
uses: docker/setup-buildx-action@v3
|
||
|
||
- name: Docker metadata main
|
||
if: ${{ needs.version-info.outputs.release == 1 && needs.version-info.outputs.dev != 1 }}
|
||
id: meta
|
||
uses: docker/metadata-action@v5
|
||
with:
|
||
images: |
|
||
estrellaxd/auto_bangumi
|
||
ghcr.io/${{ github.repository }}
|
||
tags: |
|
||
type=raw,value=${{ needs.version-info.outputs.version }}
|
||
type=raw,value=latest
|
||
|
||
- name: Docker metadata dev
|
||
if: ${{ needs.version-info.outputs.dev == 1 }}
|
||
id: meta-dev
|
||
uses: docker/metadata-action@v5
|
||
with:
|
||
images: |
|
||
estrellaxd/auto_bangumi
|
||
ghcr.io/${{ github.repository }}
|
||
tags: |
|
||
type=raw,value=${{ needs.version-info.outputs.version }}
|
||
type=raw,value=dev-latest
|
||
|
||
- name: Login to DockerHub
|
||
if: ${{ needs.version-info.outputs.release == 1 }}
|
||
uses: docker/login-action@v3
|
||
with:
|
||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||
|
||
- name: Login to ghcr.io
|
||
if: ${{ needs.version-info.outputs.release == 1 }}
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: ghcr.io
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.ACCESS_TOKEN }}
|
||
|
||
- name: Download artifact
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: dist
|
||
path: backend/src/dist
|
||
|
||
- name: Build and push
|
||
if: ${{ needs.version-info.outputs.release == 1 && needs.version-info.outputs.dev != 1 }}
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
context: .
|
||
builder: ${{ steps.buildx.outputs.name }}
|
||
platforms: linux/amd64,linux/arm64
|
||
push: True
|
||
build-args: |
|
||
VERSION=${{ needs.version-info.outputs.version }}
|
||
tags: ${{ steps.meta.outputs.tags }}
|
||
labels: ${{ steps.meta.outputs.labels }}
|
||
cache-from: type=gha,scope=${{ github.workflow }}
|
||
cache-to: type=gha,scope=${{ github.workflow }},ignore-error=true
|
||
|
||
- name: Build and push dev
|
||
if: ${{ needs.version-info.outputs.dev == 1 }}
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
context: .
|
||
builder: ${{ steps.buildx.outputs.name }}
|
||
platforms: linux/amd64,linux/arm64
|
||
push: ${{ github.event_name == 'push' }}
|
||
build-args: |
|
||
VERSION=${{ needs.version-info.outputs.version }}
|
||
tags: ${{ steps.meta-dev.outputs.tags }}
|
||
labels: ${{ steps.meta-dev.outputs.labels }}
|
||
cache-from: type=gha,scope=${{ github.workflow }}
|
||
cache-to: type=gha,scope=${{ github.workflow }},ignore-error=true
|
||
|
||
- name: Build test
|
||
if: ${{ needs.version-info.outputs.release == 0 }}
|
||
uses: docker/build-push-action@v6
|
||
with:
|
||
context: .
|
||
builder: ${{ steps.buildx.outputs.name }}
|
||
platforms: linux/amd64,linux/arm64
|
||
push: false
|
||
build-args: |
|
||
VERSION=${{ needs.version-info.outputs.version }}
|
||
tags: estrellaxd/auto_bangumi:test
|
||
cache-from: type=gha,scope=${{ github.workflow }}
|
||
cache-to: type=gha,scope=${{ github.workflow }},ignore-error=true
|
||
|
||
release:
|
||
runs-on: ubuntu-latest
|
||
needs: [build-docker, version-info]
|
||
if: ${{ needs.version-info.outputs.release == 1 }}
|
||
outputs:
|
||
url: ${{ steps.release.outputs.url }}
|
||
version: ${{ needs.version-info.outputs.version }}
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
- name: Download artifact webui
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: dist
|
||
path: webui/dist
|
||
|
||
- name: Zip webui
|
||
run: |
|
||
cd webui && ls -al && tree && zip -r dist.zip dist
|
||
|
||
- name: Download artifact app
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: dist
|
||
path: backend/src/dist
|
||
|
||
- name: Create Version info via tag
|
||
working-directory: ./backend/src
|
||
run: |
|
||
echo ${{ needs.version-info.outputs.version }}
|
||
echo "VERSION='${{ needs.version-info.outputs.version }}'" >> module/__version__.py
|
||
|
||
- uses: astral-sh/setup-uv@v4
|
||
with:
|
||
version: "latest"
|
||
|
||
- name: Generate requirements.txt for non-uv consumers (#994)
|
||
run: |
|
||
cd backend && uv export --format requirements-txt --no-hashes --no-dev -o requirements.txt
|
||
|
||
- name: Zip app
|
||
run: |
|
||
cd backend && zip -r app-v${{ needs.version-info.outputs.version }}.zip \
|
||
src pyproject.toml uv.lock requirements.txt
|
||
|
||
# 在线自动更新 bundle:module 源码树 + pyproject/uv.lock + 前端 dist + manifest。
|
||
# 同时产出 .sha256(客户端下载后据此校验完整性)。min_image_version 是能应用
|
||
# 此 bundle 布局的最低镜像版本(引入在线更新的首个版本)。
|
||
- name: Build update bundle
|
||
run: |
|
||
VERSION="${{ needs.version-info.outputs.version }}"
|
||
MIN_IMAGE_VERSION="3.3.0-beta.1"
|
||
STAGE="bundle_stage"
|
||
rm -rf "$STAGE"
|
||
mkdir -p "$STAGE/backend/src" "$STAGE/webui-dist"
|
||
cp -r backend/src/module "$STAGE/backend/src/module"
|
||
cp backend/pyproject.toml backend/uv.lock "$STAGE/backend/"
|
||
cp -r webui/dist/. "$STAGE/webui-dist/"
|
||
LOCK_SHA=$(sha256sum backend/uv.lock | awk '{print $1}')
|
||
cat > "$STAGE/manifest.json" <<EOF
|
||
{
|
||
"version": "$VERSION",
|
||
"min_image_version": "$MIN_IMAGE_VERSION",
|
||
"lockfile_sha256": "$LOCK_SHA",
|
||
"notes": "See release notes."
|
||
}
|
||
EOF
|
||
( cd "$STAGE" && zip -r "../update-bundle-${VERSION}.zip" backend webui-dist manifest.json )
|
||
sha256sum "update-bundle-${VERSION}.zip" | awk '{print $1}' > "update-bundle-${VERSION}.zip.sha256"
|
||
|
||
# 对 bundle 做 ed25519 签名(私钥在 UPDATE_SIGNING_KEY secret,公钥随镜像
|
||
# 分发)。客户端 apply 前验签,boot_overlay 每次启动重新验签——没有 .sig
|
||
# 的 release 会被更新器拒绝,所以签名失败必须让构建失败。
|
||
- name: Sign update bundle
|
||
env:
|
||
UPDATE_SIGNING_KEY: ${{ secrets.UPDATE_SIGNING_KEY }}
|
||
run: |
|
||
if [ -z "$UPDATE_SIGNING_KEY" ]; then
|
||
echo "::error::UPDATE_SIGNING_KEY secret is not set; unsigned bundles are rejected by the updater" >&2
|
||
exit 1
|
||
fi
|
||
VERSION="${{ needs.version-info.outputs.version }}"
|
||
uv run --no-project --with cryptography python - "update-bundle-${VERSION}.zip" <<'PY'
|
||
import base64, os, sys
|
||
from pathlib import Path
|
||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||
|
||
bundle = Path(sys.argv[1])
|
||
key = load_pem_private_key(os.environ["UPDATE_SIGNING_KEY"].encode(), password=None)
|
||
sig = key.sign(bundle.read_bytes())
|
||
Path(str(bundle) + ".sig").write_text(base64.b64encode(sig).decode() + "\n")
|
||
print(f"signed {bundle}")
|
||
PY
|
||
|
||
- name: Generate Release info
|
||
id: release-info
|
||
run: |
|
||
if ${{ needs.version-info.outputs.dev == 1 }}; then
|
||
echo "version=🌙${{ needs.version-info.outputs.version }}" >> $GITHUB_OUTPUT
|
||
echo "pre_release=true" >> $GITHUB_OUTPUT
|
||
else
|
||
echo "version=🌟${{ needs.version-info.outputs.version }}" >> $GITHUB_OUTPUT
|
||
echo "pre_release=false" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Read changelog
|
||
id: changelog
|
||
run: |
|
||
# Derive the changelog file from the release version's major.minor (e.g. 3.3.0-beta.1 -> 3.3.md)
|
||
CHANGELOG_VERSION=$(echo "${{ needs.version-info.outputs.version }}" | grep -oE '^[0-9]+\.[0-9]+')
|
||
CHANGELOG_FILE="docs/changelog/${CHANGELOG_VERSION}.md"
|
||
if [ -f "$CHANGELOG_FILE" ]; then
|
||
echo "body<<EOF" >> $GITHUB_OUTPUT
|
||
cat "$CHANGELOG_FILE" >> $GITHUB_OUTPUT
|
||
echo "EOF" >> $GITHUB_OUTPUT
|
||
fi
|
||
|
||
- name: Release
|
||
id: release
|
||
uses: softprops/action-gh-release@v1
|
||
with:
|
||
tag_name: ${{ needs.version-info.outputs.version }}
|
||
name: ${{ steps.release-info.outputs.version }}
|
||
body: ${{ steps.changelog.outputs.body }}
|
||
draft: false
|
||
prerelease: ${{ steps.release-info.outputs.pre_release == 'true' }}
|
||
files: |
|
||
webui/dist.zip
|
||
backend/app-v${{ needs.version-info.outputs.version }}.zip
|
||
update-bundle-${{ needs.version-info.outputs.version }}.zip
|
||
update-bundle-${{ needs.version-info.outputs.version }}.zip.sha256
|
||
update-bundle-${{ needs.version-info.outputs.version }}.zip.sig
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||
|
||
telegram:
|
||
runs-on: ubuntu-latest
|
||
needs: [release]
|
||
steps:
|
||
- name: send telegram message on push
|
||
uses: appleboy/telegram-action@master
|
||
with:
|
||
to: ${{ secrets.TELEGRAM_TO }}
|
||
token: ${{ secrets.TELEGRAM_TOKEN }}
|
||
message: |
|
||
New release: ${{ needs.release.outputs.version }}
|
||
Link: ${{ needs.release.outputs.url }}
|