Merge pull request #150 from codellm-devkit/fix/pypi-musl-alpine-smoke #5
Workflow file for this run
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: PyPI Release | |
| # Builds platform-tagged, impure wheels (each bundles a prebuilt GraalVM native | |
| # binary + the JDK .jmod files it needs at runtime), an sdist, and publishes to | |
| # PyPI via OIDC Trusted Publishing. | |
| # | |
| # Trusted Publishing: register a pending publisher on PyPI/TestPyPI with | |
| # owner=codellm-devkit repo=codeanalyzer-java workflow=release-pypi.yml | |
| # environment=pypi (for PyPI) / testpypi (for TestPyPI) | |
| # No long-lived API token is stored; OIDC mints a short-lived one per run. | |
| # | |
| # Triggers: | |
| # - push a tag vX.Y.Z -> build everything, create a GitHub Release, publish to PyPI | |
| # - manual dispatch -> build everything; optionally publish to TestPyPI or PyPI | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| workflow_dispatch: | |
| inputs: | |
| publish_target: | |
| description: "Where to publish the built artifacts" | |
| type: choice | |
| default: none | |
| options: | |
| - none | |
| - testpypi | |
| - pypi | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: release-pypi-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| GRAALVM_VERSION: "21.0.2" | |
| jobs: | |
| # -------------------------------------------------------------------------- | |
| # One impure, platform-tagged wheel per OS/arch. Each wheel embeds a native | |
| # binary built on that platform plus the bundled jmods, so compatibility is | |
| # guaranteed by construction (Linux legs build inside manylinux containers). | |
| # -------------------------------------------------------------------------- | |
| build-wheels: | |
| name: wheel (${{ matrix.name }}) | |
| runs-on: ${{ matrix.runs-on }} | |
| container: ${{ matrix.container }} | |
| continue-on-error: ${{ matrix.experimental == true }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # ---- glibc Linux (manylinux_2_28 = AlmaLinux 8, glibc 2.28) -------- | |
| - name: manylinux-x86_64 | |
| runs-on: ubuntu-22.04 | |
| container: quay.io/pypa/manylinux_2_28_x86_64 | |
| arch: x64 | |
| graal_arch: linux-x64 | |
| wheel_platform: manylinux_2_28_x86_64 | |
| musl: false | |
| - name: manylinux-aarch64 | |
| runs-on: ubuntu-22.04-arm | |
| container: quay.io/pypa/manylinux_2_28_aarch64 | |
| arch: aarch64 | |
| graal_arch: linux-aarch64 | |
| wheel_platform: manylinux_2_28_aarch64 | |
| musl: false | |
| # ---- musl Linux: fully-static binary built on the glibc container -- | |
| # GraalVM can't run on Alpine, so we build a `--static --libc=musl` | |
| # binary on a glibc host; the resulting static binary runs anywhere. | |
| - name: musllinux-x86_64 | |
| runs-on: ubuntu-22.04 | |
| container: quay.io/pypa/manylinux_2_28_x86_64 | |
| arch: x64 | |
| graal_arch: linux-x64 | |
| wheel_platform: musllinux_1_2_x86_64 | |
| musl: true | |
| experimental: true | |
| - name: musllinux-aarch64 | |
| runs-on: ubuntu-22.04-arm | |
| container: quay.io/pypa/manylinux_2_28_aarch64 | |
| arch: aarch64 | |
| graal_arch: linux-aarch64 | |
| wheel_platform: musllinux_1_2_aarch64 | |
| musl: true | |
| experimental: true | |
| # ---- macOS -------------------------------------------------------- | |
| # GitHub retired the Intel (macos-13) hosted runners, so the x86_64 | |
| # wheel is cross-built on Apple Silicon: we install an x86_64 GraalVM | |
| # and run native-image under Rosetta 2, which emits an x86_64 binary | |
| # and x86_64 jmods. (GraalVM native-image has no true cross-target.) | |
| - name: macos-x86_64 | |
| runs-on: macos-14 | |
| arch: x64 | |
| graal_arch: macos-x64 | |
| wheel_platform: macosx_11_0_x86_64 | |
| macos_target: "11.0" | |
| musl: false | |
| rosetta: true | |
| - name: macos-arm64 | |
| runs-on: macos-14 | |
| arch: arm64 | |
| wheel_platform: macosx_11_0_arm64 | |
| macos_target: "11.0" | |
| musl: false | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v4 | |
| - name: Configure git safe directory | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: git config --global --add safe.directory "$GITHUB_WORKSPACE" | |
| # native-image links libz; ensure headers/static archive are present. | |
| # Best-effort: manylinux already ships a working C toolchain. | |
| - name: Install Linux build prerequisites | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| (dnf install -y zlib-devel zlib-static || yum install -y zlib-devel zlib-static) || true | |
| # On Linux we install GraalVM by hand: setup-graalvm is awkward inside a | |
| # container, and we want the JDK image (with jmods) on the manylinux host. | |
| - name: Install GraalVM (Linux) | |
| if: runner.os == 'Linux' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-${GRAALVM_VERSION}/graalvm-community-jdk-${GRAALVM_VERSION}_${{ matrix.graal_arch }}_bin.tar.gz" | |
| echo "Downloading $url" | |
| mkdir -p /opt/graalvm | |
| curl -fsSL "$url" | tar -xz -C /opt/graalvm --strip-components=1 | |
| echo "JAVA_HOME=/opt/graalvm" >> "$GITHUB_ENV" | |
| echo "/opt/graalvm/bin" >> "$GITHUB_PATH" | |
| # Native arm64 GraalVM for the macos-arm64 leg. Skipped for the x86_64 | |
| # cross-build, which installs an x86_64 GraalVM by hand below. | |
| - name: Set up GraalVM (macOS arm64) | |
| if: runner.os == 'macOS' && matrix.rosetta != true | |
| uses: graalvm/setup-graalvm@v1 | |
| with: | |
| java-version: "21" | |
| distribution: graalvm-community | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| # macOS x86_64 cross-build: Rosetta 2 lets the x86_64 GraalVM (and the | |
| # native-image it drives) run on the Apple Silicon runner. | |
| - name: Install Rosetta 2 (macOS x86_64 cross-build) | |
| if: matrix.rosetta | |
| shell: bash | |
| run: softwareupdate --install-rosetta --agree-to-license | |
| - name: Install GraalVM x86_64 (macOS x86_64 cross-build) | |
| if: matrix.rosetta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-${GRAALVM_VERSION}/graalvm-community-jdk-${GRAALVM_VERSION}_${{ matrix.graal_arch }}_bin.tar.gz" | |
| echo "Downloading $url" | |
| mkdir -p "$RUNNER_TEMP/graalvm" | |
| # macOS tarballs nest the JDK under <top>/Contents/Home; strip the top. | |
| curl -fsSL "$url" | tar -xz -C "$RUNNER_TEMP/graalvm" --strip-components=1 | |
| home="$RUNNER_TEMP/graalvm/Contents/Home" | |
| test -x "$home/bin/native-image" || "$home/bin/gu" install native-image || true | |
| echo "JAVA_HOME=$home" >> "$GITHUB_ENV" | |
| echo "$home/bin" >> "$GITHUB_PATH" | |
| - name: Set up Python (macOS) | |
| if: runner.os == 'macOS' | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Set up musl toolchain (experimental) | |
| if: matrix.musl | |
| shell: bash | |
| run: bash .github/scripts/setup-musl.sh "${{ matrix.arch }}" | |
| - name: Build native image | |
| shell: bash | |
| env: | |
| CODEANALYZER_NATIVE_MUSL: ${{ matrix.musl && 'true' || 'false' }} | |
| MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macos_target }} | |
| run: | | |
| set -euo pipefail | |
| chmod +x ./gradlew || true | |
| # On the x86_64 leg, run the whole build under Rosetta so the x86_64 | |
| # GraalVM (JAVA_HOME) drives native-image and emits an x86_64 binary. | |
| if [ "${{ matrix.rosetta }}" = "true" ]; then | |
| arch -x86_64 ./gradlew --no-daemon clean nativeCompile | |
| else | |
| ./gradlew --no-daemon clean nativeCompile | |
| fi | |
| - name: Build wheel | |
| shell: bash | |
| env: | |
| CODEANALYZER_WHEEL_PLATFORM: ${{ matrix.wheel_platform }} | |
| MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macos_target }} | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ runner.os }}" = "Linux" ]; then | |
| PY=/opt/python/cp312-cp312/bin/python | |
| else | |
| PY=python3 | |
| fi | |
| export CODEANALYZER_NATIVE_BINARY="$PWD/build/native/nativeCompile/codeanalyzer" | |
| export CODEANALYZER_JMODS_DIR="$JAVA_HOME/jmods" | |
| echo "binary: $CODEANALYZER_NATIVE_BINARY" | |
| echo "jmods: $CODEANALYZER_JMODS_DIR" | |
| echo "platform: $CODEANALYZER_WHEEL_PLATFORM" | |
| "$PY" -m pip install --upgrade pip build | |
| "$PY" -m build --wheel --outdir "$PWD/dist" "$PWD/pypi" | |
| ls -l "$PWD/dist" | |
| # musl wheels can't be pip-installed on this glibc build host; they're | |
| # smoke-tested in a real Alpine (musl) container by the smoke-musl job. | |
| - name: Smoke test wheel | |
| if: matrix.rosetta != true && matrix.musl != true | |
| shell: bash | |
| env: | |
| PYTHON: ${{ runner.os == 'Linux' && '/opt/python/cp312-cp312/bin/python' || 'python3' }} | |
| run: bash .github/scripts/smoke-test.sh dist | |
| # The cross-built x86_64 wheel can't be installed by the runner's arm64 | |
| # Python. Run the whole smoke test under Rosetta with the universal | |
| # /usr/bin/python3, so the venv, pip install, and bundled native binary | |
| # all execute as x86_64. | |
| - name: Smoke test wheel (x86_64 via Rosetta) | |
| if: matrix.rosetta | |
| shell: bash | |
| env: | |
| PYTHON: /usr/bin/python3 | |
| run: arch -x86_64 bash .github/scripts/smoke-test.sh dist | |
| - name: Checksums (log only) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha256sum dist/*.whl | |
| else | |
| shasum -a 256 dist/*.whl | |
| fi | |
| # Non-musl wheels are uploaded under the publishable `wheel-*` name and go | |
| # straight to release/publish. musl wheels are staged under a separate | |
| # name and only promoted to `wheel-*` by smoke-musl once they pass the | |
| # Alpine smoke test — so an unverified musl wheel is never published. | |
| - name: Upload wheel artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.musl && format('musl-staging-{0}', matrix.name) || format('wheel-{0}', matrix.name) }} | |
| path: dist/*.whl | |
| if-no-files-found: error | |
| # -------------------------------------------------------------------------- | |
| # Source distribution. Carries only the wrapper sources; building a wheel from | |
| # it later requires a prebuilt binary + jmods (the build hook fails loudly | |
| # otherwise), so the sdist is a metadata/source artifact, not a build path. | |
| # -------------------------------------------------------------------------- | |
| build-sdist: | |
| name: sdist | |
| runs-on: ubuntu-22.04 | |
| steps: | |
| - name: Check out code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Build sdist | |
| run: | | |
| python -m pip install --upgrade pip build | |
| python -m build --sdist --outdir dist pypi | |
| ls -l dist | |
| - name: Upload sdist artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: sdist | |
| path: dist/*.tar.gz | |
| if-no-files-found: error | |
| # -------------------------------------------------------------------------- | |
| # Validate the musl wheels in a real Alpine (musl) container — they can't be | |
| # pip-installed on the glibc build host. Runs per-arch on native runners, | |
| # where docker is available (unlike inside the manylinux build container). | |
| # Only wheels that pass here are promoted to the publishable `wheel-*` name, | |
| # so an unverified musl wheel never reaches PyPI. Experimental: a musl | |
| # failure must never block the glibc/macOS release. | |
| # -------------------------------------------------------------------------- | |
| smoke-musl: | |
| name: smoke-musl (${{ matrix.arch }}) | |
| needs: build-wheels | |
| continue-on-error: true | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: x86_64 | |
| runs-on: ubuntu-22.04 | |
| name: musllinux-x86_64 | |
| - arch: aarch64 | |
| runs-on: ubuntu-22.04-arm | |
| name: musllinux-aarch64 | |
| runs-on: ${{ matrix.runs-on }} | |
| steps: | |
| - name: Download staged musl wheel | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: musl-staging-${{ matrix.name }} | |
| path: dist | |
| - name: Smoke test in Alpine (musl) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| docker run --rm -v "$PWD/dist:/dist:ro" python:3.12-alpine sh -euc ' | |
| wheel=$(ls /dist/*.whl | head -n1) | |
| echo "smoke-musl: testing $wheel" | |
| python -m venv /tmp/venv | |
| /tmp/venv/bin/pip install --upgrade pip >/dev/null | |
| /tmp/venv/bin/pip install "$wheel" | |
| echo "smoke-musl: codajv --version" | |
| /tmp/venv/bin/codajv --version | |
| echo "smoke-musl: codajv -s (level-1 source analysis, exercises bundled jmods)" | |
| out=$(/tmp/venv/bin/codajv -s "public class Smoke { public int answer() { return 42; } }") | |
| printf "%s" "$out" | head -c 2000; echo | |
| printf "%s" "$out" | grep -q Smoke || { echo "smoke-musl: FAILED — expected class Smoke in output"; exit 1; } | |
| echo "smoke-musl: OK" | |
| ' | |
| - name: Promote verified wheel for publishing | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: wheel-${{ matrix.name }} | |
| path: dist/*.whl | |
| if-no-files-found: error | |
| # -------------------------------------------------------------------------- | |
| # GitHub Release (tag pushes only): attach every wheel + sdist + checksums. | |
| # -------------------------------------------------------------------------- | |
| github-release: | |
| name: github-release | |
| needs: [build-wheels, build-sdist, smoke-musl] | |
| # Wait for smoke-musl (so verified musl wheels are attached) but never block | |
| # the release on it — only the essential build jobs must have succeeded. | |
| if: >- | |
| always() && | |
| needs.build-wheels.result == 'success' && | |
| needs.build-sdist.result == 'success' && | |
| github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: write | |
| steps: | |
| # Pull only publishable artifacts: `wheel-*` (incl. musl wheels promoted | |
| # by smoke-musl) and the sdist — never the `musl-staging-*` artifacts. | |
| - name: Download wheels | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: wheel-* | |
| path: dist | |
| merge-multiple: true | |
| - name: Download sdist | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: sdist | |
| path: dist | |
| - name: Generate checksums | |
| run: | | |
| cd dist | |
| sha256sum *.whl *.tar.gz > SHA256SUMS | |
| cat SHA256SUMS | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: | | |
| dist/*.whl | |
| dist/*.tar.gz | |
| dist/SHA256SUMS | |
| generate_release_notes: true | |
| # -------------------------------------------------------------------------- | |
| # Publish to PyPI via OIDC Trusted Publishing (no stored token). | |
| # Runs on tag push, or manual dispatch with publish_target=pypi. | |
| # -------------------------------------------------------------------------- | |
| publish-pypi: | |
| name: publish-pypi | |
| needs: [build-wheels, build-sdist, smoke-musl] | |
| # Wait for smoke-musl (so verified musl wheels publish too) but never block | |
| # the release on it — only the essential build jobs must have succeeded. | |
| if: >- | |
| always() && | |
| needs.build-wheels.result == 'success' && | |
| needs.build-sdist.result == 'success' && | |
| ( | |
| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || | |
| (github.event_name == 'workflow_dispatch' && inputs.publish_target == 'pypi') | |
| ) | |
| runs-on: ubuntu-22.04 | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/p/codeanalyzer-java | |
| permissions: | |
| id-token: write | |
| steps: | |
| # `wheel-*` (incl. smoke-musl-promoted musl wheels) + sdist; never staging. | |
| - name: Download wheels | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: wheel-* | |
| path: dist | |
| merge-multiple: true | |
| - name: Download sdist | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: sdist | |
| path: dist | |
| - name: Keep only distributables | |
| run: find dist -type f ! -name '*.whl' ! -name '*.tar.gz' -delete && ls -l dist | |
| - name: Publish to PyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| packages-dir: dist | |
| skip-existing: true | |
| # -------------------------------------------------------------------------- | |
| # Optional TestPyPI dry-run (manual dispatch with publish_target=testpypi). | |
| # -------------------------------------------------------------------------- | |
| publish-testpypi: | |
| name: publish-testpypi | |
| needs: [build-wheels, build-sdist, smoke-musl] | |
| if: >- | |
| always() && | |
| needs.build-wheels.result == 'success' && | |
| needs.build-sdist.result == 'success' && | |
| github.event_name == 'workflow_dispatch' && inputs.publish_target == 'testpypi' | |
| runs-on: ubuntu-22.04 | |
| environment: | |
| name: testpypi | |
| url: https://test.pypi.org/p/codeanalyzer-java | |
| permissions: | |
| id-token: write | |
| steps: | |
| # `wheel-*` (incl. smoke-musl-promoted musl wheels) + sdist; never staging. | |
| - name: Download wheels | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: wheel-* | |
| path: dist | |
| merge-multiple: true | |
| - name: Download sdist | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: sdist | |
| path: dist | |
| - name: Keep only distributables | |
| run: find dist -type f ! -name '*.whl' ! -name '*.tar.gz' -delete && ls -l dist | |
| - name: Publish to TestPyPI | |
| uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| repository-url: https://test.pypi.org/legacy/ | |
| packages-dir: dist | |
| skip-existing: true |