mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-30 12:36:55 +08:00
Merge pull request #6486 from InfinityPacer/codex/chore/floating-build-toolchain
This commit is contained in:
@@ -20,21 +20,20 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout MoviePilot
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
path: MoviePilot
|
||||
|
||||
- name: Checkout MoviePilot-Plugins
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: jxxghp/MoviePilot-Plugins
|
||||
ref: main
|
||||
path: MoviePilot-Plugins
|
||||
|
||||
- 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'
|
||||
python-version: '3.14'
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
|
||||
+25
-27
@@ -15,12 +15,10 @@ jobs:
|
||||
TRIVY_SKIP_JAVA_DB_UPDATE: "true"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up uv
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
version: '0.12.5'
|
||||
uses: astral-sh/setup-uv@v10.0.1
|
||||
|
||||
- name: Verify dependency lock
|
||||
run: uv lock --check
|
||||
@@ -29,7 +27,7 @@ jobs:
|
||||
run: |
|
||||
uv export --quiet --locked --no-default-groups --group runtime-standard \
|
||||
--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 \
|
||||
--requirement /tmp/moviepilot-audit-standard.txt
|
||||
|
||||
@@ -40,7 +38,7 @@ jobs:
|
||||
--lock uv.lock \
|
||||
--input /tmp/moviepilot-audit-free-threaded.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 \
|
||||
--requirement /tmp/moviepilot-audit-free-threaded-normalized.txt
|
||||
|
||||
@@ -78,7 +76,7 @@ jobs:
|
||||
echo "frontend_sha256=$frontend_sha256" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout Wiki Plugin Market
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: jxxghp/MoviePilot-Wiki
|
||||
ref: main
|
||||
@@ -113,7 +111,7 @@ jobs:
|
||||
|
||||
- name: Docker Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_USERNAME }}/moviepilot-v3
|
||||
@@ -123,7 +121,7 @@ jobs:
|
||||
|
||||
- name: Docker Meta free-threaded
|
||||
id: meta_ft
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_USERNAME }}/moviepilot-v3t
|
||||
@@ -132,13 +130,13 @@ jobs:
|
||||
type=raw,value=beta
|
||||
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set Up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build standard amd64 candidate
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -157,10 +155,10 @@ jobs:
|
||||
cache-to: type=gha,scope=moviepilot-v3-standard-docker-amd64,mode=max,version=2
|
||||
|
||||
- name: Scan standard amd64 candidate vulnerabilities
|
||||
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
with:
|
||||
image-ref: moviepilot-v3-candidate:linux-amd64
|
||||
version: v0.70.0
|
||||
version: latest
|
||||
cache-dir: ${{ runner.temp }}/trivy
|
||||
scanners: vuln
|
||||
vuln-type: os,library
|
||||
@@ -170,7 +168,7 @@ jobs:
|
||||
exit-code: 1
|
||||
|
||||
- name: Build standard arm64 candidate
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -189,10 +187,10 @@ jobs:
|
||||
cache-to: type=gha,scope=moviepilot-v3-standard-docker-arm64,mode=max,version=2
|
||||
|
||||
- name: Scan standard arm64 candidate vulnerabilities
|
||||
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
with:
|
||||
image-ref: moviepilot-v3-candidate:linux-arm64
|
||||
version: v0.70.0
|
||||
version: latest
|
||||
cache-dir: ${{ runner.temp }}/trivy
|
||||
scanners: vuln
|
||||
vuln-type: os,library
|
||||
@@ -202,7 +200,7 @@ jobs:
|
||||
exit-code: 1
|
||||
|
||||
- name: Build free-threaded amd64 candidate
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -221,10 +219,10 @@ jobs:
|
||||
cache-to: type=gha,scope=moviepilot-v3t-docker-amd64,mode=max,version=2
|
||||
|
||||
- name: Scan free-threaded amd64 candidate vulnerabilities
|
||||
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
with:
|
||||
image-ref: moviepilot-v3t-candidate:linux-amd64
|
||||
version: v0.70.0
|
||||
version: latest
|
||||
cache-dir: ${{ runner.temp }}/trivy
|
||||
scanners: vuln
|
||||
vuln-type: os,library
|
||||
@@ -234,7 +232,7 @@ jobs:
|
||||
exit-code: 1
|
||||
|
||||
- name: Build free-threaded arm64 candidate
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -253,10 +251,10 @@ jobs:
|
||||
cache-to: type=gha,scope=moviepilot-v3t-docker-arm64,mode=max,version=2
|
||||
|
||||
- name: Scan free-threaded arm64 candidate vulnerabilities
|
||||
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
with:
|
||||
image-ref: moviepilot-v3t-candidate:linux-arm64
|
||||
version: v0.70.0
|
||||
version: latest
|
||||
cache-dir: ${{ runner.temp }}/trivy
|
||||
scanners: vuln
|
||||
vuln-type: os,library
|
||||
@@ -266,20 +264,20 @@ jobs:
|
||||
exit-code: 1
|
||||
|
||||
- name: Login DockerHub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish standard multi-architecture image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -309,7 +307,7 @@ jobs:
|
||||
type=gha,scope=moviepilot-v3-standard-docker-arm64,version=2
|
||||
|
||||
- name: Publish free-threaded multi-architecture image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
|
||||
@@ -20,15 +20,13 @@ jobs:
|
||||
TRIVY_SKIP_JAVA_DB_UPDATE: "true"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
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'
|
||||
uses: astral-sh/setup-uv@v10.0.1
|
||||
|
||||
- name: Verify dependency lock
|
||||
run: uv lock --check
|
||||
@@ -37,7 +35,7 @@ jobs:
|
||||
run: |
|
||||
uv export --quiet --locked --no-default-groups --group runtime-standard \
|
||||
--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 \
|
||||
--requirement /tmp/moviepilot-audit-standard.txt
|
||||
|
||||
@@ -48,7 +46,7 @@ jobs:
|
||||
--lock uv.lock \
|
||||
--input /tmp/moviepilot-audit-free-threaded.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 \
|
||||
--requirement /tmp/moviepilot-audit-free-threaded-normalized.txt
|
||||
|
||||
@@ -86,7 +84,7 @@ jobs:
|
||||
echo "frontend_sha256=$frontend_sha256" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout Wiki Plugin Market
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
repository: jxxghp/MoviePilot-Wiki
|
||||
ref: main
|
||||
@@ -134,7 +132,7 @@ jobs:
|
||||
|
||||
- name: Docker Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_USERNAME }}/moviepilot-v3
|
||||
@@ -144,7 +142,7 @@ jobs:
|
||||
|
||||
- name: Docker Meta free-threaded
|
||||
id: meta_ft
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_USERNAME }}/moviepilot-v3t
|
||||
@@ -153,13 +151,13 @@ jobs:
|
||||
type=raw,value=${{ env.app_version }}
|
||||
|
||||
- name: Set Up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set Up Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Build amd64 candidate
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -178,10 +176,10 @@ jobs:
|
||||
cache-to: type=gha,scope=moviepilot-v3-standard-docker-amd64,mode=max,version=2
|
||||
|
||||
- name: Scan amd64 candidate vulnerabilities
|
||||
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
with:
|
||||
image-ref: moviepilot-v3-candidate:linux-amd64
|
||||
version: v0.70.0
|
||||
version: latest
|
||||
cache-dir: ${{ runner.temp }}/trivy
|
||||
scanners: vuln
|
||||
vuln-type: os,library
|
||||
@@ -191,7 +189,7 @@ jobs:
|
||||
exit-code: 1
|
||||
|
||||
- name: Build arm64 candidate
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -210,10 +208,10 @@ jobs:
|
||||
cache-to: type=gha,scope=moviepilot-v3-standard-docker-arm64,mode=max,version=2
|
||||
|
||||
- name: Scan arm64 candidate vulnerabilities
|
||||
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
with:
|
||||
image-ref: moviepilot-v3-candidate:linux-arm64
|
||||
version: v0.70.0
|
||||
version: latest
|
||||
cache-dir: ${{ runner.temp }}/trivy
|
||||
scanners: vuln
|
||||
vuln-type: os,library
|
||||
@@ -223,7 +221,7 @@ jobs:
|
||||
exit-code: 1
|
||||
|
||||
- name: Build free-threaded amd64 candidate
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -242,10 +240,10 @@ jobs:
|
||||
cache-to: type=gha,scope=moviepilot-v3t-docker-amd64,mode=max,version=2
|
||||
|
||||
- name: Scan free-threaded amd64 candidate vulnerabilities
|
||||
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
with:
|
||||
image-ref: moviepilot-v3t-candidate:linux-amd64
|
||||
version: v0.70.0
|
||||
version: latest
|
||||
cache-dir: ${{ runner.temp }}/trivy
|
||||
scanners: vuln
|
||||
vuln-type: os,library
|
||||
@@ -255,7 +253,7 @@ jobs:
|
||||
exit-code: 1
|
||||
|
||||
- name: Build free-threaded arm64 candidate
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -274,10 +272,10 @@ jobs:
|
||||
cache-to: type=gha,scope=moviepilot-v3t-docker-arm64,mode=max,version=2
|
||||
|
||||
- name: Scan free-threaded arm64 candidate vulnerabilities
|
||||
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
with:
|
||||
image-ref: moviepilot-v3t-candidate:linux-arm64
|
||||
version: v0.70.0
|
||||
version: latest
|
||||
cache-dir: ${{ runner.temp }}/trivy
|
||||
scanners: vuln
|
||||
vuln-type: os,library
|
||||
@@ -287,20 +285,20 @@ jobs:
|
||||
exit-code: 1
|
||||
|
||||
- name: Login DockerHub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
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
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -332,7 +330,7 @@ jobs:
|
||||
type=gha,scope=moviepilot-v3-standard-docker-arm64,version=2
|
||||
|
||||
- name: Publish free-threaded multi-architecture image
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -481,52 +479,77 @@ jobs:
|
||||
|
||||
- name: Get existing release body
|
||||
id: get_release_body
|
||||
continue-on-error: true
|
||||
env:
|
||||
CHANGELOG: ${{ env.CHANGELOG }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
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 // ""')
|
||||
tag_name="v${{ env.app_version }}"
|
||||
response_file=$(mktemp)
|
||||
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
|
||||
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
|
||||
{
|
||||
echo "RELEASE_BODY<<EOF"
|
||||
echo "$release_body"
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "RELEASE_BODY<<EOF" >> $GITHUB_ENV
|
||||
echo "$CHANGELOG" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
{
|
||||
echo "RELEASE_BODY<<EOF"
|
||||
echo "$CHANGELOG"
|
||||
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}"
|
||||
git tag -f "$tag_name" "$RELEASE_COMMIT"
|
||||
git push --force 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
|
||||
- name: Publish Release
|
||||
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
|
||||
|
||||
@@ -71,12 +71,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- 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'
|
||||
python-version: ${{ matrix.python-version }}
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
@@ -135,13 +134,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- 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
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
@@ -177,7 +176,13 @@ jobs:
|
||||
assert not expected_free_threaded or callable(moviepilot_rust.zhconv_fast);
|
||||
assert sys._is_gil_enabled() == (not expected_free_threaded)"
|
||||
|
||||
- name: Verify pinned uv version
|
||||
- name: Verify minimum uv version
|
||||
env:
|
||||
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)"
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const labelRules = [
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
- uses: actions/stale@v11
|
||||
with:
|
||||
# 标记 stale 标签时间
|
||||
days-before-issue-stale: 30
|
||||
|
||||
@@ -23,14 +23,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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'
|
||||
python-version: '3.14'
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
|
||||
@@ -47,14 +47,13 @@ jobs:
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- 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'
|
||||
enable-cache: true
|
||||
cache-dependency-glob: scripts/site_adapter_collector_requirements.txt
|
||||
|
||||
|
||||
@@ -26,12 +26,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- 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'
|
||||
python-version: '3.14'
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
@@ -101,12 +100,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- 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'
|
||||
python-version: '3.14'
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
@@ -127,12 +125,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- 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'
|
||||
python-version: '3.14'
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
ARG MOVIEPILOT_PYTHON_VARIANT="standard"
|
||||
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
|
||||
|
||||
@@ -78,7 +78,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
| Python 源码量 | 约 271,400 行 | 60 个文件超过 1,000 行,14 个超过 2,000 行 |
|
||||
| 长方法 | 281 个超过 80 行 | 67 个超过 150 行,23 个超过 250 行;大量是私有方法 |
|
||||
| 全量 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 未进入包级覆盖率门禁 |
|
||||
|
||||
### 3.3 热点文件
|
||||
|
||||
@@ -153,7 +153,7 @@ canonical 主程序;兼容只经统一 Compat/SDK 门面提供。
|
||||
| S4-L2 Event strict contract | `PLANNED` | S0-L2.6,S1-L6 | 宿主事件输入/输出按风险 strict,诊断例外只属于第三方插件兼容 |
|
||||
| S4-L3 Complexity v2 | `PLANNED` | S3 | 私有方法、class/file、圈复杂度进入门禁;所有超限通过职责拆分归零 |
|
||||
| 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 | 高风险包纳入 coverage;raw concurrency 分类清零;Module Quality 有真实 evidence test |
|
||||
|
||||
### S5:Plugin、Agent、Domain、Startup 与最终收口
|
||||
|
||||
+3
-3
@@ -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`
|
||||
- 安装后端依赖
|
||||
- 按当前仓库 `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+`,脚本会优先直接复用本地解释器
|
||||
- 如果系统里没有可用解释器,脚本会通过固定版本的 uv 安装 Python 3.14
|
||||
- 如果系统里没有可用解释器,脚本会通过最新稳定版 uv 安装 Python 3.14
|
||||
- Linux 下安装系统依赖时通常需要 `sudo`
|
||||
- 复用已有仓库时,脚本现在只会因为已跟踪源码改动而阻止自动更新,不会再被 `.DS_Store` 之类未跟踪文件卡住
|
||||
|
||||
@@ -165,7 +165,7 @@ moviepilot install deps --config-dir /path/to/moviepilot-config
|
||||
说明:
|
||||
|
||||
- 默认会自动选择本地已安装的 `Python 3.14+` 解释器
|
||||
- 安装器要求 `uv 0.12.5`,并按仓库提交的 `uv.lock` 同步依赖;不会在本地重新解析一套未锁定结果
|
||||
- 安装器要求 `uv 0.12.5+`,并按仓库提交的 `uv.lock` 同步依赖;不会在本地重新解析一套未锁定结果
|
||||
- `moviepilot_rust` 加速扩展通过 `moviepilot-rust` PyPI 依赖安装,主项目本地安装不需要 Rust toolchain
|
||||
- 安装完成后可在前端“高级设置 - 实验室”中关闭或重新开启 Rust 加速;如果后端未加载扩展,该开关会保持关闭且不可操作
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
在开始之前,请确保您的系统已安装以下软件:
|
||||
|
||||
- **Python 3.14+**
|
||||
- **uv 0.12.5**(Python 版本、虚拟环境和依赖锁定工具)
|
||||
- **uv 0.12.5+**(Python 版本、虚拟环境和依赖锁定工具,推荐使用最新稳定版)
|
||||
- **Git** (用于版本控制)
|
||||
- **RAR 解压工具**:本地开发如需测试或使用 `.rar` 字幕包解压,请安装 `unar`、`unrar`、`7z` 或 `bsdtar` 之一;Docker 镜像会内置 `unar`。
|
||||
|
||||
@@ -90,7 +90,7 @@ chmod +x scripts/start-local.sh
|
||||
1. **共享运行时依赖**:被 `app/` 生产代码直接导入,或是生产功能、后台任务、插件框架启动必需,写入 `[project].dependencies`。
|
||||
2. **ABI 敏感运行依赖**:标准与 free-threaded 解释器必须选择不同制品或版本时,分别写入 `runtime-standard` 和 `runtime-free-threaded`;两组保持互斥并由运行时统一选择。
|
||||
3. **开发 / 测试 / 静态检查 / 构建依赖**:只用于单测、覆盖率、lint 辅助、源码构建等,写入 `[dependency-groups].dev`。
|
||||
4. **工具依赖**:仓库要求使用 `uv 0.12.5`;不应为了安装工具而把它加入主程序运行依赖。
|
||||
4. **工具依赖**:仓库要求使用 `uv 0.12.5+`,推荐使用最新稳定版;不应为了安装工具而把它加入主程序运行依赖。
|
||||
5. **插件依赖**:由插件清单声明并在插件安装阶段处理,不直接并入主程序依赖。
|
||||
|
||||
修改后更新并校验锁文件:
|
||||
@@ -169,13 +169,13 @@ python -m scripts.generate_plugin_market_default \
|
||||
|
||||
### 5. 运行依赖漏洞检查
|
||||
|
||||
正式发布会使用固定版本的 `pip-audit` 检查 `uv.lock` 锁定的运行时依赖。依赖变更后也可以在
|
||||
正式发布会使用最新稳定版 `pip-audit` 检查 `uv.lock` 锁定的运行时依赖。依赖变更后也可以在
|
||||
本地执行同一检查:
|
||||
|
||||
```bash
|
||||
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 \
|
||||
uvx --from pip-audit pip-audit \
|
||||
--require-hashes --disable-pip --strict --progress-spinner off \
|
||||
--requirement /tmp/moviepilot-audit-requirements.txt
|
||||
```
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
|---|---|
|
||||
| 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 |
|
||||
| Package manager | uv 0.12.5 |
|
||||
| Package manager | uv 0.12.5+(推荐最新稳定版) |
|
||||
| Runtime install | `uv sync --locked --no-dev --no-install-project` |
|
||||
| Dev/test/lint/build install | `uv sync --locked` |
|
||||
| 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` |
|
||||
| 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` |
|
||||
| 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` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ uv sync --locked --offline --inexact --no-dev --check
|
||||
- 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.
|
||||
- `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
|
||||
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 \
|
||||
uvx --from pip-audit pip-audit \
|
||||
--require-hashes --disable-pip --strict --progress-spinner off \
|
||||
--requirement /tmp/moviepilot-audit-requirements.txt
|
||||
```
|
||||
|
||||
@@ -56,7 +56,7 @@ uv run --locked --no-sync pylint app/
|
||||
```bash
|
||||
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 \
|
||||
uvx --from pip-audit pip-audit \
|
||||
--require-hashes --disable-pip --strict --progress-spinner off \
|
||||
--requirement /tmp/moviepilot-audit-requirements.txt
|
||||
```
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ select = ["E4", "E7", "E9", "F", "I"]
|
||||
|
||||
[tool.uv]
|
||||
package = false
|
||||
required-version = "==0.12.5"
|
||||
required-version = ">=0.12.5"
|
||||
default-groups = ["dev", "runtime-standard"]
|
||||
conflicts = [
|
||||
[
|
||||
|
||||
@@ -16,7 +16,7 @@ SUPERUSER=""
|
||||
SUPERUSER_PASSWORD=""
|
||||
OS_NAME="Unknown"
|
||||
PYTHON_BIN=""
|
||||
UV_VERSION="0.12.5"
|
||||
MIN_UV_VERSION="0.12.5"
|
||||
BREW_BIN=""
|
||||
PACKAGE_MANAGER=""
|
||||
PACKAGE_INDEX_UPDATED="false"
|
||||
@@ -384,20 +384,43 @@ ensure_base_tools() {
|
||||
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() {
|
||||
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
|
||||
fi
|
||||
|
||||
echo "==> 自动安装 uv ${UV_VERSION}"
|
||||
env UV_INSTALL_DIR="$HOME/.local/bin" sh -c "$(curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh")"
|
||||
echo "==> 自动安装最新稳定版 uv"
|
||||
env UV_INSTALL_DIR="$HOME/.local/bin" sh -c "$(curl -LsSf https://astral.sh/uv/install.sh)"
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
hash -r
|
||||
|
||||
if ! command -v uv >/dev/null 2>&1 \
|
||||
|| [[ "$(uv --version 2>/dev/null | awk '{print $2}')" != "${UV_VERSION}" ]]; then
|
||||
echo "uv ${UV_VERSION} 安装失败,无法继续自动安装 Python。" >&2
|
||||
|| ! uv_version_supported "$(uv --version 2>/dev/null | awk '{print $2}')" "${MIN_UV_VERSION}"; then
|
||||
echo "uv ${MIN_UV_VERSION}+ 安装失败,无法继续自动安装 Python。" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
+16
-6
@@ -9,8 +9,8 @@ import importlib.util
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import secrets
|
||||
import re
|
||||
import secrets
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -36,7 +36,8 @@ NODE_DIR = RUNTIME_DIR / "node"
|
||||
INSTALL_ENV_FILE = ROOT / ".moviepilot.env"
|
||||
MIN_PYTHON_VERSION = (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
|
||||
LOG_DIR = CONFIG_DIR / "logs"
|
||||
@@ -618,18 +619,27 @@ def get_venv_bin_dir(venv_dir: Path) -> Path:
|
||||
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:
|
||||
"""返回仓库要求版本的 uv,避免不同安装入口使用不同解析器。"""
|
||||
"""返回满足仓库最低版本要求的 uv。"""
|
||||
uv_command = shutil.which("uv")
|
||||
if not uv_command:
|
||||
raise RuntimeError(
|
||||
f"未找到 uv {UV_VERSION},请先安装后重新执行。"
|
||||
f"未找到 uv {MIN_UV_VERSION_TEXT}+,请先安装后重新执行。"
|
||||
)
|
||||
uv_bin = Path(uv_command).expanduser().resolve()
|
||||
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(
|
||||
f"MoviePilot 需要 uv {UV_VERSION},当前为 {version or '未知版本'}。"
|
||||
f"MoviePilot 需要 uv {MIN_UV_VERSION_TEXT}+,当前为 {version or '未知版本'}。"
|
||||
)
|
||||
return uv_bin
|
||||
|
||||
|
||||
@@ -1484,9 +1484,6 @@
|
||||
"tests/test_release_group.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_release_supply_chain.py": {
|
||||
"I001": 1
|
||||
},
|
||||
"tests/test_reliability_adr.py": {
|
||||
"I001": 1
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
dockerfile = (ROOT / "docker" / "Dockerfile").read_text(encoding="utf-8")
|
||||
|
||||
assert (
|
||||
"FROM ghcr.io/astral-sh/uv:0.12.5@sha256:"
|
||||
"e85be844203885286c60ffad8a858d48afb6c5a5c237ca0e67f12e74b8f174b1 AS uv"
|
||||
in dockerfile
|
||||
)
|
||||
assert "FROM ghcr.io/astral-sh/uv:latest AS uv" in dockerfile
|
||||
assert "COPY --from=uv /uv /usr/local/bin/uv" in dockerfile
|
||||
assert "COPY pyproject.toml uv.lock ./" in dockerfile
|
||||
assert "python3 -m venv --without-pip ${VENV_PATH}" in dockerfile
|
||||
|
||||
@@ -217,17 +217,37 @@ class LocalSetupConfigDirTests(unittest.TestCase):
|
||||
self.assertIn("https://mirror.example/simple?token=abc", 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()
|
||||
uv_bin = Path("/opt/moviepilot/bin/uv")
|
||||
|
||||
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()
|
||||
|
||||
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):
|
||||
module = load_local_setup_module()
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""正式镜像发布的供应链门禁合同。"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
@@ -8,12 +11,30 @@ from ruamel.yaml import YAML
|
||||
|
||||
from scripts.normalize_audit_requirements import normalize_requirements
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOCKERFILE = ROOT / "docker" / "Dockerfile"
|
||||
RELEASE_WORKFLOW = ROOT / ".github" / "workflows" / "build-v3.yml"
|
||||
BETA_WORKFLOW = ROOT / ".github" / "workflows" / "beta.yml"
|
||||
PR_AGENT_WORKFLOW = ROOT / ".github" / "workflows" / "pr-agent.yml"
|
||||
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:
|
||||
@@ -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:
|
||||
"""基础镜像允许更新,并仅显式刷新运行时安全包而非整套 Debian。"""
|
||||
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-free-threaded" 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"):
|
||||
assert option in audit
|
||||
|
||||
@@ -142,12 +224,9 @@ def test_release_scans_both_architectures_before_registry_login_and_publish() ->
|
||||
):
|
||||
scan = indexed[name]
|
||||
assert scan["with"]["cache-dir"] == "${{ runner.temp }}/trivy"
|
||||
assert scan["uses"] == (
|
||||
"aquasecurity/trivy-action@"
|
||||
"a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8"
|
||||
)
|
||||
assert scan["uses"] == "aquasecurity/trivy-action@v0.36.0"
|
||||
assert scan["with"].items() >= {
|
||||
"version": "v0.70.0",
|
||||
"version": "latest",
|
||||
"scanners": "vuln",
|
||||
"vuln-type": "os,library",
|
||||
"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 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:
|
||||
"""漏洞豁免必须限定制品范围,并保留复查期限和接受理由。"""
|
||||
yaml = YAML(typ="safe")
|
||||
|
||||
Reference in New Issue
Block a user