Skip to content

Build and upload Alpine APK packages to Forgejo #265

Build and upload Alpine APK packages to Forgejo

Build and upload Alpine APK packages to Forgejo #265

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