|
name: Test and Release |
|
|
|
on: |
|
push: |
|
branches: |
|
- main |
|
- master |
|
|
|
permissions: |
|
id-token: write |
|
contents: write |
|
pull-requests: write |
|
|
|
env: |
|
REGISTRY: ghcr.io |
|
REPO_LOWER: ${{ github.repository_owner }}/${{ github.event.repository.name }} |
|
GHCR_REPO: ghcr.io/${{ github.repository }} |
|
DOCKERHUB_REPO: byaidu/pdf2zh |
|
WIN_EXE_PYTHON_VERSION: "3.12.9" |
|
|
|
jobs: |
|
check-repository: |
|
name: Check if running in main repository |
|
runs-on: ubuntu-latest |
|
outputs: |
|
|
|
is_main_repo: ${{ github.repository == 'Byaidu/PDFMathTranslate' }} |
|
steps: |
|
- run: echo "Running repository check" |
|
|
|
test: |
|
needs: check-repository |
|
uses: ./.github/workflows/python-test.yml |
|
if: needs.check-repository.outputs.is_main_repo == 'true' |
|
|
|
build: |
|
name: Build distribution 📦 |
|
needs: [test, check-repository] |
|
if: needs.check-repository.outputs.is_main_repo == 'true' |
|
runs-on: ubuntu-latest |
|
outputs: |
|
is_release: ${{ steps.check-version.outputs.tag }} |
|
version: ${{ steps.check-version.outputs.tag && steps.get-release-version.outputs.version || steps.get-dev-version.outputs.version }} |
|
steps: |
|
- uses: actions/checkout@v4 |
|
with: |
|
persist-credentials: true |
|
fetch-depth: 2 |
|
token: ${{ secrets.GITHUB_TOKEN }} |
|
|
|
- name: Setup uv with Python 3.12 |
|
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 |
|
with: |
|
python-version: "3.12" |
|
enable-cache: true |
|
cache-dependency-glob: "pyproject.toml" |
|
|
|
- name: Check if there is a parent commit |
|
id: check-parent-commit |
|
run: | |
|
echo "sha=$(git rev-parse --verify --quiet HEAD^)" >> $GITHUB_OUTPUT |
|
|
|
- name: Detect and tag new version |
|
id: check-version |
|
if: steps.check-parent-commit.outputs.sha |
|
uses: salsify/action-detect-and-tag-new-version@b1778166f13188a9d478e2d1198f993011ba9864 |
|
with: |
|
version-command: | |
|
cat pyproject.toml | grep "version = " | head -n 1 | awk -F'"' '{print $2}' |
|
tag-template: 'v{VERSION}' |
|
|
|
- name: Install Dependencies |
|
run: | |
|
uv sync |
|
|
|
- name: Bump version for developmental release |
|
if: "!steps.check-version.outputs.tag" |
|
id: get-dev-version |
|
run: | |
|
version=$(bumpver update --patch --tag=final --dry 2>&1 | grep "New Version" | awk '{print $NF}') |
|
echo "version=$version.dev$(date +%s)" >> $GITHUB_OUTPUT |
|
bumpver update --set-version $version.dev$(date +%s) |
|
|
|
- name: Get release version |
|
if: steps.check-version.outputs.tag |
|
id: get-release-version |
|
run: | |
|
version=$(cat pyproject.toml | grep "version = " | head -n 1 | awk -F'"' '{print $2}') |
|
echo "version=$version" >> $GITHUB_OUTPUT |
|
|
|
- name: Build package |
|
run: "uv build" |
|
|
|
- name: Store the distribution packages |
|
uses: actions/[email protected] |
|
with: |
|
name: python-package-distributions |
|
path: dist/ |
|
|
|
publish-to-pypi: |
|
name: Publish Python 🐍 distribution 📦 to PyPI |
|
if: needs.build.outputs.is_release != '' |
|
needs: |
|
- check-repository |
|
- build |
|
- test-win64-exe |
|
runs-on: ubuntu-latest |
|
environment: |
|
name: pypi |
|
url: https://pypi.org/p/pdf2zh |
|
|
|
permissions: |
|
id-token: write |
|
|
|
steps: |
|
- name: Download all the dists |
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 |
|
with: |
|
name: python-package-distributions |
|
path: dist/ |
|
|
|
- name: Publish distribution 📦 to PyPI |
|
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc |
|
|
|
publish-to-testpypi: |
|
name: Publish Python 🐍 distribution 📦 to TestPyPI |
|
if: needs.build.outputs.is_release == '' |
|
needs: |
|
- check-repository |
|
- build |
|
- test-win64-exe |
|
runs-on: ubuntu-latest |
|
environment: |
|
name: testpypi |
|
url: https://test.pypi.org/p/pdf2zh |
|
|
|
permissions: |
|
id-token: write |
|
|
|
steps: |
|
- name: Download all the dists |
|
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 |
|
with: |
|
name: python-package-distributions |
|
path: dist/ |
|
|
|
- name: Publish distribution 📦 to TestPyPI |
|
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc |
|
with: |
|
repository-url: https://test.pypi.org/legacy/ |
|
|
|
build-docker-image: |
|
strategy: |
|
fail-fast: false |
|
matrix: |
|
include: |
|
- platform: linux/amd64 |
|
runner: ubuntu-latest |
|
- platform: linux/arm64 |
|
runner: ubuntu-24.04-arm |
|
runs-on: ${{ matrix.runner }} |
|
needs: |
|
- build |
|
- check-repository |
|
if: needs.check-repository.outputs.is_main_repo == 'true' |
|
environment: |
|
name: ${{ needs.build.outputs.is_release != '' && 'pypi' || 'testpypi' }} |
|
url: ${{ needs.build.outputs.is_release != '' && 'https://hub.docker.com/r/byaidu/pdf2zh/tags?name=latest' || 'https://hub.docker.com/r/byaidu/pdf2zh/tags?name=dev' }} |
|
permissions: |
|
contents: read |
|
packages: write |
|
|
|
steps: |
|
- name: Convert to lowercase |
|
run: | |
|
echo "GHCR_REPO_LOWER=$(echo ${{ env.GHCR_REPO }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV |
|
|
|
- name: Prepare |
|
run: | |
|
platform=${{ matrix.platform }} |
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV |
|
|
|
- name: Checkout repository |
|
uses: actions/checkout@v4 |
|
|
|
- name: Setup uv with Python 3.12 |
|
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 |
|
with: |
|
python-version: "3.12" |
|
enable-cache: true |
|
cache-dependency-glob: "pyproject.toml" |
|
|
|
- name: Set version from build job |
|
if: needs.build.outputs.is_release == '' |
|
run: | |
|
uv tool install bumpver |
|
echo "Using version: ${{ needs.build.outputs.version }}" |
|
bumpver update --set-version ${{ needs.build.outputs.version }} |
|
|
|
- name: Docker meta |
|
id: meta |
|
uses: docker/metadata-action@v5 |
|
with: |
|
images: | |
|
${{ env.DOCKERHUB_REPO }} |
|
${{ env.GHCR_REPO_LOWER }} |
|
tags: | |
|
type=raw,value=dev |
|
type=raw,value=${{ needs.build.outputs.version }},enable=${{ needs.build.outputs.is_release != '' }} |
|
type=raw,value=latest,enable=${{ needs.build.outputs.is_release != '' }} |
|
|
|
- name: Login to Docker.io |
|
uses: docker/login-action@v3 |
|
with: |
|
registry: docker.io |
|
username: ${{ secrets.DOCKER_USERNAME }} |
|
password: ${{ secrets.DOCKER_PASSWORD }} |
|
|
|
- name: Login to GHCR |
|
uses: docker/login-action@v3 |
|
with: |
|
registry: ghcr.io |
|
username: ${{ github.repository_owner }} |
|
password: ${{ secrets.GITHUB_TOKEN }} |
|
|
|
- name: Set up Docker Buildx |
|
uses: docker/setup-buildx-action@v3 |
|
|
|
- name: Build and push by digest |
|
id: build |
|
uses: docker/build-push-action@v6 |
|
with: |
|
platforms: ${{ matrix.platform }} |
|
labels: ${{ steps.meta.outputs.labels }} |
|
outputs: type=image,"name=${{ env.DOCKERHUB_REPO }},${{ env.GHCR_REPO_LOWER }}",push-by-digest=true,name-canonical=true,push=true |
|
cache-from: ${{ matrix.platform == 'linux/amd64' && 'type=gha' || '' }} |
|
cache-to: ${{ matrix.platform == 'linux/amd64' && 'type=gha,mode=max' || '' }} |
|
|
|
- name: Export digest |
|
run: | |
|
mkdir -p ${{ runner.temp }}/digests |
|
digest="${{ steps.build.outputs.digest }}" |
|
touch "${{ runner.temp }}/digests/${digest#sha256:}" |
|
|
|
- name: Upload digest |
|
uses: actions/upload-artifact@v4 |
|
with: |
|
name: digests-${{ env.PLATFORM_PAIR }} |
|
path: ${{ runner.temp }}/digests/* |
|
if-no-files-found: error |
|
retention-days: 1 |
|
|
|
merge-docker-image: |
|
runs-on: ubuntu-latest |
|
permissions: |
|
packages: write |
|
needs: |
|
- build-docker-image |
|
- check-repository |
|
- test-win64-exe |
|
- build |
|
if: needs.check-repository.outputs.is_main_repo == 'true' |
|
steps: |
|
- name: Convert to lowercase |
|
run: | |
|
echo "GHCR_REPO_LOWER=$(echo ${{ env.GHCR_REPO }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV |
|
|
|
- name: Download digests |
|
uses: actions/download-artifact@v4 |
|
with: |
|
path: ${{ runner.temp }}/digests |
|
pattern: digests-* |
|
merge-multiple: true |
|
|
|
- name: Login to Docker.io |
|
uses: docker/login-action@v3 |
|
with: |
|
registry: docker.io |
|
username: ${{ secrets.DOCKER_USERNAME }} |
|
password: ${{ secrets.DOCKER_PASSWORD }} |
|
|
|
- name: Login to GHCR |
|
uses: docker/login-action@v3 |
|
with: |
|
registry: ghcr.io |
|
username: ${{ github.repository_owner }} |
|
password: ${{ secrets.GITHUB_TOKEN }} |
|
|
|
- name: Set up Docker Buildx |
|
uses: docker/setup-buildx-action@v3 |
|
|
|
- name: Docker meta |
|
id: meta |
|
uses: docker/metadata-action@v5 |
|
with: |
|
images: | |
|
${{ env.DOCKERHUB_REPO }} |
|
${{ env.GHCR_REPO_LOWER }} |
|
tags: | |
|
type=raw,value=dev |
|
type=raw,value=${{ needs.build.outputs.version }},enable=${{ needs.build.outputs.is_release != '' && 'true' || 'false' }} |
|
type=raw,value=latest,enable=${{ needs.build.outputs.is_release != '' && 'true' || 'false' }} |
|
|
|
- name: Create manifest list and push |
|
working-directory: ${{ runner.temp }}/digests |
|
run: | |
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ |
|
$(printf '${{ env.DOCKERHUB_REPO }}@sha256:%s ' *) |
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ |
|
$(printf '${{ env.GHCR_REPO_LOWER }}@sha256:%s ' *) |
|
|
|
- name: Inspect image |
|
run: | |
|
docker buildx imagetools inspect ${{ env.DOCKERHUB_REPO }}:${{ steps.meta.outputs.version }} |
|
docker buildx imagetools inspect ${{ env.GHCR_REPO_LOWER }}:${{ steps.meta.outputs.version }} |
|
|
|
|
|
build-win64-exe: |
|
runs-on: windows-latest |
|
needs: |
|
- check-repository |
|
if: needs.check-repository.outputs.is_main_repo == 'true' |
|
steps: |
|
- name: checkout babeldoc metadata |
|
uses: actions/checkout@v4 |
|
with: |
|
repository: funstory-ai/BabelDOC |
|
path: babeldoctemp1234567 |
|
token: ${{ secrets.GITHUB_TOKEN }} |
|
sparse-checkout: babeldoc/assets/embedding_assets_metadata.py |
|
- name: Cached Assets |
|
id: cache-assets |
|
uses: actions/[email protected] |
|
with: |
|
path: ~/.cache/babeldoc |
|
key: test-1-babeldoc-assets-${{ hashFiles('babeldoctemp1234567/babeldoc/assets/embedding_assets_metadata.py') }} |
|
- name: 检出代码 |
|
uses: actions/checkout@v4 |
|
|
|
- name: Setup uv with Python ${{ env.WIN_EXE_PYTHON_VERSION }} |
|
uses: astral-sh/setup-uv@f94ec6bedd8674c4426838e6b50417d36b6ab231 |
|
with: |
|
python-version: ${{ env.WIN_EXE_PYTHON_VERSION }} |
|
enable-cache: true |
|
cache-dependency-glob: "pyproject.toml" |
|
|
|
- name: 执行所有任务(创建目录、下载、解压、复制文件、安装依赖) |
|
shell: pwsh |
|
run: | |
|
Write-Host "==== 创建必要的目录 ====" |
|
New-Item -Path "./build" -ItemType Directory -Force |
|
New-Item -Path "./build/runtime" -ItemType Directory -Force |
|
New-Item -Path "./dep_build" -ItemType Directory -Force |
|
|
|
Write-Host "==== 复制代码到 dep_build ====" |
|
Get-ChildItem -Path "./" -Exclude "dep_build", "build" | Copy-Item -Destination "./dep_build" -Recurse -Force |
|
|
|
Write-Host "==== 下载并解压 Python ${{ env.WIN_EXE_PYTHON_VERSION }} ====" |
|
Write-Host "pythonUrl: https://www.python.org/ftp/python/${{ env.WIN_EXE_PYTHON_VERSION }}/python-${{ env.WIN_EXE_PYTHON_VERSION }}-embed-amd64.zip" |
|
$pythonUrl = "https://www.python.org/ftp/python/${{ env.WIN_EXE_PYTHON_VERSION }}/python-${{ env.WIN_EXE_PYTHON_VERSION }}-embed-amd64.zip" |
|
$pythonZip = "./dep_build/python.zip" |
|
Invoke-WebRequest -Uri $pythonUrl -OutFile $pythonZip |
|
Expand-Archive -Path $pythonZip -DestinationPath "./build/runtime" -Force |
|
|
|
Write-Host "==== 下载 Visual C++ Redistributable 安装程序 ====" |
|
$vcRedistUrl = "https://aka.ms/vs/17/release/vc_redist.x64.exe" |
|
$vcRedistPath = "./build/无法运行请安装vc_redist.x64.exe" |
|
Invoke-WebRequest -Uri $vcRedistUrl -OutFile $vcRedistPath |
|
Write-Host "已下载 Visual C++ Redistributable 安装程序到: $vcRedistPath" |
|
|
|
Write-Host "==== 下载并解压 PyStand ====" |
|
$pystandUrl = "https://github.com/skywind3000/PyStand/releases/download/1.1.4/PyStand-v1.1.4-exe.zip" |
|
$pystandZip = "./dep_build/PyStand.zip" |
|
Invoke-WebRequest -Uri $pystandUrl -OutFile $pystandZip |
|
Expand-Archive -Path $pystandZip -DestinationPath "./dep_build/PyStand" -Force |
|
|
|
Write-Host "==== 复制 PyStand.exe 到 build 并重命名 ====" |
|
$pystandExe = "./dep_build/PyStand/PyStand-x64-CLI/PyStand.exe" |
|
$destExe = "./build/pdf2zh.exe" |
|
if (Test-Path $pystandExe) { |
|
Copy-Item -Path $pystandExe -Destination $destExe -Force |
|
} else { |
|
Write-Host "错误: PyStand.exe 未找到!" |
|
exit 1 |
|
} |
|
Write-Host "==== 创建 Python venv 在 dep_build ====" |
|
uv venv ./dep_build/venv |
|
|
|
./dep_build/venv/Scripts/activate |
|
|
|
Write-Host "==== 在 venv 环境中安装项目依赖 ====" |
|
uv pip install . |
|
|
|
Write-Host "==== 复制 venv/Lib/site-packages 到 build/ ====" |
|
Copy-Item -Path "./dep_build/venv/Lib/site-packages" -Destination "./build/site-packages" -Recurse -Force |
|
|
|
Write-Host "==== 复制 script/_pystand_static.int 到 build/ ====" |
|
$staticFile = "./script/_pystand_static.int" |
|
$destStatic = "./build/_pystand_static.int" |
|
if (Test-Path $staticFile) { |
|
Copy-Item -Path $staticFile -Destination $destStatic -Force |
|
} else { |
|
Write-Host "错误: script/_pystand_static.int 未找到!" |
|
exit 1 |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- name: Generate offline assets |
|
shell: pwsh |
|
run: | |
|
Write-Host "==== 生成离线资源包 ====" |
|
uv run --active babeldoc --generate-offline-assets ./build |
|
|
|
- name: Upload build with offline assets artifact |
|
uses: actions/upload-artifact@v4 |
|
with: |
|
name: win64-exe-with-assets |
|
path: ./build |
|
if-no-files-found: error |
|
compression-level: 1 |
|
include-hidden-files: true |
|
|
|
|
|
test-win64-exe: |
|
needs: |
|
- build-win64-exe |
|
- check-repository |
|
if: needs.check-repository.outputs.is_main_repo == 'true' |
|
runs-on: windows-latest |
|
steps: |
|
- name: 检出代码 |
|
uses: actions/checkout@v4 |
|
|
|
- name: Download build artifact |
|
uses: actions/download-artifact@v4 |
|
with: |
|
name: win64-exe-with-assets |
|
path: ./build |
|
|
|
- name: Test show version |
|
run: | |
|
./build/pdf2zh.exe --version |
|
|
|
- name: Test - Translate a PDF file with plain text only |
|
run: | |
|
./build/pdf2zh.exe ./test/file/translate.cli.plain.text.pdf -o ./test/file |
|
|
|
- name: Test - Translate a PDF file figure |
|
run: | |
|
./build/pdf2zh.exe ./test/file/translate.cli.text.with.figure.pdf -o ./test/file |
|
|
|
- name: Delete offline assets and cache |
|
shell: pwsh |
|
run: | |
|
Write-Host "==== 查找并删除离线资源包 ====" |
|
$offlineAssetsPath = Get-ChildItem -Path "./build" -Filter "offline_assets_*.zip" -Recurse | Select-Object -First 1 -ExpandProperty FullName |
|
if ($offlineAssetsPath) { |
|
Write-Host "找到离线资源包: $offlineAssetsPath" |
|
Remove-Item -Path $offlineAssetsPath -Force |
|
Write-Host "已删除离线资源包" |
|
} else { |
|
Write-Host "未找到离线资源包" |
|
} |
|
|
|
Write-Host "==== 删除缓存目录 ====" |
|
$cachePath = "$env:USERPROFILE/.cache/babeldoc" |
|
if (Test-Path $cachePath) { |
|
Remove-Item -Path $cachePath -Recurse -Force |
|
Write-Host "已删除缓存目录: $cachePath" |
|
} else { |
|
Write-Host "缓存目录不存在: $cachePath" |
|
} |
|
|
|
- name: Test - Translate without offline assets |
|
run: | |
|
Write-Host "==== 测试离线资源包 ====" |
|
New-Item -Path "./test/file/offline_result" -ItemType Directory -Force |
|
./build/pdf2zh.exe ./test/file/translate.cli.plain.text.pdf -o ./test/file/offline_result |
|
|
|
|
|
- name: Upload test results |
|
uses: actions/upload-artifact@v4 |
|
with: |
|
name: test-results |
|
path: ./test/file/ |
|
|
|
|
|
|
|
release-draft: |
|
name: Release Draft Tasks |
|
needs: |
|
- check-repository |
|
- build |
|
- publish-to-pypi |
|
- publish-to-testpypi |
|
- merge-docker-image |
|
- test-win64-exe |
|
if: | |
|
always() && needs.check-repository.outputs.is_main_repo == 'true' && |
|
(needs.publish-to-pypi.result == 'success' || needs.publish-to-testpypi.result == 'success') && |
|
needs.merge-docker-image.result == 'success' && |
|
needs.test-win64-exe.result == 'success' |
|
runs-on: ubuntu-latest |
|
permissions: |
|
contents: write |
|
pull-requests: write |
|
outputs: |
|
tag_name: ${{ steps.release-drafter.outputs.tag_name }} |
|
steps: |
|
- uses: actions/checkout@v4 |
|
with: |
|
persist-credentials: true |
|
fetch-depth: 2 |
|
token: ${{ secrets.GITHUB_TOKEN }} |
|
|
|
- name: Publish the release notes |
|
id: release-drafter |
|
uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 |
|
with: |
|
publish: ${{ needs.build.outputs.is_release != '' }} |
|
tag: ${{ needs.build.outputs.is_release }} |
|
env: |
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
|
|
|
|
|
upload-release: |
|
needs: [release-draft, check-repository] |
|
runs-on: ubuntu-latest |
|
if: always() && needs.check-repository.outputs.is_main_repo == 'true' && |
|
needs.release-draft.result == 'success' |
|
steps: |
|
- name: 检出代码 |
|
uses: actions/checkout@v4 |
|
|
|
- name: Download build artifact |
|
uses: actions/download-artifact@v4 |
|
with: |
|
name: win64-exe-with-assets |
|
path: ./build |
|
|
|
- name: Create release zip |
|
run: | |
|
mv ./build ./pdf2zh |
|
zip -9qr "pdf2zh-${{ needs.release-draft.outputs.tag_name }}-with-assets-win64.zip" ./pdf2zh/* |
|
|
|
|
|
find ./pdf2zh -name "offline_assets_*.zip" -type f -print -delete |
|
|
|
echo "Remaining offline assets files (should be empty):" |
|
find ./pdf2zh -name "offline_assets_*.zip" -type f |
|
|
|
zip -9qr "pdf2zh-${{ needs.release-draft.outputs.tag_name }}-win64.zip" ./pdf2zh/* |
|
|
|
- name: Upload to latest release |
|
env: |
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
|
run: | |
|
# Get the latest release (including drafts and pre-releases) |
|
LATEST_RELEASE=${{ needs.release-draft.outputs.tag_name }} |
|
echo "Latest release tag: $LATEST_RELEASE" |
|
|
|
|
|
gh release upload "$LATEST_RELEASE" "pdf2zh-${{ needs.release-draft.outputs.tag_name }}-win64.zip" --clobber |
|
gh release upload "$LATEST_RELEASE" "pdf2zh-${{ needs.release-draft.outputs.tag_name }}-with-assets-win64.zip" --clobber |