Repository navigation
Build and upload Alpine APK packages to Forgejo #265
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 and upload Alpine APK packages to Forgejo | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| iteration: | |
| description: "Optional: override package iteration (integer). Leave empty for auto" | |
| required: false | |
| default: "" | |
| bump: | |
| description: "Bump each package iteration to (latest published on the repo) + 1" | |
| type: boolean | |
| required: false | |
| default: false | |
| php_versions: | |
| description: "Optional: PHP versions (comma-separated, e.g., 8.2,8.5). Leave empty for all" | |
| required: false | |
| default: "" | |
| architectures: | |
| description: "Optional: Architectures (comma-separated, e.g., x86_64,aarch64). Leave empty for all" | |
| required: false | |
| default: "" | |
| packages: | |
| description: "Optional: override packages list. Leave empty for default" | |
| required: false | |
| default: "" | |
| debug_tmate: | |
| description: "Open tmate session on failure" | |
| type: boolean | |
| required: false | |
| default: false | |
| repository_dispatch: | |
| types: [spc-download] | |
| permissions: | |
| contents: read | |
| packages: read | |
| jobs: | |
| setup-matrix: | |
| runs-on: ubuntu-24.04 | |
| permissions: | |
| contents: read | |
| outputs: | |
| libs-pairs: ${{ steps.set-matrix.outputs.libs-pairs }} | |
| php-versions: ${{ steps.set-matrix.outputs.php-versions }} | |
| steps: | |
| - name: Set up matrix | |
| id: set-matrix | |
| run: | | |
| default_php='["8.2","8.3","8.4","8.5","8.6"]' | |
| default_arch='["x86_64","aarch64"]' | |
| if [[ -n "${INPUTS_PHP_VERSIONS}" ]]; then | |
| php_versions=$(echo "${INPUTS_PHP_VERSIONS}" | jq -Rc 'split(",") | map(gsub("^\\s+|\\s+$";""))') | |
| else | |
| php_versions=$default_php | |
| fi | |
| if [[ -n "${INPUTS_ARCHITECTURES}" ]]; then | |
| arch_versions=$(echo "${INPUTS_ARCHITECTURES}" | jq -Rc 'split(",") | map(gsub("^\\s+|\\s+$";""))') | |
| else | |
| arch_versions=$default_arch | |
| fi | |
| libs_pairs=$(jq -nc --argjson arch "$arch_versions" '{include: [$arch[] as $r | {arch: $r}]}') | |
| echo "libs-pairs=$libs_pairs" >> $GITHUB_OUTPUT | |
| echo "php-versions=$php_versions" >> $GITHUB_OUTPUT | |
| env: | |
| INPUTS_PHP_VERSIONS: ${{ inputs.php_versions }} | |
| INPUTS_ARCHITECTURES: ${{ inputs.architectures }} | |
| build-libs: | |
| needs: setup-matrix | |
| name: Build libs (${{ matrix.arch }}) | |
| runs-on: ${{ matrix.arch == 'x86_64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }} | |
| permissions: | |
| contents: write | |
| packages: read | |
| defaults: | |
| run: | |
| shell: bash | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.setup-matrix.outputs.libs-pairs) }} | |
| # The builder image is real Alpine (musl). A musl `container:` can't run JavaScript | |
| # actions (checkout/cache/…) on arm64 — GitHub only ships a musl Node for x64 — so the | |
| # job runs on the glibc host and the build itself runs in the image via `docker run`. | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| IMAGE: ghcr.io/static-php/packages-builder-alpine:latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| persist-credentials: false | |
| - name: Compute buildroot cache key | |
| id: cache-key | |
| run: | | |
| set -euo pipefail | |
| WEEK=$(date -u +%G-%V) | |
| echo "key=buildroot-apk-${{ matrix.arch }}-${WEEK}" >> $GITHUB_OUTPUT | |
| - name: Restore buildroot cache | |
| id: cache | |
| uses: ./.github/actions/buildroot-cache | |
| with: | |
| mode: restore | |
| key: ${{ steps.cache-key.outputs.key }} | |
| - name: Log in to GHCR | |
| if: steps.cache.outputs.cache-hit != 'true' | |
| env: | |
| ACTOR: ${{ github.actor }} | |
| run: echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$ACTOR" --password-stdin | |
| - name: Prepare Composer cache dir | |
| if: steps.cache.outputs.cache-hit != 'true' | |
| run: mkdir -p ~/.cache/composer | |
| - name: Cache Composer downloads | |
| if: steps.cache.outputs.cache-hit != 'true' | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ hashFiles('**/composer.lock') }} | |
| restore-keys: | | |
| composer- | |
| - name: Download artifact from spc-download.yml | |
| if: steps.cache.outputs.cache-hit != 'true' | |
| uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11 | |
| with: | |
| workflow: spc-download.yml | |
| name: downloads-tarball | |
| search_artifacts: true | |
| - name: Extract with permissions | |
| if: steps.cache.outputs.cache-hit != 'true' | |
| run: | | |
| mkdir -p downloads | |
| tar --no-same-owner -xzf downloads.tar.gz -C downloads | |
| rm downloads.tar.gz | |
| - name: Build libs | |
| if: steps.cache.outputs.cache-hit != 'true' | |
| run: | | |
| docker run --rm \ | |
| -v "$GITHUB_WORKSPACE":/build -w /build \ | |
| -v "$HOME/.cache/composer":/composer-cache \ | |
| -e GITHUB_TOKEN -e COMPOSER_CACHE_DIR=/composer-cache \ | |
| "$IMAGE" \ | |
| bash -lc ' | |
| set -e | |
| # Tarball is extracted on the host (runner uid); this build runs as | |
| # root, so git rejects the cached source repos ("dubious ownership") | |
| # and SPC re-clones every git source. Trust them. | |
| git config --global --add safe.directory "*" | |
| composer install --no-interaction --prefer-dist --no-progress | |
| bin/spp build --phpv=8.5 --type=apk --libs-only | |
| ' | |
| - name: Pack buildroot | |
| if: steps.cache.outputs.cache-hit != 'true' | |
| run: tar --zstd -cf buildroot.tar.zst buildroot | |
| - name: Save buildroot cache | |
| if: steps.cache.outputs.cache-hit != 'true' | |
| uses: ./.github/actions/buildroot-cache | |
| with: | |
| mode: save | |
| key: ${{ steps.cache-key.outputs.key }} | |
| - name: Upload logs on failure | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: build-logs-libs-${{ matrix.arch }} | |
| path: log | |
| - name: Setup tmate session | |
| if: ${{ failure() && inputs.debug_tmate == true }} | |
| uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3 | |
| timeout-minutes: 30 | |
| compute-build-matrix: | |
| needs: [setup-matrix, build-libs] | |
| runs-on: ubuntu-24.04 | |
| if: ${{ !cancelled() }} | |
| permissions: | |
| actions: read | |
| outputs: | |
| matrix: ${{ steps.filter.outputs.matrix }} | |
| any: ${{ steps.filter.outputs.any }} | |
| steps: | |
| - name: Filter build matrix to arches with successful libs | |
| id: filter | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PHP_VERSIONS: ${{ needs.setup-matrix.outputs.php-versions }} | |
| run: | | |
| set -euo pipefail | |
| succeeded=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs" --paginate \ | |
| --jq '.jobs[] | select(.conclusion == "success" and (.name | test("^Build libs \\("))) | .name' \ | |
| | sed -E 's/^Build libs \(([^)]+)\)$/\1/' \ | |
| | sort -u) | |
| echo "Successful libs arches:" | |
| printf '%s\n' "$succeeded" | |
| include=$(jq -nc --arg s "$succeeded" --argjson p "$PHP_VERSIONS" ' | |
| ($s | split("\n") | map(select(length > 0) | {arch: .})) as $arches | | |
| [ $arches[] as $a | $p[] as $php | $a + {"php-version": $php} ] | |
| ') | |
| count=$(jq 'length' <<< "$include") | |
| echo "Filtered combos: $count" | |
| printf '%s\n' "$include" | jq . | |
| if [[ "$count" -gt 0 ]]; then | |
| echo "any=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "any=false" >> $GITHUB_OUTPUT | |
| fi | |
| echo "matrix={\"include\":$include}" >> $GITHUB_OUTPUT | |
| build: | |
| needs: [compute-build-matrix] | |
| if: ${{ !cancelled() && needs.compute-build-matrix.outputs.any == 'true' }} | |
| name: Build for ${{ matrix.arch }} PHP ${{ matrix.php-version }} | |
| runs-on: ${{ matrix.arch == 'x86_64' && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }} | |
| permissions: | |
| contents: read | |
| packages: read | |
| defaults: | |
| run: | |
| shell: bash | |
| strategy: | |
| fail-fast: false | |
| matrix: ${{ fromJson(needs.compute-build-matrix.outputs.matrix) }} | |
| # Real-Alpine (musl) builder: run on the glibc host and build via `docker run`, because | |
| # a musl `container:` can't run JavaScript actions on arm64 (no musl arm64 Node). | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| IMAGE: ghcr.io/static-php/packages-builder-alpine:latest | |
| ITERATION: ${{ inputs.iteration || '' }} | |
| BUMP: ${{ inputs.bump || false }} | |
| PACKAGES: ${{ inputs.packages || '' }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| persist-credentials: false | |
| - name: Set PHP version short | |
| run: | | |
| # Convert "8.5" to "85" | |
| PHP_VERSION_SHORT=$(echo "${{ matrix.php-version }}" | tr -d '.') | |
| echo "PHP_VERSION_SHORT=$PHP_VERSION_SHORT" >> $GITHUB_ENV | |
| - name: Log in to GHCR | |
| env: | |
| ACTOR: ${{ github.actor }} | |
| run: echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$ACTOR" --password-stdin | |
| - name: Prepare Composer cache dir | |
| run: mkdir -p ~/.cache/composer | |
| - name: Cache Composer downloads | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: ~/.cache/composer | |
| key: composer-${{ hashFiles('**/composer.lock') }} | |
| restore-keys: | | |
| composer- | |
| - name: Download artifact from spc-download.yml | |
| uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11 | |
| with: | |
| workflow: spc-download.yml | |
| name: downloads-tarball | |
| search_artifacts: true | |
| - name: Extract with permissions | |
| run: | | |
| mkdir -p downloads | |
| tar --no-same-owner -xzf downloads.tar.gz -C downloads | |
| rm downloads.tar.gz | |
| - name: Compute buildroot cache key | |
| id: cache-key | |
| run: | | |
| set -euo pipefail | |
| WEEK=$(date -u +%G-%V) | |
| echo "key=buildroot-apk-${{ matrix.arch }}-${WEEK}" >> $GITHUB_OUTPUT | |
| - name: Restore buildroot cache | |
| uses: ./.github/actions/buildroot-cache | |
| with: | |
| mode: restore | |
| key: ${{ steps.cache-key.outputs.key }} | |
| fail-on-cache-miss: true | |
| - name: Extract buildroot | |
| run: | | |
| tar --zstd -xf buildroot.tar.zst | |
| rm buildroot.tar.zst | |
| - name: Build PHP packages | |
| run: | | |
| docker run --rm \ | |
| -v "$GITHUB_WORKSPACE":/build -w /build \ | |
| -v "$HOME/.cache/composer":/composer-cache \ | |
| -e GITHUB_TOKEN -e COMPOSER_CACHE_DIR=/composer-cache \ | |
| -e ITERATION -e BUMP -e PACKAGES \ | |
| -e PHPV="${{ matrix.php-version }}" \ | |
| "$IMAGE" \ | |
| bash -lc ' | |
| set -e | |
| # Tarball is extracted on the host (runner uid); this build runs as | |
| # root, so git rejects the cached source repos ("dubious ownership") | |
| # and SPC re-clones every git source. Trust them. | |
| git config --global --add safe.directory "*" | |
| composer install --no-interaction --prefer-dist --no-progress | |
| PACKAGES_FLAG=""; [ -n "$PACKAGES" ] && PACKAGES_FLAG="--packages=$PACKAGES" | |
| ITERATION_FLAG=""; [ -n "$ITERATION" ] && ITERATION_FLAG="--iteration=$ITERATION" | |
| BUMP_FLAG=""; [ "$BUMP" = "true" ] && BUMP_FLAG="--bump" | |
| php bin/spp all --phpv="$PHPV" --prefix=-zts --type=apk $ITERATION_FLAG $BUMP_FLAG $PACKAGES_FLAG | |
| ' | |
| - name: Test packages | |
| run: | | |
| docker run --rm \ | |
| -v "$GITHUB_WORKSPACE":/build -w /build \ | |
| -e PHPV="${{ matrix.php-version }}" \ | |
| "$IMAGE" \ | |
| bash -lc 'php bin/spp test --phpv="$PHPV" --prefix=-zts --type=apk' | |
| - name: Report skipped shared extensions | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -uo pipefail | |
| LABEL="${MATRIX_ARCH} php${MATRIX_PHP_VERSION}" | |
| MANIFEST="" | |
| for candidate in "build/${MATRIX_PHP_VERSION}/skipped-shared-extensions.json" "buildroot/skipped-shared-extensions.json"; do | |
| if [[ -f "$candidate" ]]; then | |
| MANIFEST="$candidate" | |
| break | |
| fi | |
| done | |
| if [[ -z "$MANIFEST" ]]; then | |
| echo "No skipped-shared-extensions manifest for ${LABEL}; nothing to report." | |
| exit 0 | |
| fi | |
| if ! tolerant=$(jq -r 'if (.allow_shared_ext_failure // false) == true then "true" else "false" end' "$MANIFEST"); then | |
| echo "::warning::unreadable skipped-extension manifest at ${MANIFEST} (${LABEL})" | |
| exit 0 | |
| fi | |
| entries=$(jq -r '(.skipped // [])[] | [(.extension // "?"), (.phase // "unknown"), ((.message // "") | gsub("[\r\n|]"; " "))] | @tsv' "$MANIFEST") | |
| if [[ -z "$entries" ]]; then | |
| echo "No shared extensions were skipped for ${LABEL} (allow_shared_ext_failure=${tolerant})." | |
| exit 0 | |
| fi | |
| { | |
| echo "### Skipped shared extensions — ${LABEL}" | |
| echo "" | |
| echo "Manifest: \`${MANIFEST}\` (allow_shared_ext_failure=${tolerant})" | |
| echo "" | |
| echo "| Extension | Phase | Reason |" | |
| echo "| --- | --- | --- |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| while IFS=$'\t' read -r ext phase message; do | |
| echo "| ${ext} | ${phase} | ${message} |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "::warning::${LABEL}: skipped shared extension ${ext} (${phase}): ${message}" | |
| done <<< "$entries" | |
| env: | |
| MATRIX_ARCH: ${{ matrix.arch }} | |
| MATRIX_PHP_VERSION: ${{ matrix.php-version }} | |
| - name: Upload logs on failure | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: build-logs-${{ matrix.arch }}-php${{ matrix.php-version }} | |
| path: log | |
| - name: Upload to Forgejo Host 1 | |
| env: | |
| FORGEJO_PASSWORD: ${{ secrets.FORGEJO_PASSWORD }} | |
| run: | | |
| docker run --rm \ | |
| -v "$GITHUB_WORKSPACE":/build -w /build/dist/apk \ | |
| -e FORGEJO_PASSWORD -e PHP_VERSION_SHORT \ | |
| "$IMAGE" \ | |
| bash -lc ' | |
| echo "listing from host 1" | |
| ../../bin/forgejo-helper list alpine "$PHP_VERSION_SHORT" "$FORGEJO_PASSWORD" "*.apk" | |
| echo "uploading to host 1" | |
| ../../bin/forgejo-helper upload alpine "$PHP_VERSION_SHORT" "$FORGEJO_PASSWORD" "*.apk" | |
| ' | |
| - name: Upload to Forgejo Host 2 | |
| continue-on-error: true | |
| env: | |
| FORGEJO_PASSWORD: ${{ secrets.FORGEJO_PASSWORD }} | |
| run: | | |
| docker run --rm \ | |
| -v "$GITHUB_WORKSPACE":/build -w /build/dist/apk \ | |
| -e FORGEJO_PASSWORD -e PHP_VERSION_SHORT \ | |
| "$IMAGE" \ | |
| bash -lc ' | |
| echo "listing from host 2" | |
| ../../bin/forgejo-helper --host=2 list alpine "$PHP_VERSION_SHORT" "$FORGEJO_PASSWORD" "*.apk" | |
| echo "uploading to host 2" | |
| ../../bin/forgejo-helper --host=2 upload alpine "$PHP_VERSION_SHORT" "$FORGEJO_PASSWORD" "*.apk" | |
| ' | |
| - name: Setup tmate session | |
| if: ${{ failure() && inputs.debug_tmate == true }} | |
| uses: mxschmitt/action-tmate@c0afd6f790e3a5564914980036ebf83216678101 # v3 | |
| timeout-minutes: 30 |