mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-30 20:54:32 +08:00
394 lines
16 KiB
YAML
394 lines
16 KiB
YAML
name: MoviePilot Builder v3
|
||
on:
|
||
workflow_dispatch:
|
||
push:
|
||
branches:
|
||
- v3
|
||
paths:
|
||
- 'version.py'
|
||
|
||
permissions:
|
||
contents: write
|
||
packages: write
|
||
|
||
jobs:
|
||
Docker-build:
|
||
runs-on: ubuntu-latest
|
||
name: Build Docker Image
|
||
steps:
|
||
- name: Checkout
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
fetch-tags: true
|
||
|
||
- name: Set up uv
|
||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||
with:
|
||
version: '0.12.5'
|
||
|
||
- name: Verify dependency lock
|
||
run: uv lock --check
|
||
|
||
- name: Audit locked Python dependencies
|
||
run: |
|
||
uv export --quiet --locked --no-dev --no-emit-project \
|
||
--output-file /tmp/moviepilot-audit-requirements.txt
|
||
uvx --from pip-audit==2.10.1 pip-audit \
|
||
--require-hashes \
|
||
--disable-pip \
|
||
--strict \
|
||
--progress-spinner off \
|
||
--requirement /tmp/moviepilot-audit-requirements.txt
|
||
|
||
- name: Release version
|
||
id: release_version
|
||
run: |
|
||
app_version=$(cat version.py |sed -ne "s/APP_VERSION\s=\s'v\(.*\)'/\1/gp")
|
||
frontend_version=$(sed -ne "s/FRONTEND_VERSION\s*=\s*'\([^']*\)'/\1/gp" version.py)
|
||
echo "app_version=$app_version" >> $GITHUB_ENV
|
||
echo "SOURCE_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||
echo "frontend_version=$frontend_version" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Resolve External Payload Revisions
|
||
id: payloads
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
FRONTEND_VERSION: ${{ steps.release_version.outputs.frontend_version }}
|
||
run: |
|
||
plugins_revision=$(git ls-remote https://github.com/jxxghp/MoviePilot-Plugins.git refs/heads/main | awk '{print $1}')
|
||
resources_revision=$(git ls-remote https://github.com/jxxghp/MoviePilot-Resources.git refs/heads/main | awk '{print $1}')
|
||
frontend_digest=$(gh api "repos/jxxghp/MoviePilot-Frontend/releases/tags/${FRONTEND_VERSION}" \
|
||
--jq '.assets[] | select(.name == "dist.zip") | .digest')
|
||
|
||
[[ "$plugins_revision" =~ ^[0-9a-f]{40}$ ]]
|
||
[[ "$resources_revision" =~ ^[0-9a-f]{40}$ ]]
|
||
case "$frontend_digest" in
|
||
sha256:*) frontend_sha256=${frontend_digest#sha256:} ;;
|
||
*) echo "dist.zip 缺少 SHA-256 摘要" >&2; exit 1 ;;
|
||
esac
|
||
[[ "$frontend_sha256" =~ ^[0-9a-f]{64}$ ]]
|
||
|
||
echo "plugins_revision=$plugins_revision" >> "$GITHUB_OUTPUT"
|
||
echo "resources_revision=$resources_revision" >> "$GITHUB_OUTPUT"
|
||
echo "frontend_digest=$frontend_digest" >> "$GITHUB_OUTPUT"
|
||
echo "frontend_sha256=$frontend_sha256" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Checkout Wiki Plugin Market
|
||
uses: actions/checkout@v4
|
||
with:
|
||
repository: jxxghp/MoviePilot-Wiki
|
||
ref: main
|
||
path: .build/moviepilot-wiki
|
||
sparse-checkout: plugin.md
|
||
sparse-checkout-cone-mode: false
|
||
persist-credentials: false
|
||
|
||
- name: Generate Plugin Market Default
|
||
id: plugin_market
|
||
run: |
|
||
python3 -m scripts.generate_plugin_market_default \
|
||
--wiki-file .build/moviepilot-wiki/plugin.md \
|
||
--config-file app/runtime/config.py
|
||
wiki_commit=$(git -C .build/moviepilot-wiki rev-parse HEAD)
|
||
echo "wiki_commit=$wiki_commit" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Download models.dev catalog
|
||
id: models_catalog
|
||
run: |
|
||
temp_file=$(mktemp app/agent/llm/models.json.XXXXXX)
|
||
trap 'rm -f "$temp_file"' EXIT
|
||
curl --fail --show-error --silent --location --retry 3 \
|
||
--connect-timeout 10 --max-time 120 \
|
||
"https://models.dev/api.json" -o "$temp_file"
|
||
jq -e 'type == "object"' "$temp_file" >/dev/null
|
||
# Git keeps only a small placeholder; the release image receives the current catalog.
|
||
chmod 644 "$temp_file"
|
||
mv "$temp_file" app/agent/llm/models.json
|
||
echo "digest=sha256:$(sha256sum app/agent/llm/models.json | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
||
echo "Downloaded models.dev catalog ($(wc -c < app/agent/llm/models.json) bytes)"
|
||
|
||
- name: Create Release Snapshot
|
||
id: release_snapshot
|
||
env:
|
||
WIKI_COMMIT: ${{ steps.plugin_market.outputs.wiki_commit }}
|
||
run: |
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||
git add app/runtime/config.py
|
||
if ! git diff --cached --quiet; then
|
||
git commit -m "build(plugin-market): sync default from MoviePilot-Wiki@${WIKI_COMMIT:0:12}"
|
||
fi
|
||
echo "release_commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: Docker Meta
|
||
id: meta
|
||
uses: docker/metadata-action@v5
|
||
with:
|
||
images: |
|
||
${{ secrets.DOCKER_USERNAME }}/moviepilot-v3
|
||
ghcr.io/${{ github.repository }}-v3
|
||
tags: |
|
||
type=raw,value=${{ env.app_version }}
|
||
type=raw,value=latest
|
||
|
||
- name: Set Up QEMU
|
||
uses: docker/setup-qemu-action@v3
|
||
|
||
- name: Set Up Buildx
|
||
uses: docker/setup-buildx-action@v3
|
||
|
||
- name: Build amd64 candidate
|
||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||
with:
|
||
context: .
|
||
file: docker/Dockerfile
|
||
platforms: linux/amd64
|
||
load: true
|
||
push: false
|
||
pull: true
|
||
tags: moviepilot-v3-candidate:linux-amd64
|
||
build-args: |
|
||
MOVIEPILOT_FRONTEND_VERSION=${{ steps.release_version.outputs.frontend_version }}
|
||
MOVIEPILOT_FRONTEND_SHA256=${{ steps.payloads.outputs.frontend_sha256 }}
|
||
MOVIEPILOT_PLUGINS_REF=${{ steps.payloads.outputs.plugins_revision }}
|
||
MOVIEPILOT_RESOURCES_REF=${{ steps.payloads.outputs.resources_revision }}
|
||
cache-from: type=gha,scope=moviepilot-v3-docker-amd64,version=2
|
||
cache-to: type=gha,scope=moviepilot-v3-docker-amd64,mode=max,version=2
|
||
|
||
- name: Scan amd64 candidate vulnerabilities
|
||
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
|
||
with:
|
||
image-ref: moviepilot-v3-candidate:linux-amd64
|
||
version: v0.70.0
|
||
cache-dir: ${{ runner.temp }}/trivy
|
||
scanners: vuln
|
||
vuln-type: os,library
|
||
severity: HIGH,CRITICAL
|
||
ignore-unfixed: true
|
||
trivyignores: .trivyignore.yaml
|
||
exit-code: 1
|
||
|
||
- name: Build arm64 candidate
|
||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||
with:
|
||
context: .
|
||
file: docker/Dockerfile
|
||
platforms: linux/arm64/v8
|
||
load: true
|
||
push: false
|
||
pull: true
|
||
tags: moviepilot-v3-candidate:linux-arm64
|
||
build-args: |
|
||
MOVIEPILOT_FRONTEND_VERSION=${{ steps.release_version.outputs.frontend_version }}
|
||
MOVIEPILOT_FRONTEND_SHA256=${{ steps.payloads.outputs.frontend_sha256 }}
|
||
MOVIEPILOT_PLUGINS_REF=${{ steps.payloads.outputs.plugins_revision }}
|
||
MOVIEPILOT_RESOURCES_REF=${{ steps.payloads.outputs.resources_revision }}
|
||
cache-from: type=gha,scope=moviepilot-v3-docker-arm64,version=2
|
||
cache-to: type=gha,scope=moviepilot-v3-docker-arm64,mode=max,version=2
|
||
|
||
- name: Scan arm64 candidate vulnerabilities
|
||
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
|
||
with:
|
||
image-ref: moviepilot-v3-candidate:linux-arm64
|
||
version: v0.70.0
|
||
cache-dir: ${{ runner.temp }}/trivy
|
||
scanners: vuln
|
||
vuln-type: os,library
|
||
severity: HIGH,CRITICAL
|
||
ignore-unfixed: true
|
||
trivyignores: .trivyignore.yaml
|
||
exit-code: 1
|
||
|
||
- name: Login DockerHub
|
||
uses: docker/login-action@v3
|
||
with:
|
||
username: ${{ secrets.DOCKER_USERNAME }}
|
||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||
|
||
- name: Login GitHub Container Registry
|
||
uses: docker/login-action@v3
|
||
with:
|
||
registry: ghcr.io
|
||
username: ${{ github.actor }}
|
||
password: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Publish multi-architecture image
|
||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||
with:
|
||
context: .
|
||
file: docker/Dockerfile
|
||
platforms: |
|
||
linux/amd64
|
||
linux/arm64/v8
|
||
push: true
|
||
pull: false
|
||
tags: ${{ steps.meta.outputs.tags }}
|
||
build-args: |
|
||
MOVIEPILOT_FRONTEND_VERSION=${{ steps.release_version.outputs.frontend_version }}
|
||
MOVIEPILOT_FRONTEND_SHA256=${{ steps.payloads.outputs.frontend_sha256 }}
|
||
MOVIEPILOT_PLUGINS_REF=${{ steps.payloads.outputs.plugins_revision }}
|
||
MOVIEPILOT_RESOURCES_REF=${{ steps.payloads.outputs.resources_revision }}
|
||
labels: |
|
||
${{ steps.meta.outputs.labels }}
|
||
org.opencontainers.image.revision=${{ steps.release_snapshot.outputs.release_commit }}
|
||
org.moviepilot.source-revision=${{ env.SOURCE_COMMIT }}
|
||
org.moviepilot.release-snapshot-revision=${{ steps.release_snapshot.outputs.release_commit }}
|
||
org.moviepilot.frontend-version=${{ steps.release_version.outputs.frontend_version }}
|
||
org.moviepilot.frontend-digest=${{ steps.payloads.outputs.frontend_digest }}
|
||
org.moviepilot.plugins-revision=${{ steps.payloads.outputs.plugins_revision }}
|
||
org.moviepilot.resources-revision=${{ steps.payloads.outputs.resources_revision }}
|
||
org.moviepilot.plugin-market-wiki-revision=${{ steps.plugin_market.outputs.wiki_commit }}
|
||
org.moviepilot.models-catalog-digest=${{ steps.models_catalog.outputs.digest }}
|
||
cache-from: |
|
||
type=gha,scope=moviepilot-v3-docker-amd64,version=2
|
||
type=gha,scope=moviepilot-v3-docker-arm64,version=2
|
||
|
||
- name: Generate Changelog
|
||
id: changelog
|
||
run: |
|
||
# 获取上一个 tag(优先 v3.*,回退到任意 v* 版本 tag)
|
||
PREVIOUS_TAG=$(git tag -l 'v3.*' --sort=-v:refname | grep -v "^v${{ env.app_version }}$" | head -n 1)
|
||
if [ -z "$PREVIOUS_TAG" ]; then
|
||
PREVIOUS_TAG=$(git tag -l 'v[0-9]*' --sort=-v:refname | grep -v "^v${{ env.app_version }}$" | head -n 1)
|
||
fi
|
||
echo "Previous tag: $PREVIOUS_TAG"
|
||
|
||
# 使用 || 作为分隔符,同时获取 commit 消息和作者 GitHub 用户名
|
||
if [ -z "$PREVIOUS_TAG" ]; then
|
||
# 首次发布且无任何历史版本 tag,限制条数避免打印整库历史撑爆环境变量
|
||
COMMITS=$(git log --pretty=format:"%s||%an" -n 300 "${SOURCE_COMMIT}")
|
||
else
|
||
COMMITS=$(git log --pretty=format:"%s||%an" "${PREVIOUS_TAG}..${SOURCE_COMMIT}")
|
||
fi
|
||
|
||
# 分类收集 commit 消息(使用关联数组去重)
|
||
declare -A SEEN
|
||
FEATURES=""
|
||
FIXES=""
|
||
OTHERS=""
|
||
|
||
while IFS= read -r line; do
|
||
# 跳过空行
|
||
if [ -z "$line" ]; then
|
||
continue
|
||
fi
|
||
|
||
# 分离 commit 消息和作者
|
||
msg=$(echo "$line" | sed 's/||[^|]*$//')
|
||
author=$(echo "$line" | sed 's/.*||//')
|
||
|
||
# 跳过 Merge commit 和版本更新 commit
|
||
if echo "$msg" | grep -qE "^Merge pull request|^Merge branch|^更新 version"; then
|
||
continue
|
||
fi
|
||
|
||
# 按 Conventional Commits 前缀分类
|
||
if echo "$msg" | grep -qiE "^feat(\(.+\))?:"; then
|
||
desc=$(echo "$msg" | sed -E 's/^feat(\([^)]*\))?:\s*//')
|
||
category="FEATURES"
|
||
elif echo "$msg" | grep -qiE "^fix(\(.+\))?:"; then
|
||
desc=$(echo "$msg" | sed -E 's/^fix(\([^)]*\))?:\s*//')
|
||
category="FIXES"
|
||
elif echo "$msg" | grep -qiE "^(docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?:"; then
|
||
desc=$(echo "$msg" | sed -E 's/^(docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]*\))?:\s*//')
|
||
category="OTHERS"
|
||
else
|
||
desc="$msg"
|
||
category="OTHERS"
|
||
fi
|
||
|
||
# 使用 "分类+描述" 作为去重的 key,跳过重复内容
|
||
dedup_key="${category}::${desc}"
|
||
if [ -n "${SEEN[$dedup_key]+x}" ]; then
|
||
continue
|
||
fi
|
||
SEEN[$dedup_key]=1
|
||
|
||
# 添加 by @author 引用
|
||
entry="- ${desc} by @${author}"
|
||
|
||
case "$category" in
|
||
FEATURES) FEATURES="${FEATURES}${entry}\n" ;;
|
||
FIXES) FIXES="${FIXES}${entry}\n" ;;
|
||
OTHERS) OTHERS="${OTHERS}${entry}\n" ;;
|
||
esac
|
||
done <<< "$COMMITS"
|
||
|
||
# 组装 changelog
|
||
CHANGELOG=""
|
||
|
||
if [ -n "$FEATURES" ]; then
|
||
CHANGELOG="${CHANGELOG}### ✨ 新功能\n\n${FEATURES}\n"
|
||
fi
|
||
|
||
if [ -n "$FIXES" ]; then
|
||
CHANGELOG="${CHANGELOG}### 🐛 修复\n\n${FIXES}\n"
|
||
fi
|
||
|
||
if [ -n "$OTHERS" ]; then
|
||
CHANGELOG="${CHANGELOG}### 🔧 其他\n\n${OTHERS}\n"
|
||
fi
|
||
|
||
# 添加版本对比链接
|
||
if [ -n "$PREVIOUS_TAG" ]; then
|
||
CHANGELOG="${CHANGELOG}**完整更新记录**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...v${{ env.app_version }}"
|
||
fi
|
||
|
||
# 写入环境变量
|
||
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
|
||
echo -e "$CHANGELOG" >> $GITHUB_ENV
|
||
echo "EOF" >> $GITHUB_ENV
|
||
|
||
- name: Get existing release body
|
||
id: get_release_body
|
||
continue-on-error: true
|
||
env:
|
||
CHANGELOG: ${{ env.CHANGELOG }}
|
||
run: |
|
||
release_body=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||
"https://api.github.com/repos/${{ github.repository }}/releases/tags/v${{ env.app_version }}" | \
|
||
jq -r '.body // ""')
|
||
|
||
# 如果已有手动编写的 release body,则保留;否则使用自动生成的 changelog
|
||
if [ -n "$release_body" ] && [ "$release_body" != "null" ] && [ "$release_body" != "" ]; then
|
||
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
|
||
echo "$release_body" >> $GITHUB_ENV
|
||
echo "EOF" >> $GITHUB_ENV
|
||
else
|
||
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
|
||
echo "$CHANGELOG" >> $GITHUB_ENV
|
||
echo "EOF" >> $GITHUB_ENV
|
||
fi
|
||
|
||
- name: Delete Release
|
||
uses: dev-drprasad/delete-tag-and-release@v1.1
|
||
continue-on-error: true
|
||
with:
|
||
tag_name: v${{ env.app_version }}
|
||
delete_release: true
|
||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Publish Release Tag
|
||
env:
|
||
RELEASE_COMMIT: ${{ steps.release_snapshot.outputs.release_commit }}
|
||
run: |
|
||
tag_name="v${{ env.app_version }}"
|
||
if git show-ref --verify --quiet "refs/tags/${tag_name}"; then
|
||
git tag -d "$tag_name"
|
||
fi
|
||
git tag "$tag_name" "$RELEASE_COMMIT"
|
||
git push origin "refs/tags/${tag_name}"
|
||
|
||
- name: Generate Release
|
||
uses: softprops/action-gh-release@v2
|
||
with:
|
||
tag_name: v${{ env.app_version }}
|
||
name: v${{ env.app_version }}
|
||
body: ${{ env.RELEASE_BODY }}
|
||
draft: false
|
||
prerelease: false
|
||
make_latest: true
|
||
env:
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|