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: # debug purpose 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 # v5.3.1 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 # v2.0.3 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/upload-artifact@v4.6.0 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 # v4.1.8 with: name: python-package-distributions path: dist/ - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 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 # v4.1.8 with: name: python-package-distributions path: dist/ - name: Publish distribution 📦 to TestPyPI uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 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 # v5.3.1 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/cache@v4.2.2 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 # v5.3.1 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: Upload build artifact # uses: actions/upload-artifact@v4 # with: # name: win64-exe # path: ./build # if-no-files-found: error # compression-level: 1 # include-hidden-files: true - 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 # v6.1.0 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" # Upload the zip file to the 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