fix(cli): harden builds and artifact verification (#139) #221
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build ABK CLI | |
| permissions: | |
| contents: read | |
| on: | |
| workflow_dispatch: | |
| push: | |
| paths: | |
| - "cli/**" | |
| - ".github/workflows/build-abk-cli.yml" | |
| pull_request: | |
| paths: | |
| - "cli/**" | |
| - ".github/workflows/build-abk-cli.yml" | |
| concurrency: | |
| group: build-abk-cli-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| build: | |
| name: ${{ matrix.target_name }} | |
| runs-on: ${{ matrix.runs_on }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target_name: linux-arm64 | |
| target_id: linux-arm64 | |
| runs_on: ubuntu-24.04-arm | |
| archive_ext: tar.gz | |
| python_arch: arm64 | |
| binary_name: abk | |
| - target_name: linux-armhf | |
| target_id: linux-armhf | |
| runs_on: ubuntu-24.04-arm | |
| archive_ext: tar.gz | |
| python_arch: arm64 | |
| binary_name: abk | |
| - target_name: linux-x86 | |
| target_id: linux-x86 | |
| runs_on: ubuntu-24.04 | |
| archive_ext: tar.gz | |
| python_arch: x64 | |
| binary_name: abk | |
| - target_name: linux-x64 | |
| target_id: linux-x64 | |
| runs_on: ubuntu-24.04 | |
| archive_ext: tar.gz | |
| python_arch: x64 | |
| binary_name: abk | |
| - target_name: windows-arm64 | |
| target_id: windows-arm64 | |
| runs_on: windows-11-arm | |
| archive_ext: zip | |
| python_arch: arm64 | |
| binary_name: abk.exe | |
| - target_name: windows-x86 | |
| target_id: windows-x86 | |
| runs_on: windows-2025 | |
| archive_ext: zip | |
| python_arch: x86 | |
| binary_name: abk.exe | |
| - target_name: windows-x64 | |
| target_id: windows-x64 | |
| runs_on: windows-2025 | |
| archive_ext: zip | |
| python_arch: x64 | |
| binary_name: abk.exe | |
| - target_name: macos-arm64 | |
| target_id: macos-arm64 | |
| runs_on: macos-14 | |
| archive_ext: tar.gz | |
| python_arch: arm64 | |
| binary_name: abk | |
| - target_name: macos-x64 | |
| target_id: macos-x64 | |
| runs_on: macos-15-intel | |
| archive_ext: tar.gz | |
| python_arch: x64 | |
| binary_name: abk | |
| env: | |
| PYTHON_VERSION: "3.12" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| architecture: ${{ matrix.python_arch }} | |
| - name: Cache pip and cargo (cryptography Rust build) | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cache/pip | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| key: ${{ runner.os }}-${{ matrix.target_id }}-pip-crypto-v3 | |
| restore-keys: | | |
| ${{ runner.os }}-${{ matrix.target_id }}-pip-crypto- | |
| # Windows pip cache: ~\AppData\Local\pip\Cache — handled via different ~ mapping | |
| - name: Ensure pip cache on Windows | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| # Pip cache location differs on Windows; ensure the cache dir exists | |
| $pipCache = Join-Path $env:LOCALAPPDATA "pip\Cache" | |
| if (-not (Test-Path $pipCache)) { New-Item -ItemType Directory -Force $pipCache | Out-Null } | |
| - name: Install build dependencies | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python -m pip install --upgrade pip | |
| python -m pip install pyinstaller certifi pycryptodome pynacl | |
| python -m pip install cryptography 2>/dev/null || echo "::warning::cryptography unavailable, using pycryptodome" | |
| - name: Install Rust (cryptography build dependency) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| # Rust is needed for cryptography's pyo3-based native build | |
| if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { | |
| Write-Host "Installing Rust via rustup..." | |
| Set-ExecutionPolicy Unrestricted -Scope Process -Force | |
| & { iwr https://win.rustup.rs -UseBasicParsing | iex } -y --default-toolchain stable | |
| $env:Path = "$env:USERPROFILE\.cargo\bin;$env:Path" | |
| [Environment]::SetEnvironmentVariable("Path", "$env:USERPROFILE\.cargo\bin;$env:Path", "User") | |
| } | |
| cargo --version | |
| - name: Install build dependencies | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install setuptools-rust wheel pycryptodome pynacl | |
| python -m pip install cryptography 2>&1 | Out-Null | |
| if (-not $?) { Write-Warning "cryptography unavailable, using pycryptodome instead" } | |
| python -m pip install pyinstaller certifi | |
| - name: Run CLI regression tests | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s cli/tests -v | |
| - name: Run CLI regression tests | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $env:PYTHONDONTWRITEBYTECODE = "1" | |
| python -m unittest discover -s cli/tests -v | |
| if (-not $?) { exit 1 } | |
| - name: Build native one-dir bundle | |
| if: ${{ matrix.target_id == 'linux-arm64' || matrix.target_id == 'linux-x64' || matrix.target_id == 'macos-arm64' || matrix.target_id == 'macos-x64' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| rm -rf build dist | |
| pyinstaller \ | |
| --noconfirm \ | |
| --clean \ | |
| --onedir \ | |
| --name abk \ | |
| --hidden-import cryptography \ | |
| --hidden-import cryptography.hazmat.primitives \ | |
| --hidden-import cryptography.hazmat.primitives.asymmetric \ | |
| --hidden-import cryptography.hazmat.primitives.asymmetric.padding \ | |
| --hidden-import cryptography.hazmat.primitives.hashes \ | |
| --hidden-import cryptography.hazmat.backends.openssl \ | |
| --add-data "cli/i18n:i18n" \ | |
| cli/abk.py | |
| - name: Build Windows one-dir bundle | |
| if: ${{ matrix.target_id == 'windows-arm64' || matrix.target_id == 'windows-x86' || matrix.target_id == 'windows-x64' }} | |
| shell: pwsh | |
| run: | | |
| if (Test-Path build) { Remove-Item build -Recurse -Force } | |
| if (Test-Path dist) { Remove-Item dist -Recurse -Force } | |
| pyinstaller ` | |
| --noconfirm ` | |
| --clean ` | |
| --onedir ` | |
| --name abk ` | |
| --hidden-import cryptography ` | |
| --hidden-import cryptography.hazmat.primitives ` | |
| --hidden-import cryptography.hazmat.primitives.asymmetric ` | |
| --hidden-import cryptography.hazmat.primitives.asymmetric.padding ` | |
| --hidden-import cryptography.hazmat.primitives.hashes ` | |
| --hidden-import cryptography.hazmat.backends.openssl ` | |
| --add-data "cli/i18n;i18n" ` | |
| cli/abk.py | |
| - name: Prepare Linux cross-packaging runtime | |
| if: ${{ matrix.target_id == 'linux-armhf' || matrix.target_id == 'linux-x86' }} | |
| shell: bash | |
| env: | |
| CROSS_PLATFORM: ${{ matrix.target_id == 'linux-armhf' && 'linux/arm/v7' || 'linux/386' }} | |
| CROSS_BINFMT: ${{ matrix.target_id == 'linux-armhf' && 'arm' || '386' }} | |
| run: | | |
| set -euo pipefail | |
| if ! command -v docker >/dev/null 2>&1; then | |
| echo "::error::Docker is required for cross-packaging but is not available on this runner." | |
| exit 1 | |
| fi | |
| sudo systemctl start docker || sudo service docker start || true | |
| if ! sudo docker info >/dev/null 2>&1; then | |
| echo "::error::Docker daemon is unavailable on this runner." | |
| exit 1 | |
| fi | |
| # x86_64 Linux normally executes i386 containers natively, and some | |
| # arm64 runners provide arm32 EL0 support. Install only the emulator | |
| # that is actually needed when native execution is unavailable. | |
| docker pull --platform "$CROSS_PLATFORM" python:3.12-bookworm | |
| if docker run --rm \ | |
| --platform "$CROSS_PLATFORM" \ | |
| --entrypoint /bin/true \ | |
| python:3.12-bookworm; then | |
| echo "Native $CROSS_PLATFORM container execution is available." | |
| else | |
| echo "Native $CROSS_PLATFORM execution unavailable; enabling $CROSS_BINFMT emulation." | |
| sudo docker run --privileged --rm tonistiigi/binfmt --install "$CROSS_BINFMT" | |
| docker run --rm \ | |
| --platform "$CROSS_PLATFORM" \ | |
| --entrypoint /bin/true \ | |
| python:3.12-bookworm | |
| fi | |
| - name: Build linux-armhf bundle in container | |
| if: ${{ matrix.target_id == 'linux-armhf' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| rm -rf build dist | |
| pip_cache="$HOME/.cache/pip" | |
| runner_uid="$(id -u)" | |
| runner_gid="$(id -g)" | |
| mkdir -p "$pip_cache" | |
| sudo chown -R 0:0 "$pip_cache" | |
| restore_pip_cache_owner() { | |
| sudo chown -R "$runner_uid:$runner_gid" "$pip_cache" | |
| } | |
| trap restore_pip_cache_owner EXIT | |
| docker run --rm \ | |
| --platform linux/arm/v7 \ | |
| -v "$PWD:/work" \ | |
| -v "$pip_cache:/root/.cache/pip" \ | |
| -w /work \ | |
| python:3.12-bookworm \ | |
| bash -lc ' | |
| set -euo pipefail | |
| apt-get update | |
| apt-get install -y build-essential zlib1g-dev | |
| python -m pip install --upgrade pip | |
| python -m pip install pyinstaller certifi pycryptodome pynacl | |
| PYTHONPATH=cli python -c "import abk; assert abk._CRYPTO_BACKEND == \"pycryptodome\"" | |
| PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s cli/tests -v | |
| pyinstaller \ | |
| --noconfirm \ | |
| --clean \ | |
| --onedir \ | |
| --name abk \ | |
| --hidden-import certifi \ | |
| --collect-all certifi \ | |
| --hidden-import nacl \ | |
| --hidden-import nacl.bindings \ | |
| --hidden-import Crypto \ | |
| --hidden-import Crypto.PublicKey \ | |
| --hidden-import Crypto.Signature \ | |
| --hidden-import Crypto.Hash \ | |
| --add-data "cli/i18n:i18n" \ | |
| cli/abk.py | |
| ' | |
| sudo chown -R "$runner_uid:$runner_gid" build dist | |
| - name: Build linux-x86 bundle in container | |
| if: ${{ matrix.target_id == 'linux-x86' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| rm -rf build dist | |
| pip_cache="$HOME/.cache/pip" | |
| runner_uid="$(id -u)" | |
| runner_gid="$(id -g)" | |
| mkdir -p "$pip_cache" | |
| sudo chown -R 0:0 "$pip_cache" | |
| restore_pip_cache_owner() { | |
| sudo chown -R "$runner_uid:$runner_gid" "$pip_cache" | |
| } | |
| trap restore_pip_cache_owner EXIT | |
| docker run --rm \ | |
| --platform linux/386 \ | |
| -v "$PWD:/work" \ | |
| -v "$pip_cache:/root/.cache/pip" \ | |
| -w /work \ | |
| python:3.12-bookworm \ | |
| bash -lc ' | |
| set -euo pipefail | |
| apt-get update | |
| apt-get install -y build-essential zlib1g-dev | |
| python -m pip install --upgrade pip | |
| python -m pip install pyinstaller certifi pycryptodome pynacl | |
| PYTHONPATH=cli python -c "import abk; assert abk._CRYPTO_BACKEND == \"pycryptodome\"" | |
| PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s cli/tests -v | |
| pyinstaller \ | |
| --noconfirm \ | |
| --clean \ | |
| --onedir \ | |
| --name abk \ | |
| --hidden-import certifi \ | |
| --collect-all certifi \ | |
| --hidden-import nacl \ | |
| --hidden-import nacl.bindings \ | |
| --hidden-import Crypto \ | |
| --hidden-import Crypto.PublicKey \ | |
| --hidden-import Crypto.Signature \ | |
| --hidden-import Crypto.Hash \ | |
| --add-data "cli/i18n:i18n" \ | |
| cli/abk.py | |
| ' | |
| sudo chown -R "$runner_uid:$runner_gid" build dist | |
| - name: Add bundled README | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist/abk | |
| cat > dist/abk/README.txt <<'EOF' | |
| ABK CLI | |
| Run: | |
| Linux/macOS: ./abk --help | |
| Windows: abk.exe --help | |
| Environment: | |
| Set GITHUB_TOKEN before triggering builds that require authentication. | |
| EOF | |
| curl https://raw.githubusercontent.com/xingguangcuican6666/ABK/refs/heads/dev/LICENSE --output dist/abk/LICENSE | |
| - name: Add bundled README | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Path dist/abk -Force | Out-Null | |
| @" | |
| ABK CLI | |
| Run: | |
| Linux/macOS: ./abk --help | |
| Windows: abk.exe --help | |
| Environment: | |
| Set GITHUB_TOKEN before triggering builds that require authentication. | |
| "@ | Set-Content -Path dist/abk/README.txt | |
| curl https://raw.githubusercontent.com/xingguangcuican6666/ABK/refs/heads/dev/LICENSE --output dist/abk/LICENSE | |
| - name: Verify bundle contents | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| test -f "dist/abk/${{ matrix.binary_name }}" | |
| test -f "dist/abk/README.txt" | |
| test -f "dist/abk/_internal/i18n/zh-cn.json" | |
| test -f "dist/abk/_internal/i18n/en-us.json" | |
| - name: Verify bundle contents | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| if (!(Test-Path "dist/abk/${{ matrix.binary_name }}")) { throw "Missing binary" } | |
| if (!(Test-Path "dist/abk/README.txt")) { throw "Missing README" } | |
| if (!(Test-Path "dist/abk/_internal/i18n/zh-cn.json")) { throw "Missing zh-cn.json" } | |
| if (!(Test-Path "dist/abk/_internal/i18n/en-us.json")) { throw "Missing en-us.json" } | |
| - name: Smoke test | |
| if: runner.os != 'Windows' && matrix.target_id != 'linux-armhf' && matrix.target_id != 'linux-x86' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ./dist/abk/abk --help > /dev/null | |
| - name: Smoke test linux-armhf | |
| if: ${{ matrix.target_id == 'linux-armhf' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| docker run --rm --platform linux/arm/v7 -v "$PWD/dist:/work/dist" debian:bookworm-slim /work/dist/abk/abk --help > /dev/null | |
| - name: Smoke test linux-x86 | |
| if: ${{ matrix.target_id == 'linux-x86' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| docker run --rm --platform linux/386 -v "$PWD/dist:/work/dist" debian:bookworm-slim /work/dist/abk/abk --help > /dev/null | |
| - name: Smoke test Windows | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| .\dist\abk\abk.exe --lang en-us --help | Out-Null | |
| - name: Package artifact | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tar -C dist/abk -czf "dist/abk-cli-${{ matrix.target_id }}.tar.gz" . | |
| - name: Package artifact | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| Compress-Archive -Path dist/abk/* -DestinationPath dist/abk-cli-${{ matrix.target_id }}.zip -Force | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: abk-cli-${{ matrix.target_id }} | |
| path: dist/abk-cli-${{ matrix.target_id }}.${{ matrix.archive_ext }} | |
| if-no-files-found: error |