Merge pull request #6486 from InfinityPacer/codex/chore/floating-build-toolchain

This commit is contained in:
jxxghp
2026-08-28 11:01:15 +08:00
committed by GitHub
23 changed files with 459 additions and 162 deletions
+3 -4
View File
@@ -20,21 +20,20 @@ jobs:
steps: steps:
- name: Checkout MoviePilot - name: Checkout MoviePilot
uses: actions/checkout@v5 uses: actions/checkout@v7
with: with:
path: MoviePilot path: MoviePilot
- name: Checkout MoviePilot-Plugins - name: Checkout MoviePilot-Plugins
uses: actions/checkout@v5 uses: actions/checkout@v7
with: with:
repository: jxxghp/MoviePilot-Plugins repository: jxxghp/MoviePilot-Plugins
ref: main ref: main
path: MoviePilot-Plugins path: MoviePilot-Plugins
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@v10.0.1
with: with:
version: '0.12.5'
python-version: '3.14' python-version: '3.14'
enable-cache: true enable-cache: true
cache-dependency-glob: | cache-dependency-glob: |
+25 -27
View File
@@ -15,12 +15,10 @@ jobs:
TRIVY_SKIP_JAVA_DB_UPDATE: "true" TRIVY_SKIP_JAVA_DB_UPDATE: "true"
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@v10.0.1
with:
version: '0.12.5'
- name: Verify dependency lock - name: Verify dependency lock
run: uv lock --check run: uv lock --check
@@ -29,7 +27,7 @@ jobs:
run: | run: |
uv export --quiet --locked --no-default-groups --group runtime-standard \ uv export --quiet --locked --no-default-groups --group runtime-standard \
--no-emit-project --output-file /tmp/moviepilot-audit-standard.txt --no-emit-project --output-file /tmp/moviepilot-audit-standard.txt
uvx --from pip-audit==2.10.1 pip-audit \ uvx --from pip-audit pip-audit \
--require-hashes --disable-pip --strict --progress-spinner off \ --require-hashes --disable-pip --strict --progress-spinner off \
--requirement /tmp/moviepilot-audit-standard.txt --requirement /tmp/moviepilot-audit-standard.txt
@@ -40,7 +38,7 @@ jobs:
--lock uv.lock \ --lock uv.lock \
--input /tmp/moviepilot-audit-free-threaded.txt \ --input /tmp/moviepilot-audit-free-threaded.txt \
--output /tmp/moviepilot-audit-free-threaded-normalized.txt --output /tmp/moviepilot-audit-free-threaded-normalized.txt
uvx --from pip-audit==2.10.1 pip-audit \ uvx --from pip-audit pip-audit \
--no-deps --disable-pip --strict --progress-spinner off \ --no-deps --disable-pip --strict --progress-spinner off \
--requirement /tmp/moviepilot-audit-free-threaded-normalized.txt --requirement /tmp/moviepilot-audit-free-threaded-normalized.txt
@@ -78,7 +76,7 @@ jobs:
echo "frontend_sha256=$frontend_sha256" >> "$GITHUB_OUTPUT" echo "frontend_sha256=$frontend_sha256" >> "$GITHUB_OUTPUT"
- name: Checkout Wiki Plugin Market - name: Checkout Wiki Plugin Market
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
repository: jxxghp/MoviePilot-Wiki repository: jxxghp/MoviePilot-Wiki
ref: main ref: main
@@ -113,7 +111,7 @@ jobs:
- name: Docker Meta - name: Docker Meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: | images: |
${{ secrets.DOCKER_USERNAME }}/moviepilot-v3 ${{ secrets.DOCKER_USERNAME }}/moviepilot-v3
@@ -123,7 +121,7 @@ jobs:
- name: Docker Meta free-threaded - name: Docker Meta free-threaded
id: meta_ft id: meta_ft
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: | images: |
${{ secrets.DOCKER_USERNAME }}/moviepilot-v3t ${{ secrets.DOCKER_USERNAME }}/moviepilot-v3t
@@ -132,13 +130,13 @@ jobs:
type=raw,value=beta type=raw,value=beta
- name: Set Up QEMU - name: Set Up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set Up Buildx - name: Set Up Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build standard amd64 candidate - name: Build standard amd64 candidate
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -157,10 +155,10 @@ jobs:
cache-to: type=gha,scope=moviepilot-v3-standard-docker-amd64,mode=max,version=2 cache-to: type=gha,scope=moviepilot-v3-standard-docker-amd64,mode=max,version=2
- name: Scan standard amd64 candidate vulnerabilities - name: Scan standard amd64 candidate vulnerabilities
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 uses: aquasecurity/trivy-action@v0.36.0
with: with:
image-ref: moviepilot-v3-candidate:linux-amd64 image-ref: moviepilot-v3-candidate:linux-amd64
version: v0.70.0 version: latest
cache-dir: ${{ runner.temp }}/trivy cache-dir: ${{ runner.temp }}/trivy
scanners: vuln scanners: vuln
vuln-type: os,library vuln-type: os,library
@@ -170,7 +168,7 @@ jobs:
exit-code: 1 exit-code: 1
- name: Build standard arm64 candidate - name: Build standard arm64 candidate
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -189,10 +187,10 @@ jobs:
cache-to: type=gha,scope=moviepilot-v3-standard-docker-arm64,mode=max,version=2 cache-to: type=gha,scope=moviepilot-v3-standard-docker-arm64,mode=max,version=2
- name: Scan standard arm64 candidate vulnerabilities - name: Scan standard arm64 candidate vulnerabilities
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 uses: aquasecurity/trivy-action@v0.36.0
with: with:
image-ref: moviepilot-v3-candidate:linux-arm64 image-ref: moviepilot-v3-candidate:linux-arm64
version: v0.70.0 version: latest
cache-dir: ${{ runner.temp }}/trivy cache-dir: ${{ runner.temp }}/trivy
scanners: vuln scanners: vuln
vuln-type: os,library vuln-type: os,library
@@ -202,7 +200,7 @@ jobs:
exit-code: 1 exit-code: 1
- name: Build free-threaded amd64 candidate - name: Build free-threaded amd64 candidate
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -221,10 +219,10 @@ jobs:
cache-to: type=gha,scope=moviepilot-v3t-docker-amd64,mode=max,version=2 cache-to: type=gha,scope=moviepilot-v3t-docker-amd64,mode=max,version=2
- name: Scan free-threaded amd64 candidate vulnerabilities - name: Scan free-threaded amd64 candidate vulnerabilities
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 uses: aquasecurity/trivy-action@v0.36.0
with: with:
image-ref: moviepilot-v3t-candidate:linux-amd64 image-ref: moviepilot-v3t-candidate:linux-amd64
version: v0.70.0 version: latest
cache-dir: ${{ runner.temp }}/trivy cache-dir: ${{ runner.temp }}/trivy
scanners: vuln scanners: vuln
vuln-type: os,library vuln-type: os,library
@@ -234,7 +232,7 @@ jobs:
exit-code: 1 exit-code: 1
- name: Build free-threaded arm64 candidate - name: Build free-threaded arm64 candidate
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -253,10 +251,10 @@ jobs:
cache-to: type=gha,scope=moviepilot-v3t-docker-arm64,mode=max,version=2 cache-to: type=gha,scope=moviepilot-v3t-docker-arm64,mode=max,version=2
- name: Scan free-threaded arm64 candidate vulnerabilities - name: Scan free-threaded arm64 candidate vulnerabilities
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 uses: aquasecurity/trivy-action@v0.36.0
with: with:
image-ref: moviepilot-v3t-candidate:linux-arm64 image-ref: moviepilot-v3t-candidate:linux-arm64
version: v0.70.0 version: latest
cache-dir: ${{ runner.temp }}/trivy cache-dir: ${{ runner.temp }}/trivy
scanners: vuln scanners: vuln
vuln-type: os,library vuln-type: os,library
@@ -266,20 +264,20 @@ jobs:
exit-code: 1 exit-code: 1
- name: Login DockerHub - name: Login DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login GitHub Container Registry - name: Login GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish standard multi-architecture image - name: Publish standard multi-architecture image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -309,7 +307,7 @@ jobs:
type=gha,scope=moviepilot-v3-standard-docker-arm64,version=2 type=gha,scope=moviepilot-v3-standard-docker-arm64,version=2
- name: Publish free-threaded multi-architecture image - name: Publish free-threaded multi-architecture image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
+83 -60
View File
@@ -20,15 +20,13 @@ jobs:
TRIVY_SKIP_JAVA_DB_UPDATE: "true" TRIVY_SKIP_JAVA_DB_UPDATE: "true"
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
fetch-tags: true fetch-tags: true
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@v10.0.1
with:
version: '0.12.5'
- name: Verify dependency lock - name: Verify dependency lock
run: uv lock --check run: uv lock --check
@@ -37,7 +35,7 @@ jobs:
run: | run: |
uv export --quiet --locked --no-default-groups --group runtime-standard \ uv export --quiet --locked --no-default-groups --group runtime-standard \
--no-emit-project --output-file /tmp/moviepilot-audit-standard.txt --no-emit-project --output-file /tmp/moviepilot-audit-standard.txt
uvx --from pip-audit==2.10.1 pip-audit \ uvx --from pip-audit pip-audit \
--require-hashes --disable-pip --strict --progress-spinner off \ --require-hashes --disable-pip --strict --progress-spinner off \
--requirement /tmp/moviepilot-audit-standard.txt --requirement /tmp/moviepilot-audit-standard.txt
@@ -48,7 +46,7 @@ jobs:
--lock uv.lock \ --lock uv.lock \
--input /tmp/moviepilot-audit-free-threaded.txt \ --input /tmp/moviepilot-audit-free-threaded.txt \
--output /tmp/moviepilot-audit-free-threaded-normalized.txt --output /tmp/moviepilot-audit-free-threaded-normalized.txt
uvx --from pip-audit==2.10.1 pip-audit \ uvx --from pip-audit pip-audit \
--no-deps --disable-pip --strict --progress-spinner off \ --no-deps --disable-pip --strict --progress-spinner off \
--requirement /tmp/moviepilot-audit-free-threaded-normalized.txt --requirement /tmp/moviepilot-audit-free-threaded-normalized.txt
@@ -86,7 +84,7 @@ jobs:
echo "frontend_sha256=$frontend_sha256" >> "$GITHUB_OUTPUT" echo "frontend_sha256=$frontend_sha256" >> "$GITHUB_OUTPUT"
- name: Checkout Wiki Plugin Market - name: Checkout Wiki Plugin Market
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
repository: jxxghp/MoviePilot-Wiki repository: jxxghp/MoviePilot-Wiki
ref: main ref: main
@@ -134,7 +132,7 @@ jobs:
- name: Docker Meta - name: Docker Meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: | images: |
${{ secrets.DOCKER_USERNAME }}/moviepilot-v3 ${{ secrets.DOCKER_USERNAME }}/moviepilot-v3
@@ -144,7 +142,7 @@ jobs:
- name: Docker Meta free-threaded - name: Docker Meta free-threaded
id: meta_ft id: meta_ft
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: | images: |
${{ secrets.DOCKER_USERNAME }}/moviepilot-v3t ${{ secrets.DOCKER_USERNAME }}/moviepilot-v3t
@@ -153,13 +151,13 @@ jobs:
type=raw,value=${{ env.app_version }} type=raw,value=${{ env.app_version }}
- name: Set Up QEMU - name: Set Up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set Up Buildx - name: Set Up Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Build amd64 candidate - name: Build amd64 candidate
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -178,10 +176,10 @@ jobs:
cache-to: type=gha,scope=moviepilot-v3-standard-docker-amd64,mode=max,version=2 cache-to: type=gha,scope=moviepilot-v3-standard-docker-amd64,mode=max,version=2
- name: Scan amd64 candidate vulnerabilities - name: Scan amd64 candidate vulnerabilities
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 uses: aquasecurity/trivy-action@v0.36.0
with: with:
image-ref: moviepilot-v3-candidate:linux-amd64 image-ref: moviepilot-v3-candidate:linux-amd64
version: v0.70.0 version: latest
cache-dir: ${{ runner.temp }}/trivy cache-dir: ${{ runner.temp }}/trivy
scanners: vuln scanners: vuln
vuln-type: os,library vuln-type: os,library
@@ -191,7 +189,7 @@ jobs:
exit-code: 1 exit-code: 1
- name: Build arm64 candidate - name: Build arm64 candidate
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -210,10 +208,10 @@ jobs:
cache-to: type=gha,scope=moviepilot-v3-standard-docker-arm64,mode=max,version=2 cache-to: type=gha,scope=moviepilot-v3-standard-docker-arm64,mode=max,version=2
- name: Scan arm64 candidate vulnerabilities - name: Scan arm64 candidate vulnerabilities
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 uses: aquasecurity/trivy-action@v0.36.0
with: with:
image-ref: moviepilot-v3-candidate:linux-arm64 image-ref: moviepilot-v3-candidate:linux-arm64
version: v0.70.0 version: latest
cache-dir: ${{ runner.temp }}/trivy cache-dir: ${{ runner.temp }}/trivy
scanners: vuln scanners: vuln
vuln-type: os,library vuln-type: os,library
@@ -223,7 +221,7 @@ jobs:
exit-code: 1 exit-code: 1
- name: Build free-threaded amd64 candidate - name: Build free-threaded amd64 candidate
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -242,10 +240,10 @@ jobs:
cache-to: type=gha,scope=moviepilot-v3t-docker-amd64,mode=max,version=2 cache-to: type=gha,scope=moviepilot-v3t-docker-amd64,mode=max,version=2
- name: Scan free-threaded amd64 candidate vulnerabilities - name: Scan free-threaded amd64 candidate vulnerabilities
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 uses: aquasecurity/trivy-action@v0.36.0
with: with:
image-ref: moviepilot-v3t-candidate:linux-amd64 image-ref: moviepilot-v3t-candidate:linux-amd64
version: v0.70.0 version: latest
cache-dir: ${{ runner.temp }}/trivy cache-dir: ${{ runner.temp }}/trivy
scanners: vuln scanners: vuln
vuln-type: os,library vuln-type: os,library
@@ -255,7 +253,7 @@ jobs:
exit-code: 1 exit-code: 1
- name: Build free-threaded arm64 candidate - name: Build free-threaded arm64 candidate
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -274,10 +272,10 @@ jobs:
cache-to: type=gha,scope=moviepilot-v3t-docker-arm64,mode=max,version=2 cache-to: type=gha,scope=moviepilot-v3t-docker-arm64,mode=max,version=2
- name: Scan free-threaded arm64 candidate vulnerabilities - name: Scan free-threaded arm64 candidate vulnerabilities
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 uses: aquasecurity/trivy-action@v0.36.0
with: with:
image-ref: moviepilot-v3t-candidate:linux-arm64 image-ref: moviepilot-v3t-candidate:linux-arm64
version: v0.70.0 version: latest
cache-dir: ${{ runner.temp }}/trivy cache-dir: ${{ runner.temp }}/trivy
scanners: vuln scanners: vuln
vuln-type: os,library vuln-type: os,library
@@ -287,20 +285,20 @@ jobs:
exit-code: 1 exit-code: 1
- name: Login DockerHub - name: Login DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login GitHub Container Registry - name: Login GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish multi-architecture image - name: Publish multi-architecture image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -332,7 +330,7 @@ jobs:
type=gha,scope=moviepilot-v3-standard-docker-arm64,version=2 type=gha,scope=moviepilot-v3-standard-docker-arm64,version=2
- name: Publish free-threaded multi-architecture image - name: Publish free-threaded multi-architecture image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -481,52 +479,77 @@ jobs:
- name: Get existing release body - name: Get existing release body
id: get_release_body id: get_release_body
continue-on-error: true
env: env:
CHANGELOG: ${{ env.CHANGELOG }} CHANGELOG: ${{ env.CHANGELOG }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
release_body=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ tag_name="v${{ env.app_version }}"
"https://api.github.com/repos/${{ github.repository }}/releases/tags/v${{ env.app_version }}" | \ response_file=$(mktemp)
jq -r '.body // ""') error_file=$(mktemp)
trap 'rm -f "$response_file" "$error_file"' EXIT
if gh api --include \
"repos/${GITHUB_REPOSITORY}/releases/tags/${tag_name}" \
--jq '.body' > "$response_file" 2> "$error_file"; then
release_exists=true
release_body=$(awk 'body { sub(/\r$/, ""); print } /^[[:space:]]*$/ { body=1 }' "$response_file")
else
status_code=$(awk 'NR == 1 { print $2 }' "$response_file")
if [ "$status_code" = "404" ]; then
release_exists=false
release_body=""
else
cat "$error_file" >&2
exit 1
fi
fi
echo "exists=$release_exists" >> "$GITHUB_OUTPUT"
# 如果已有手动编写的 release body,则保留;否则使用自动生成的 changelog # 如果已有手动编写的 release body,则保留;否则使用自动生成的 changelog
if [ -n "$release_body" ] && [ "$release_body" != "null" ] && [ "$release_body" != "" ]; then if [ -n "$release_body" ] && [ "$release_body" != "null" ] && [ "$release_body" != "" ]; then
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV {
echo "$release_body" >> $GITHUB_ENV echo "RELEASE_BODY<<EOF"
echo "EOF" >> $GITHUB_ENV echo "$release_body"
echo "EOF"
} >> "$GITHUB_ENV"
else else
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV {
echo "$CHANGELOG" >> $GITHUB_ENV echo "RELEASE_BODY<<EOF"
echo "EOF" >> $GITHUB_ENV echo "$CHANGELOG"
echo "EOF"
} >> "$GITHUB_ENV"
fi 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 - name: Publish Release Tag
env: env:
RELEASE_COMMIT: ${{ steps.release_snapshot.outputs.release_commit }} RELEASE_COMMIT: ${{ steps.release_snapshot.outputs.release_commit }}
run: | run: |
tag_name="v${{ env.app_version }}" tag_name="v${{ env.app_version }}"
if git show-ref --verify --quiet "refs/tags/${tag_name}"; then git tag -f "$tag_name" "$RELEASE_COMMIT"
git tag -d "$tag_name" git push --force origin "refs/tags/${tag_name}"
fi
git tag "$tag_name" "$RELEASE_COMMIT"
git push origin "refs/tags/${tag_name}"
- name: Generate Release - name: Publish 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: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_BODY: ${{ env.RELEASE_BODY }}
RELEASE_EXISTS: ${{ steps.get_release_body.outputs.exists }}
run: |
tag_name="v${{ env.app_version }}"
notes_file=$(mktemp)
trap 'rm -f "$notes_file"' EXIT
printf '%s\n' "$RELEASE_BODY" > "$notes_file"
if [ "$RELEASE_EXISTS" = "true" ]; then
gh release edit "$tag_name" \
--repo "$GITHUB_REPOSITORY" \
--title "$tag_name" \
--notes-file "$notes_file" \
--draft=false \
--prerelease=false \
--latest
else
gh release create "$tag_name" \
--repo "$GITHUB_REPOSITORY" \
--title "$tag_name" \
--notes-file "$notes_file" \
--latest
fi
+13 -8
View File
@@ -71,12 +71,11 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v7
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@v10.0.1
with: with:
version: '0.12.5'
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
enable-cache: true enable-cache: true
cache-dependency-glob: | cache-dependency-glob: |
@@ -135,13 +134,13 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v7
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 uses: docker/setup-buildx-action@v4
- name: Build locked dependency stage - name: Build locked dependency stage
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: docker/Dockerfile file: docker/Dockerfile
@@ -177,7 +176,13 @@ jobs:
assert not expected_free_threaded or callable(moviepilot_rust.zhconv_fast); assert not expected_free_threaded or callable(moviepilot_rust.zhconv_fast);
assert sys._is_gil_enabled() == (not expected_free_threaded)" assert sys._is_gil_enabled() == (not expected_free_threaded)"
- name: Verify pinned uv version - name: Verify minimum uv version
env: env:
IMAGE_TAG: ${{ matrix.image-tag }} IMAGE_TAG: ${{ matrix.image-tag }}
run: docker run --rm "${IMAGE_TAG}" uv --version | grep -F 'uv 0.12.5' run: >-
docker run --rm "${IMAGE_TAG}" /opt/venv/bin/python -c
"from packaging.version import Version;
import subprocess;
version = subprocess.check_output(['uv', '--version'], text=True).split()[1];
assert Version(version) >= Version('0.12.5'), version;
print(version)"
+3 -3
View File
@@ -17,7 +17,7 @@ jobs:
permissions: permissions:
issues: write issues: write
steps: steps:
- uses: actions/github-script@v7 - uses: actions/github-script@v9
with: with:
script: | script: |
const issue = context.payload.issue; const issue = context.payload.issue;
@@ -74,7 +74,7 @@ jobs:
permissions: permissions:
issues: write issues: write
steps: steps:
- uses: actions/github-script@v7 - uses: actions/github-script@v9
with: with:
script: | script: |
const labelRules = [ const labelRules = [
@@ -139,7 +139,7 @@ jobs:
issues: write issues: write
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/stale@v5 - uses: actions/stale@v11
with: with:
# 标记 stale 标签时间 # 标记 stale 标签时间
days-before-issue-stale: 30 days-before-issue-stale: 30
+2 -3
View File
@@ -23,14 +23,13 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@v10.0.1
with: with:
version: '0.12.5'
python-version: '3.14' python-version: '3.14'
enable-cache: true enable-cache: true
cache-dependency-glob: | cache-dependency-glob: |
+2 -3
View File
@@ -47,14 +47,13 @@ jobs:
uses: actions/checkout@v7 uses: actions/checkout@v7
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v7
with: with:
python-version: '3.14' python-version: '3.14'
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@v10.0.1
with: with:
version: '0.12.5'
enable-cache: true enable-cache: true
cache-dependency-glob: scripts/site_adapter_collector_requirements.txt cache-dependency-glob: scripts/site_adapter_collector_requirements.txt
+6 -9
View File
@@ -26,12 +26,11 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v7
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@v10.0.1
with: with:
version: '0.12.5'
python-version: '3.14' python-version: '3.14'
enable-cache: true enable-cache: true
cache-dependency-glob: | cache-dependency-glob: |
@@ -101,12 +100,11 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v7
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@v10.0.1
with: with:
version: '0.12.5'
python-version: '3.14' python-version: '3.14'
enable-cache: true enable-cache: true
cache-dependency-glob: | cache-dependency-glob: |
@@ -127,12 +125,11 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v7
- name: Set up uv - name: Set up uv
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 uses: astral-sh/setup-uv@v10.0.1
with: with:
version: '0.12.5'
python-version: '3.14' python-version: '3.14'
enable-cache: true enable-cache: true
cache-dependency-glob: | cache-dependency-glob: |
+1 -1
View File
@@ -3,7 +3,7 @@
ARG MOVIEPILOT_PYTHON_VARIANT="standard" ARG MOVIEPILOT_PYTHON_VARIANT="standard"
ARG MOVIEPILOT_PYTHON_VERSION="3.14.7" ARG MOVIEPILOT_PYTHON_VERSION="3.14.7"
FROM ghcr.io/astral-sh/uv:0.12.5@sha256:e85be844203885286c60ffad8a858d48afb6c5a5c237ca0e67f12e74b8f174b1 AS uv FROM ghcr.io/astral-sh/uv:latest AS uv
FROM rclone/rclone:1.75.0@sha256:b06aed988cf5967de7c25be5925240983981c757f4ed1ac9d2fa659d51d60548 AS rclone FROM rclone/rclone:1.75.0@sha256:b06aed988cf5967de7c25be5925240983981c757f4ed1ac9d2fa659d51d60548 AS rclone
+1 -1
View File
@@ -78,7 +78,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
| Python 源码量 | 约 271,400 行 | 60 个文件超过 1,000 行,14 个超过 2,000 行 | | Python 源码量 | 约 271,400 行 | 60 个文件超过 1,000 行,14 个超过 2,000 行 |
| 长方法 | 281 个超过 80 行 | 67 个超过 150 行,23 个超过 250 行;大量是私有方法 | | 长方法 | 281 个超过 80 行 | 67 个超过 150 行,23 个超过 250 行;大量是私有方法 |
| 全量 mypy 历史债务 | 11,808 / 596 文件 | strict frontier 当前覆盖 41 个文件,本批迁移路径的类型债务已清零 | | 全量 mypy 历史债务 | 11,808 / 596 文件 | strict frontier 当前覆盖 41 个文件,本批迁移路径的类型债务已清零 |
| Ruff 历史诊断 | 841 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` | | Ruff 历史诊断 | 840 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
| 覆盖率低水位 | Application 79.39%Domain 79.29% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 | | 覆盖率低水位 | Application 79.39%Domain 79.29% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |
### 3.3 热点文件 ### 3.3 热点文件
+1 -1
View File
@@ -153,7 +153,7 @@ canonical 主程序;兼容只经统一 Compat/SDK 门面提供。
| S4-L2 Event strict contract | `PLANNED` | S0-L2.6,S1-L6 | 宿主事件输入/输出按风险 strict,诊断例外只属于第三方插件兼容 | | S4-L2 Event strict contract | `PLANNED` | S0-L2.6,S1-L6 | 宿主事件输入/输出按风险 strict,诊断例外只属于第三方插件兼容 |
| S4-L3 Complexity v2 | `PLANNED` | S3 | 私有方法、class/file、圈复杂度进入门禁;所有超限通过职责拆分归零 | | S4-L3 Complexity v2 | `PLANNED` | S3 | 私有方法、class/file、圈复杂度进入门禁;所有超限通过职责拆分归零 |
| S4-L4 全量 mypy 清零 | `PLANNED` | S3,S4-L1,S4-L2 | `mypy-baseline.json` 归零并删除债务接受路径,全宿主 strict 类型通过 | | S4-L4 全量 mypy 清零 | `PLANNED` | S3,S4-L1,S4-L2 | `mypy-baseline.json` 归零并删除债务接受路径,全宿主 strict 类型通过 |
| S4-L5 Ruff 治理债务清零 | `PLANNED` | S3 | 当前受控 841 条诊断归零,规则集扩展经过独立审查且新增诊断为零 | | S4-L5 Ruff 治理债务清零 | `PLANNED` | S3 | 当前受控 840 条诊断归零,规则集扩展经过独立审查且新增诊断为零 |
| S4-L6 Coverage/并发/质量证据 | `PLANNED` | S3,S4-L1,S4-L2 | 高风险包纳入 coverageraw concurrency 分类清零;Module Quality 有真实 evidence test | | S4-L6 Coverage/并发/质量证据 | `PLANNED` | S3,S4-L1,S4-L2 | 高风险包纳入 coverageraw concurrency 分类清零;Module Quality 有真实 evidence test |
### S5Plugin、Agent、Domain、Startup 与最终收口 ### S5Plugin、Agent、Domain、Startup 与最终收口
+3 -3
View File
@@ -11,7 +11,7 @@ curl -fsSL https://raw.githubusercontent.com/jxxghp/MoviePilot/v3/scripts/bootst
脚本会自动: 脚本会自动:
- 检测操作系统 - 检测操作系统
- 自动检查并尽量安装 `git``curl``uv 0.12.5``Python 3.14+` - 自动检查并尽量安装 `git``curl``uv 0.12.5+``Python 3.14+`
- 克隆 `MoviePilot` - 克隆 `MoviePilot`
- 安装后端依赖 - 安装后端依赖
- 按当前仓库 `version.py` 中的 `FRONTEND_VERSION` 下载对应前端 release 的 `dist.zip` - 按当前仓库 `version.py` 中的 `FRONTEND_VERSION` 下载对应前端 release 的 `dist.zip`
@@ -25,7 +25,7 @@ curl -fsSL https://raw.githubusercontent.com/jxxghp/MoviePilot/v3/scripts/bootst
说明: 说明:
- 如果系统里已经有可用的 `Python 3.14+`,脚本会优先直接复用本地解释器 - 如果系统里已经有可用的 `Python 3.14+`,脚本会优先直接复用本地解释器
- 如果系统里没有可用解释器,脚本会通过固定版本的 uv 安装 Python 3.14 - 如果系统里没有可用解释器,脚本会通过最新稳定版 uv 安装 Python 3.14
- Linux 下安装系统依赖时通常需要 `sudo` - Linux 下安装系统依赖时通常需要 `sudo`
- 复用已有仓库时,脚本现在只会因为已跟踪源码改动而阻止自动更新,不会再被 `.DS_Store` 之类未跟踪文件卡住 - 复用已有仓库时,脚本现在只会因为已跟踪源码改动而阻止自动更新,不会再被 `.DS_Store` 之类未跟踪文件卡住
@@ -165,7 +165,7 @@ moviepilot install deps --config-dir /path/to/moviepilot-config
说明: 说明:
- 默认会自动选择本地已安装的 `Python 3.14+` 解释器 - 默认会自动选择本地已安装的 `Python 3.14+` 解释器
- 安装器要求 `uv 0.12.5`,并按仓库提交的 `uv.lock` 同步依赖;不会在本地重新解析一套未锁定结果 - 安装器要求 `uv 0.12.5+`,并按仓库提交的 `uv.lock` 同步依赖;不会在本地重新解析一套未锁定结果
- `moviepilot_rust` 加速扩展通过 `moviepilot-rust` PyPI 依赖安装,主项目本地安装不需要 Rust toolchain - `moviepilot_rust` 加速扩展通过 `moviepilot-rust` PyPI 依赖安装,主项目本地安装不需要 Rust toolchain
- 安装完成后可在前端“高级设置 - 实验室”中关闭或重新开启 Rust 加速;如果后端未加载扩展,该开关会保持关闭且不可操作 - 安装完成后可在前端“高级设置 - 实验室”中关闭或重新开启 Rust 加速;如果后端未加载扩展,该开关会保持关闭且不可操作
+4 -4
View File
@@ -7,7 +7,7 @@
在开始之前,请确保您的系统已安装以下软件: 在开始之前,请确保您的系统已安装以下软件:
- **Python 3.14+** - **Python 3.14+**
- **uv 0.12.5**Python 版本、虚拟环境和依赖锁定工具) - **uv 0.12.5+**(Python 版本、虚拟环境和依赖锁定工具,推荐使用最新稳定版
- **Git** (用于版本控制) - **Git** (用于版本控制)
- **RAR 解压工具**:本地开发如需测试或使用 `.rar` 字幕包解压,请安装 `unar``unrar``7z``bsdtar` 之一;Docker 镜像会内置 `unar` - **RAR 解压工具**:本地开发如需测试或使用 `.rar` 字幕包解压,请安装 `unar``unrar``7z``bsdtar` 之一;Docker 镜像会内置 `unar`
@@ -90,7 +90,7 @@ chmod +x scripts/start-local.sh
1. **共享运行时依赖**:被 `app/` 生产代码直接导入,或是生产功能、后台任务、插件框架启动必需,写入 `[project].dependencies` 1. **共享运行时依赖**:被 `app/` 生产代码直接导入,或是生产功能、后台任务、插件框架启动必需,写入 `[project].dependencies`
2. **ABI 敏感运行依赖**:标准与 free-threaded 解释器必须选择不同制品或版本时,分别写入 `runtime-standard``runtime-free-threaded`;两组保持互斥并由运行时统一选择。 2. **ABI 敏感运行依赖**:标准与 free-threaded 解释器必须选择不同制品或版本时,分别写入 `runtime-standard``runtime-free-threaded`;两组保持互斥并由运行时统一选择。
3. **开发 / 测试 / 静态检查 / 构建依赖**:只用于单测、覆盖率、lint 辅助、源码构建等,写入 `[dependency-groups].dev` 3. **开发 / 测试 / 静态检查 / 构建依赖**:只用于单测、覆盖率、lint 辅助、源码构建等,写入 `[dependency-groups].dev`
4. **工具依赖**:仓库要求使用 `uv 0.12.5`;不应为了安装工具而把它加入主程序运行依赖。 4. **工具依赖**:仓库要求使用 `uv 0.12.5+`,推荐使用最新稳定版;不应为了安装工具而把它加入主程序运行依赖。
5. **插件依赖**:由插件清单声明并在插件安装阶段处理,不直接并入主程序依赖。 5. **插件依赖**:由插件清单声明并在插件安装阶段处理,不直接并入主程序依赖。
修改后更新并校验锁文件: 修改后更新并校验锁文件:
@@ -169,13 +169,13 @@ python -m scripts.generate_plugin_market_default \
### 5. 运行依赖漏洞检查 ### 5. 运行依赖漏洞检查
正式发布会使用固定版本的 `pip-audit` 检查 `uv.lock` 锁定的运行时依赖。依赖变更后也可以在 正式发布会使用最新稳定版 `pip-audit` 检查 `uv.lock` 锁定的运行时依赖。依赖变更后也可以在
本地执行同一检查: 本地执行同一检查:
```bash ```bash
uv export --quiet --locked --no-dev --no-emit-project \ uv export --quiet --locked --no-dev --no-emit-project \
--output-file /tmp/moviepilot-audit-requirements.txt --output-file /tmp/moviepilot-audit-requirements.txt
uvx --from pip-audit==2.10.1 pip-audit \ uvx --from pip-audit pip-audit \
--require-hashes --disable-pip --strict --progress-spinner off \ --require-hashes --disable-pip --strict --progress-spinner off \
--requirement /tmp/moviepilot-audit-requirements.txt --requirement /tmp/moviepilot-audit-requirements.txt
``` ```
+2 -2
View File
@@ -107,7 +107,7 @@
|---|---| |---|---|
| Project metadata | `pyproject.toml` — runtime dependencies in `[project].dependencies`, development tooling in `[dependency-groups].dev` | | Project metadata | `pyproject.toml` — runtime dependencies in `[project].dependencies`, development tooling in `[dependency-groups].dev` |
| Lock | `uv.lock` — committed resolution for Python 3.14+ and supported platforms | | Lock | `uv.lock` — committed resolution for Python 3.14+ and supported platforms |
| Package manager | uv 0.12.5 | | Package manager | uv 0.12.5+(推荐最新稳定版) |
| Runtime install | `uv sync --locked --no-dev --no-install-project` | | Runtime install | `uv sync --locked --no-dev --no-install-project` |
| Dev/test/lint/build install | `uv sync --locked` | | Dev/test/lint/build install | `uv sync --locked` |
| Supported platforms | Linux x86_64/arm64, macOS x86_64/arm64, Windows x64 | | Supported platforms | Linux x86_64/arm64, macOS x86_64/arm64, Windows x64 |
@@ -132,7 +132,7 @@
| pytest | Test runner | `uv run --locked --no-sync pytest tests/test_xxx.py` | | pytest | Test runner | `uv run --locked --no-sync pytest tests/test_xxx.py` |
| pylint | Static analysis | `uv run --locked --no-sync pylint app/` | | pylint | Static analysis | `uv run --locked --no-sync pylint app/` |
| uv | Lock and environment consistency | `uv lock --check && uv sync --locked --offline --inexact --no-dev --check` | | uv | Lock and environment consistency | `uv lock --check && uv sync --locked --offline --inexact --no-dev --check` |
| pip-audit | Locked dependency vulnerability scan | `uv export --quiet --locked --no-dev --no-emit-project -o /tmp/moviepilot-audit-requirements.txt && uvx --from pip-audit==2.10.1 pip-audit --require-hashes --disable-pip --strict --progress-spinner off -r /tmp/moviepilot-audit-requirements.txt` | | pip-audit | Locked dependency vulnerability scan | `uv export --quiet --locked --no-dev --no-emit-project -o /tmp/moviepilot-audit-requirements.txt && uvx --from pip-audit pip-audit --require-hashes --disable-pip --strict --progress-spinner off -r /tmp/moviepilot-audit-requirements.txt` |
--- ---
+2 -2
View File
@@ -34,7 +34,7 @@ uv sync --locked --offline --inexact --no-dev --check
- Test, coverage, lint, and explicit build tooling belong in `[dependency-groups].dev`. - Test, coverage, lint, and explicit build tooling belong in `[dependency-groups].dev`.
- Commit the updated `uv.lock`; do not maintain or generate main-program requirements files. - Commit the updated `uv.lock`; do not maintain or generate main-program requirements files.
- `uv pip check` is diagnostic only because unmaintained third-party metadata may name a compatible superseded distribution. - `uv pip check` is diagnostic only because unmaintained third-party metadata may name a compatible superseded distribution.
- Use uv 0.12.5 and Python 3.14+. - Use uv 0.12.5+ and Python 3.14+; prefer the latest stable uv release.
--- ---
@@ -82,7 +82,7 @@ uv run --locked --no-sync pylint app/chain/download.py
```bash ```bash
uv export --quiet --locked --no-dev --no-emit-project \ uv export --quiet --locked --no-dev --no-emit-project \
--output-file /tmp/moviepilot-audit-requirements.txt --output-file /tmp/moviepilot-audit-requirements.txt
uvx --from pip-audit==2.10.1 pip-audit \ uvx --from pip-audit pip-audit \
--require-hashes --disable-pip --strict --progress-spinner off \ --require-hashes --disable-pip --strict --progress-spinner off \
--requirement /tmp/moviepilot-audit-requirements.txt --requirement /tmp/moviepilot-audit-requirements.txt
``` ```
+1 -1
View File
@@ -56,7 +56,7 @@ uv run --locked --no-sync pylint app/
```bash ```bash
uv export --quiet --locked --no-dev --no-emit-project \ uv export --quiet --locked --no-dev --no-emit-project \
--output-file /tmp/moviepilot-audit-requirements.txt --output-file /tmp/moviepilot-audit-requirements.txt
uvx --from pip-audit==2.10.1 pip-audit \ uvx --from pip-audit pip-audit \
--require-hashes --disable-pip --strict --progress-spinner off \ --require-hashes --disable-pip --strict --progress-spinner off \
--requirement /tmp/moviepilot-audit-requirements.txt --requirement /tmp/moviepilot-audit-requirements.txt
``` ```
+1 -1
View File
@@ -135,7 +135,7 @@ select = ["E4", "E7", "E9", "F", "I"]
[tool.uv] [tool.uv]
package = false package = false
required-version = "==0.12.5" required-version = ">=0.12.5"
default-groups = ["dev", "runtime-standard"] default-groups = ["dev", "runtime-standard"]
conflicts = [ conflicts = [
[ [
+29 -6
View File
@@ -16,7 +16,7 @@ SUPERUSER=""
SUPERUSER_PASSWORD="" SUPERUSER_PASSWORD=""
OS_NAME="Unknown" OS_NAME="Unknown"
PYTHON_BIN="" PYTHON_BIN=""
UV_VERSION="0.12.5" MIN_UV_VERSION="0.12.5"
BREW_BIN="" BREW_BIN=""
PACKAGE_MANAGER="" PACKAGE_MANAGER=""
PACKAGE_INDEX_UPDATED="false" PACKAGE_INDEX_UPDATED="false"
@@ -384,20 +384,43 @@ ensure_base_tools() {
fi fi
} }
uv_version_supported() {
local actual="$1"
local minimum="$2"
local -a actual_parts minimum_parts
local index actual_part minimum_part
IFS=. read -r -a actual_parts <<< "$actual"
IFS=. read -r -a minimum_parts <<< "$minimum"
for index in 0 1 2; do
actual_part="${actual_parts[$index]:-0}"
minimum_part="${minimum_parts[$index]:-0}"
[[ "$actual_part" =~ ^[0-9]+$ ]] || return 1
[[ "$minimum_part" =~ ^[0-9]+$ ]] || return 1
if (( 10#$actual_part > 10#$minimum_part )); then
return 0
fi
if (( 10#$actual_part < 10#$minimum_part )); then
return 1
fi
done
return 0
}
ensure_uv() { ensure_uv() {
if command -v uv >/dev/null 2>&1 \ if command -v uv >/dev/null 2>&1 \
&& [[ "$(uv --version 2>/dev/null | awk '{print $2}')" == "${UV_VERSION}" ]]; then && uv_version_supported "$(uv --version 2>/dev/null | awk '{print $2}')" "${MIN_UV_VERSION}"; then
return 0 return 0
fi fi
echo "==> 自动安装 uv ${UV_VERSION}" echo "==> 自动安装最新稳定版 uv"
env UV_INSTALL_DIR="$HOME/.local/bin" sh -c "$(curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh")" env UV_INSTALL_DIR="$HOME/.local/bin" sh -c "$(curl -LsSf https://astral.sh/uv/install.sh)"
export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.local/bin:$PATH"
hash -r hash -r
if ! command -v uv >/dev/null 2>&1 \ if ! command -v uv >/dev/null 2>&1 \
|| [[ "$(uv --version 2>/dev/null | awk '{print $2}')" != "${UV_VERSION}" ]]; then || ! uv_version_supported "$(uv --version 2>/dev/null | awk '{print $2}')" "${MIN_UV_VERSION}"; then
echo "uv ${UV_VERSION} 安装失败,无法继续自动安装 Python。" >&2 echo "uv ${MIN_UV_VERSION}+ 安装失败,无法继续自动安装 Python。" >&2
return 1 return 1
fi fi
} }
+16 -6
View File
@@ -9,8 +9,8 @@ import importlib.util
import json import json
import os import os
import platform import platform
import secrets
import re import re
import secrets
import shlex import shlex
import shutil import shutil
import subprocess import subprocess
@@ -36,7 +36,8 @@ NODE_DIR = RUNTIME_DIR / "node"
INSTALL_ENV_FILE = ROOT / ".moviepilot.env" INSTALL_ENV_FILE = ROOT / ".moviepilot.env"
MIN_PYTHON_VERSION = (3, 14) MIN_PYTHON_VERSION = (3, 14)
SUPPORTED_PYTHON_TEXT = "Python 3.14+" SUPPORTED_PYTHON_TEXT = "Python 3.14+"
UV_VERSION = "0.12.5" MIN_UV_VERSION = (0, 12, 5)
MIN_UV_VERSION_TEXT = ".".join(str(part) for part in MIN_UV_VERSION)
CONFIG_DIR = LEGACY_CONFIG_DIR CONFIG_DIR = LEGACY_CONFIG_DIR
LOG_DIR = CONFIG_DIR / "logs" LOG_DIR = CONFIG_DIR / "logs"
@@ -618,18 +619,27 @@ def get_venv_bin_dir(venv_dir: Path) -> Path:
return venv_dir / "bin" return venv_dir / "bin"
def parse_uv_version(output: str) -> tuple[int, int, int] | None:
"""从 uv 版本输出中提取稳定版三段版本号。"""
match = re.match(r"^uv\s+(\d+)\.(\d+)\.(\d+)(?:\s|$)", output.strip())
if not match:
return None
return tuple(int(part) for part in match.groups())
def require_uv() -> Path: def require_uv() -> Path:
"""返回仓库要求版本的 uv,避免不同安装入口使用不同解析器""" """返回满足仓库最低版本要求的 uv"""
uv_command = shutil.which("uv") uv_command = shutil.which("uv")
if not uv_command: if not uv_command:
raise RuntimeError( raise RuntimeError(
f"未找到 uv {UV_VERSION},请先安装后重新执行。" f"未找到 uv {MIN_UV_VERSION_TEXT}+,请先安装后重新执行。"
) )
uv_bin = Path(uv_command).expanduser().resolve() uv_bin = Path(uv_command).expanduser().resolve()
version = capture([str(uv_bin), "--version"]) version = capture([str(uv_bin), "--version"])
if version.split()[:2] != ["uv", UV_VERSION]: parsed_version = parse_uv_version(version)
if parsed_version is None or parsed_version < MIN_UV_VERSION:
raise RuntimeError( raise RuntimeError(
f"MoviePilot 需要 uv {UV_VERSION},当前为 {version or '未知版本'}" f"MoviePilot 需要 uv {MIN_UV_VERSION_TEXT}+,当前为 {version or '未知版本'}"
) )
return uv_bin return uv_bin
-3
View File
@@ -1484,9 +1484,6 @@
"tests/test_release_group.py": { "tests/test_release_group.py": {
"I001": 1 "I001": 1
}, },
"tests/test_release_supply_chain.py": {
"I001": 1
},
"tests/test_reliability_adr.py": { "tests/test_reliability_adr.py": {
"I001": 1 "I001": 1
}, },
+1 -5
View File
@@ -30,11 +30,7 @@ def _write_bundle(path: Path, label: str, *, extra_files: tuple[str, ...] = ())
def test_dockerfile_control_bundle_build_checks_fail_closed() -> None: def test_dockerfile_control_bundle_build_checks_fail_closed() -> None:
dockerfile = (ROOT / "docker" / "Dockerfile").read_text(encoding="utf-8") dockerfile = (ROOT / "docker" / "Dockerfile").read_text(encoding="utf-8")
assert ( assert "FROM ghcr.io/astral-sh/uv:latest AS uv" in dockerfile
"FROM ghcr.io/astral-sh/uv:0.12.5@sha256:"
"e85be844203885286c60ffad8a858d48afb6c5a5c237ca0e67f12e74b8f174b1 AS uv"
in dockerfile
)
assert "COPY --from=uv /uv /usr/local/bin/uv" in dockerfile assert "COPY --from=uv /uv /usr/local/bin/uv" in dockerfile
assert "COPY pyproject.toml uv.lock ./" in dockerfile assert "COPY pyproject.toml uv.lock ./" in dockerfile
assert "python3 -m venv --without-pip ${VENV_PATH}" in dockerfile assert "python3 -m venv --without-pip ${VENV_PATH}" in dockerfile
+22 -2
View File
@@ -217,17 +217,37 @@ class LocalSetupConfigDirTests(unittest.TestCase):
self.assertIn("https://mirror.example/simple?token=abc", redacted) self.assertIn("https://mirror.example/simple?token=abc", redacted)
self.assertNotIn("user:pass", " ".join(redacted)) self.assertNotIn("user:pass", " ".join(redacted))
def test_require_uv_accepts_repository_version(self): def test_require_uv_accepts_minimum_version(self):
module = load_local_setup_module() module = load_local_setup_module()
uv_bin = Path("/opt/moviepilot/bin/uv") uv_bin = Path("/opt/moviepilot/bin/uv")
with patch.object(module.shutil, "which", return_value=str(uv_bin)), patch.object( with patch.object(module.shutil, "which", return_value=str(uv_bin)), patch.object(
module, "capture", return_value=f"uv {module.UV_VERSION} (test-target)" module, "capture", return_value=f"uv {module.MIN_UV_VERSION_TEXT} (test-target)"
): ):
result = module.require_uv() result = module.require_uv()
self.assertEqual(result, uv_bin.resolve()) self.assertEqual(result, uv_bin.resolve())
def test_require_uv_accepts_newer_version(self):
module = load_local_setup_module()
uv_bin = Path("/opt/moviepilot/bin/uv")
with patch.object(module.shutil, "which", return_value=str(uv_bin)), patch.object(
module, "capture", return_value="uv 1.0.0 (test-target)"
):
result = module.require_uv()
self.assertEqual(result, uv_bin.resolve())
def test_require_uv_rejects_older_version(self):
module = load_local_setup_module()
uv_bin = Path("/opt/moviepilot/bin/uv")
with patch.object(module.shutil, "which", return_value=str(uv_bin)), patch.object(
module, "capture", return_value="uv 0.12.4 (test-target)"
), self.assertRaisesRegex(RuntimeError, "需要 uv 0.12.5\\+"):
module.require_uv()
def test_windows_expose_uv_keeps_existing_source_when_target_is_same(self): def test_windows_expose_uv_keeps_existing_source_when_target_is_same(self):
module = load_local_setup_module() module = load_local_setup_module()
+238 -7
View File
@@ -1,5 +1,8 @@
"""正式镜像发布的供应链门禁合同。""" """正式镜像发布的供应链门禁合同。"""
import os
import shutil
import subprocess
from datetime import date from datetime import date
from pathlib import Path from pathlib import Path
@@ -8,12 +11,30 @@ from ruamel.yaml import YAML
from scripts.normalize_audit_requirements import normalize_requirements from scripts.normalize_audit_requirements import normalize_requirements
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
DOCKERFILE = ROOT / "docker" / "Dockerfile" DOCKERFILE = ROOT / "docker" / "Dockerfile"
RELEASE_WORKFLOW = ROOT / ".github" / "workflows" / "build-v3.yml" RELEASE_WORKFLOW = ROOT / ".github" / "workflows" / "build-v3.yml"
BETA_WORKFLOW = ROOT / ".github" / "workflows" / "beta.yml" BETA_WORKFLOW = ROOT / ".github" / "workflows" / "beta.yml"
PR_AGENT_WORKFLOW = ROOT / ".github" / "workflows" / "pr-agent.yml"
TRIVY_IGNORE = ROOT / ".trivyignore.yaml" TRIVY_IGNORE = ROOT / ".trivyignore.yaml"
WORKFLOW_ROOT = ROOT / ".github" / "workflows"
ALLOWED_ACTION_REFS = {
"actions/checkout@v7",
"actions/setup-python@v7",
"actions/github-script@v9",
"actions/stale@v11",
"astral-sh/setup-uv@v10.0.1",
"docker/metadata-action@v6",
"docker/setup-qemu-action@v4",
"docker/setup-buildx-action@v4",
"docker/build-push-action@v7",
"docker/login-action@v4",
"aquasecurity/trivy-action@v0.36.0",
"actions/upload-artifact@v7",
"actions/download-artifact@v8",
"docker://ghcr.io/infinitypacer/pr-review-runner:latest",
}
def _load_workflow(path: Path = RELEASE_WORKFLOW) -> dict: def _load_workflow(path: Path = RELEASE_WORKFLOW) -> dict:
@@ -31,6 +52,66 @@ def _steps_by_name(workflow: dict) -> dict[str, dict]:
} }
def _write_fake_gh(tmp_path: Path) -> Path:
"""创建可控制响应和退出状态的 gh 测试替身。"""
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
gh = bin_dir / "gh"
gh.write_text(
"""#!/usr/bin/env bash
printf '%s\n' "$*" >> "$GH_LOG"
cat "$GH_RESPONSE_FILE"
cat "$GH_ERROR_FILE" >&2
exit "$GH_EXIT_CODE"
""",
encoding="utf-8",
)
gh.chmod(0o755)
return bin_dir
def _run_release_script(
script: str,
tmp_path: Path,
*,
response: str = "",
error: str = "",
exit_code: int = 0,
extra_env: dict[str, str] | None = None,
) -> subprocess.CompletedProcess[str]:
"""在隔离的 gh 替身环境中执行发布 workflow 脚本。"""
bash = shutil.which("bash")
if bash is None:
pytest.skip("release workflow contract requires Bash")
response_file = tmp_path / "response.txt"
error_file = tmp_path / "error.txt"
response_file.write_text(response, encoding="utf-8")
error_file.write_text(error, encoding="utf-8")
env = os.environ.copy()
env.update(
{
"PATH": f"{_write_fake_gh(tmp_path)}:{env['PATH']}",
"GH_RESPONSE_FILE": str(response_file),
"GH_ERROR_FILE": str(error_file),
"GH_EXIT_CODE": str(exit_code),
"GH_LOG": str(tmp_path / "gh.log"),
"GITHUB_REPOSITORY": "jxxghp/MoviePilot",
"GITHUB_ENV": str(tmp_path / "github.env"),
"GITHUB_OUTPUT": str(tmp_path / "github.output"),
"CHANGELOG": "generated changelog",
}
)
env.update(extra_env or {})
return subprocess.run(
[bash, "-euo", "pipefail", "-c", script],
cwd=tmp_path,
env=env,
capture_output=True,
text=True,
check=False,
)
def test_base_image_uses_refreshable_tag_and_apt_does_not_upgrade_in_place() -> None: def test_base_image_uses_refreshable_tag_and_apt_does_not_upgrade_in_place() -> None:
"""基础镜像允许更新,并仅显式刷新运行时安全包而非整套 Debian。""" """基础镜像允许更新,并仅显式刷新运行时安全包而非整套 Debian。"""
dockerfile = DOCKERFILE.read_text(encoding="utf-8") dockerfile = DOCKERFILE.read_text(encoding="utf-8")
@@ -62,7 +143,8 @@ def test_release_audits_locked_runtime_dependencies_before_building() -> None:
assert "--group runtime-standard" in audit assert "--group runtime-standard" in audit
assert "--group runtime-free-threaded" in audit assert "--group runtime-free-threaded" in audit
assert "scripts/normalize_audit_requirements.py" in audit assert "scripts/normalize_audit_requirements.py" in audit
assert "pip-audit==2.10.1" in audit assert "uvx --from pip-audit pip-audit" in audit
assert "pip-audit==" not in audit
for option in ("--require-hashes", "--no-deps", "--disable-pip", "--strict"): for option in ("--require-hashes", "--no-deps", "--disable-pip", "--strict"):
assert option in audit assert option in audit
@@ -142,12 +224,9 @@ def test_release_scans_both_architectures_before_registry_login_and_publish() ->
): ):
scan = indexed[name] scan = indexed[name]
assert scan["with"]["cache-dir"] == "${{ runner.temp }}/trivy" assert scan["with"]["cache-dir"] == "${{ runner.temp }}/trivy"
assert scan["uses"] == ( assert scan["uses"] == "aquasecurity/trivy-action@v0.36.0"
"aquasecurity/trivy-action@"
"a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8"
)
assert scan["with"].items() >= { assert scan["with"].items() >= {
"version": "v0.70.0", "version": "latest",
"scanners": "vuln", "scanners": "vuln",
"vuln-type": "os,library", "vuln-type": "os,library",
"severity": "HIGH,CRITICAL", "severity": "HIGH,CRITICAL",
@@ -170,6 +249,158 @@ def test_release_scans_both_architectures_before_registry_login_and_publish() ->
assert last_scan < names.index("Publish multi-architecture image") assert last_scan < names.index("Publish multi-architecture image")
assert last_scan < names.index("Publish free-threaded multi-architecture image") assert last_scan < names.index("Publish free-threaded multi-architecture image")
def test_workflows_follow_maintained_action_channels() -> None:
"""官方工具使用批准的稳定引用,不引入未知来源或手工 commit SHA。"""
for workflow_path in sorted(WORKFLOW_ROOT.glob("*.yml")):
workflow = _load_workflow(workflow_path)
for job_name, job in workflow.get("jobs", {}).items():
for step in job.get("steps", []):
uses = step.get("uses")
if uses:
assert uses in ALLOWED_ACTION_REFS, (
f"{workflow_path}:{job_name}:{step.get('name', '<unnamed>')}: {uses}"
)
if uses == "astral-sh/setup-uv@v10.0.1":
assert "version" not in step.get("with", {})
def test_all_workflows_are_valid_yaml() -> None:
"""所有 GitHub Actions 工作流都必须能被 YAML 1.2 解析。"""
for workflow_path in sorted(WORKFLOW_ROOT.glob("*.yml")):
workflow = _load_workflow(workflow_path)
assert isinstance(workflow, dict), workflow_path
assert isinstance(workflow.get("jobs"), dict), workflow_path
def test_pr_agent_keeps_pull_request_target_api_only_boundary() -> None:
"""带凭据的 PR 审查只读 GitHub API,不 checkout 或执行 PR 分支代码。"""
workflow = _load_workflow(PR_AGENT_WORKFLOW)
assert "pull_request_target" in workflow["on"]
assert workflow["permissions"] == {
"contents": "read",
"pull-requests": "write",
"issues": "write",
}
steps = workflow["jobs"]["pr-agent"]["steps"]
assert len(steps) == 1
review_step = steps[0]
assert review_step["uses"] == "docker://ghcr.io/infinitypacer/pr-review-runner:latest"
assert "run" not in review_step
def test_release_uses_github_cli_for_tag_and_release_lifecycle() -> None:
"""正式发布复用 GitHub CLI,并只把明确不存在识别为新 Release。"""
workflow = _load_workflow()
indexed = _steps_by_name(workflow)
serialized = RELEASE_WORKFLOW.read_text(encoding="utf-8")
assert "dev-drprasad/delete-tag-and-release" not in serialized
assert "softprops/action-gh-release" not in serialized
release_query = indexed["Get existing release body"]["run"]
assert "gh api --include" in release_query
assert 'if [ "$status_code" = "404" ]' in release_query
assert "cat \"$error_file\" >&2\n exit 1" in release_query
assert "gh release delete" not in serialized
assert 'git tag -f "$tag_name" "$RELEASE_COMMIT"' in indexed["Publish Release Tag"]["run"]
assert 'git push --force origin "refs/tags/${tag_name}"' in indexed["Publish Release Tag"]["run"]
publish_release = indexed["Publish Release"]["run"]
assert 'if [ "$RELEASE_EXISTS" = "true" ]' in publish_release
assert "gh release edit" in publish_release
assert "gh release create" in publish_release
assert '--notes-file "$notes_file"' in publish_release
assert "--draft=false" in publish_release
assert "--prerelease=false" in publish_release
assert "--latest" in publish_release
names = [step.get("name") for step in workflow["jobs"]["Docker-build"]["steps"]]
assert names.index("Get existing release body") < names.index("Publish Release Tag")
assert names.index("Publish Release Tag") < names.index("Publish Release")
@pytest.mark.parametrize(
("response", "exit_code", "expected_exists", "expected_body"),
[
("HTTP/2.0 200 OK\nHeader: value\n\nmanual body\n", 0, "true", "manual body"),
("HTTP/2.0 404 Not Found\n\n", 1, "false", "generated changelog"),
],
)
def test_release_query_preserves_existing_body_or_handles_explicit_404(
tmp_path: Path,
response: str,
exit_code: int,
expected_exists: str,
expected_body: str,
) -> None:
"""已有 Release 保留正文,只有明确 404 才使用自动变更记录。"""
script = _steps_by_name(_load_workflow())["Get existing release body"]["run"]
script = script.replace("v${{ env.app_version }}", "v3.0.0")
result = _run_release_script(script, tmp_path, response=response, exit_code=exit_code)
assert result.returncode == 0, result.stderr
output = (tmp_path / "github.output").read_text(encoding="utf-8")
environment = (tmp_path / "github.env").read_text(encoding="utf-8")
assert f"exists={expected_exists}" in output
assert expected_body in environment
def test_release_query_fails_closed_on_non_404_error(tmp_path: Path) -> None:
"""网络或服务端错误不得伪装成 Release 不存在。"""
script = _steps_by_name(_load_workflow())["Get existing release body"]["run"]
script = script.replace("v${{ env.app_version }}", "v3.0.0")
result = _run_release_script(
script,
tmp_path,
response="HTTP/2.0 500 Internal Server Error\n\n",
error="GitHub API unavailable\n",
exit_code=1,
)
assert result.returncode != 0
assert "GitHub API unavailable" in result.stderr
assert not (tmp_path / "github.env").exists()
@pytest.mark.parametrize(
("release_exists", "expected_command"),
[("true", "release edit"), ("false", "release create")],
)
def test_release_publish_selects_edit_or_create(
tmp_path: Path,
release_exists: str,
expected_command: str,
) -> None:
"""发布阶段按查询结果原位更新或创建 Release。"""
script = _steps_by_name(_load_workflow())["Publish Release"]["run"]
script = script.replace("v${{ env.app_version }}", "v3.0.0")
result = _run_release_script(
script,
tmp_path,
extra_env={"RELEASE_EXISTS": release_exists, "RELEASE_BODY": "release notes"},
)
assert result.returncode == 0, result.stderr
log = (tmp_path / "gh.log").read_text(encoding="utf-8")
assert expected_command in log
if release_exists == "true":
assert "--draft=false" in log
assert "--prerelease=false" in log
def test_dependency_compat_checks_minimum_uv_version() -> None:
"""依赖兼容 job 必须断言 uv 满足最低版本,而不是只打印版本。"""
workflow = _load_workflow(ROOT / ".github" / "workflows" / "dependency-compat.yml")
steps = workflow["jobs"]["docker-dependencies"]["steps"]
verify = next(step for step in steps if step.get("name") == "Verify minimum uv version")
command = verify["run"]
assert "['uv', '--version']" in command
assert "Version(version) >= Version('0.12.5')" in command
assert "assert" in command
def test_vulnerability_ignores_are_scoped_justified_and_time_bounded() -> None: def test_vulnerability_ignores_are_scoped_justified_and_time_bounded() -> None:
"""漏洞豁免必须限定制品范围,并保留复查期限和接受理由。""" """漏洞豁免必须限定制品范围,并保留复查期限和接受理由。"""
yaml = YAML(typ="safe") yaml = YAML(typ="safe")