Merge pull request #16 from netresearch/feat/nginx-image #31
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
| # SPDX-License-Identifier: MIT | |
| # Copyright (c) 2026 Netresearch DTT GmbH | |
| # | |
| # The daily rebuild is the whole point of this repository: upstream refreshes | |
| # its image when phpMyAdmin is released, we refresh ours when its base picks up | |
| # fixes. Two variants per run — `pinned` ships the libraries as released, | |
| # `rolling` refreshes them within phpMyAdmin's own constraints. | |
| name: build | |
| on: | |
| schedule: | |
| - cron: '0 3 * * *' # 03:00 UTC, before the workday | |
| push: | |
| branches: [main] | |
| paths-ignore: | |
| - 'docs/**' | |
| - 'examples/**' | |
| - '**.md' | |
| pull_request: | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository_owner }}/phpmyadmin-php-fpm | |
| # nginx with the configuration and document root baked in, so a consumer | |
| # needs two containers and no mounted files. Always deployed at the same | |
| # tag as IMAGE_NAME — see the note on the web stage in the Dockerfile. | |
| WEB_IMAGE_NAME: ${{ github.repository_owner }}/phpmyadmin-nginx | |
| permissions: {} | |
| concurrency: | |
| group: build-${{ github.ref }} | |
| # A scheduled multi-arch rebuild must not be interrupted half-pushed. | |
| cancel-in-progress: ${{ github.event_name != 'schedule' }} | |
| jobs: | |
| build: | |
| name: ${{ matrix.variant }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - variant: pinned | |
| rolling: "false" | |
| suffix: "" | |
| - variant: rolling | |
| rolling: "true" | |
| suffix: "-rolling" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Read the pinned phpMyAdmin release | |
| id: version | |
| run: echo "pma=$(cat .phpmyadmin-version)" >> "$GITHUB_OUTPUT" | |
| - uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 | |
| - uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 | |
| # Pushing only happens from main and the schedule: a pull request must be | |
| # able to prove the image still builds without being able to publish it. | |
| - name: Log in to the registry | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Tags and labels | |
| id: meta | |
| uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| flavor: | | |
| suffix=${{ matrix.suffix }},onlatest=true | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=raw,value=${{ steps.version.outputs.pma }},enable={{is_default_branch}} | |
| type=raw,value=${{ steps.version.outputs.pma }}-{{date 'YYYYMMDD'}},enable={{is_default_branch}} | |
| type=ref,event=pr | |
| - name: Build and push | |
| id: push | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | |
| with: | |
| context: . | |
| # A pull request builds one architecture and keeps it local: `--load` | |
| # cannot import a manifest list, so asking for both there fails with | |
| # "docker exporter does not currently support exporting manifest | |
| # lists" before a single layer is built. | |
| platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} | |
| push: ${{ github.event_name != 'pull_request' }} | |
| load: ${{ github.event_name == 'pull_request' }} | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| cache-from: type=gha,scope=${{ matrix.variant }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.variant }} | |
| build-args: | | |
| PMA_VERSION=${{ steps.version.outputs.pma }} | |
| ROLLING_DEPS=${{ matrix.rolling }} | |
| BUILD_DATE=${{ github.event.repository.updated_at }} | |
| VCS_REF=${{ github.sha }} | |
| # The SBOM and provenance attestations travel with the manifest, so | |
| # a consumer can ask the registry what is inside the image it pulled. | |
| # Both need an exporter that writes a manifest, which the local | |
| # `--load` on pull requests is not. | |
| sbom: ${{ github.event_name != 'pull_request' }} | |
| provenance: ${{ github.event_name != 'pull_request' && 'mode=max' || 'false' }} | |
| - name: Attest the build | |
| if: github.event_name != 'pull_request' | |
| uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 | |
| with: | |
| subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| subject-digest: ${{ steps.push.outputs.digest }} | |
| push-to-registry: true | |
| # The web image is built here rather than in a job of its own so that | |
| # both images come out of one run with one tag set. They share the | |
| # document root, and deploying them at different tags means running two | |
| # phpMyAdmin versions against each other. | |
| - name: Tags and labels (web) | |
| id: meta-web | |
| uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 | |
| with: | |
| images: ${{ env.REGISTRY }}/${{ env.WEB_IMAGE_NAME }} | |
| flavor: | | |
| suffix=${{ matrix.suffix }},onlatest=true | |
| tags: | | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| type=raw,value=${{ steps.version.outputs.pma }},enable={{is_default_branch}} | |
| type=raw,value=${{ steps.version.outputs.pma }}-{{date 'YYYYMMDD'}},enable={{is_default_branch}} | |
| type=ref,event=pr | |
| - name: Build and push (web) | |
| id: push-web | |
| uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 | |
| with: | |
| context: . | |
| target: web | |
| platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} | |
| push: ${{ github.event_name != 'pull_request' }} | |
| load: ${{ github.event_name == 'pull_request' }} | |
| tags: ${{ steps.meta-web.outputs.tags }} | |
| labels: ${{ steps.meta-web.outputs.labels }} | |
| # Same cache scope as the runtime build: both share the fetcher | |
| # stage, so the second build reuses it instead of downloading and | |
| # verifying the release a second time. | |
| cache-from: type=gha,scope=${{ matrix.variant }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.variant }} | |
| build-args: | | |
| PMA_VERSION=${{ steps.version.outputs.pma }} | |
| ROLLING_DEPS=${{ matrix.rolling }} | |
| BUILD_DATE=${{ github.event.repository.updated_at }} | |
| VCS_REF=${{ github.sha }} | |
| sbom: ${{ github.event_name != 'pull_request' }} | |
| provenance: ${{ github.event_name != 'pull_request' && 'mode=max' || 'false' }} | |
| - name: Attest the build (web) | |
| if: github.event_name != 'pull_request' | |
| uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 | |
| with: | |
| subject-name: ${{ env.REGISTRY }}/${{ env.WEB_IMAGE_NAME }} | |
| subject-digest: ${{ steps.push-web.outputs.digest }} | |
| push-to-registry: true | |
| # Proves the image actually serves phpMyAdmin, not merely that it built. | |
| # A dependency refresh that breaks the application would pass every static | |
| # check and fail here. | |
| smoke-test: | |
| name: smoke test (${{ matrix.variant }}) | |
| needs: build | |
| if: github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| variant: [pinned, rolling] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Start the stack | |
| run: | | |
| set -euo pipefail | |
| cp .env.example .env | |
| { | |
| echo "MARIADB_ROOT_PASSWORD=smoke-test-only" | |
| echo "PMA_BLOWFISH_SECRET=smoketestsmoketestsmoketest12345" | |
| if [ "${{ matrix.variant }}" = "rolling" ]; then | |
| echo "PMA_IMAGE_TAG=latest-rolling" | |
| fi | |
| } >> .env | |
| docker compose --profile standalone up -d --wait --wait-timeout 300 | |
| - name: phpMyAdmin answers and logs in | |
| run: | | |
| set -euo pipefail | |
| jar=$(mktemp) | |
| code=$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/) | |
| [ "$code" = "200" ] || { echo "::error::login page returned $code"; exit 1; } | |
| token=$(curl -sS -c "$jar" http://127.0.0.1:8080/index.php \ | |
| | grep -oE 'name="token" value="[^"]+"' | head -1 | sed 's/.*value="//;s/"//') | |
| [ -n "$token" ] || { echo "::error::no CSRF token in the login form"; exit 1; } | |
| curl -sS -b "$jar" -c "$jar" -o /dev/null \ | |
| -X POST -d "pma_username=root&pma_password=smoke-test-only&server=1&target=index.php&token=$token" \ | |
| http://127.0.0.1:8080/index.php | |
| # Assert on the absence of the login form, not on a phrase from the | |
| # authenticated page: that wording depends on the phpMyAdmin version | |
| # and the negotiated language. Rejected credentials re-render the | |
| # form, so its presence is the discriminator — verified both ways: | |
| # the unauthenticated page carries `name="pma_username"` exactly | |
| # once, the authenticated one not at all. | |
| page=$(curl -sS -b "$jar" -w '\n%{http_code}' http://127.0.0.1:8080/index.php) | |
| code=${page##*$'\n'} | |
| [ "$code" = "200" ] || { echo "::error::authenticated page returned $code"; exit 1; } | |
| if printf '%s' "$page" | grep -q 'name="pma_username"'; then | |
| echo "::error::still on the login form — the credentials were rejected" | |
| printf '%s' "$page" | grep -oE 'class="alert[^"]*"[^<]*<[^>]*>[^<]{0,200}' | head -3 | |
| exit 1 | |
| fi | |
| echo "logged in against the database" | |
| - name: php-fpm endpoints are not public | |
| run: | | |
| set -euo pipefail | |
| for path in ping status; do | |
| code=$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:8080/$path") | |
| [ "$code" = "403" ] || { echo "::error::/$path answered $code, expected 403"; exit 1; } | |
| done | |
| - name: Logs on failure | |
| if: failure() | |
| run: docker compose --profile standalone logs |