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<> $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<> $GITHUB_ENV echo "$release_body" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV else echo "RELEASE_BODY<> $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 }}