From 7cd648359ac79c25e8903fcfce2c794ca55f977d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 24 Oct 2026 09:37:09 +0200 Subject: [PATCH 1/4] ci: build release artifacts in reusable workflows for SLSA Build L3 Attestations are now signed by static.yaml, docker.yaml and windows.yaml as reusable workflows; the build-*.yaml callers only hold the triggers. --- .github/workflows/build-docker.yaml | 42 ++++++++++++++++++++++++++++ .github/workflows/build-static.yaml | 42 ++++++++++++++++++++++++++++ .github/workflows/build-windows.yaml | 40 ++++++++++++++++++++++++++ .github/workflows/docker.yaml | 20 +++---------- .github/workflows/release.yaml | 2 +- .github/workflows/static.yaml | 21 +++----------- .github/workflows/windows.yaml | 18 +----------- 7 files changed, 134 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/build-docker.yaml create mode 100644 .github/workflows/build-static.yaml create mode 100644 .github/workflows/build-windows.yaml diff --git a/.github/workflows/build-docker.yaml b/.github/workflows/build-docker.yaml new file mode 100644 index 0000000000..862660e95f --- /dev/null +++ b/.github/workflows/build-docker.yaml @@ -0,0 +1,42 @@ +--- +name: Build Docker Images +concurrency: + cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + branches: + - main + push: + branches: + - main + tags: + - v*.*.* + workflow_dispatch: + inputs: + #checkov:skip=CKV_GHA_7 + version: + description: "FrankenPHP version" + required: false + type: string + schedule: + - cron: "0 4 * * *" + +permissions: + contents: read + +jobs: + # Attestations are signed by the reusable workflow, which verifiers pin with --signer-workflow (SLSA Build L3) + build: + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + uses: ./.github/workflows/docker.yaml + with: + version: ${{ inputs.version }} + secrets: + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} # zizmor: ignore[secrets-outside-env] TODO: drop once super-linter ships zizmor >=1.24.0, then the allowlist in zizmor.yaml takes over diff --git a/.github/workflows/build-static.yaml b/.github/workflows/build-static.yaml new file mode 100644 index 0000000000..b22f53dcde --- /dev/null +++ b/.github/workflows/build-static.yaml @@ -0,0 +1,42 @@ +--- +name: Build binary releases +concurrency: + cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + branches: + - main + push: + branches: + - main + tags: + - v*.*.* + workflow_dispatch: + inputs: + #checkov:skip=CKV_GHA_7 + version: + description: "FrankenPHP version" + required: false + type: string + schedule: + - cron: "0 0 * * *" + +permissions: + contents: read + +jobs: + # Attestations are signed by the reusable workflow, which verifiers pin with --signer-workflow (SLSA Build L3) + build: + permissions: + contents: write + id-token: write + attestations: write + artifact-metadata: write + uses: ./.github/workflows/static.yaml + with: + version: ${{ inputs.version }} + secrets: + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} # zizmor: ignore[secrets-outside-env] TODO: drop once super-linter ships zizmor >=1.24.0, then the allowlist in zizmor.yaml takes over diff --git a/.github/workflows/build-windows.yaml b/.github/workflows/build-windows.yaml new file mode 100644 index 0000000000..93a23669c2 --- /dev/null +++ b/.github/workflows/build-windows.yaml @@ -0,0 +1,40 @@ +--- +name: Build Windows release +concurrency: + cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + branches: + - main + push: + branches: + - main + tags: + - v*.*.* + workflow_dispatch: + inputs: + #checkov:skip=CKV_GHA_7 + version: + description: "FrankenPHP version" + required: false + type: string + schedule: + - cron: "0 8 * * *" + +permissions: + contents: read + +jobs: + # Attestations are signed by the reusable workflow, which verifiers pin with --signer-workflow (SLSA Build L3) + build: + permissions: + contents: write + id-token: write + attestations: write + artifact-metadata: write + uses: ./.github/workflows/windows.yaml + with: + version: ${{ inputs.version }} diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 288c4c2c71..caa4795605 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -1,27 +1,15 @@ --- name: Build Docker Images -concurrency: - cancel-in-progress: true - group: ${{ github.workflow }}-${{ github.ref }} on: - pull_request: - types: [opened, synchronize, reopened, labeled] - branches: - - main - push: - branches: - - main - tags: - - v*.*.* - workflow_dispatch: + workflow_call: inputs: - #checkov:skip=CKV_GHA_7 version: description: "FrankenPHP version" required: false type: string - schedule: - - cron: "0 4 * * *" + secrets: + DOCKERHUB_TOKEN: + required: false permissions: contents: read env: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f23fd55fe0..52f276e3d0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -371,7 +371,7 @@ jobs: VERSION: ${{ inputs.version }} run: | failed=() - for wf in static.yaml docker.yaml windows.yaml; do + for wf in build-static.yaml build-docker.yaml build-windows.yaml; do if ! gh workflow run "${wf}" --repo "${REPO}" --ref "v${VERSION}" -f version="${VERSION}"; then failed+=("${wf}") fi diff --git a/.github/workflows/static.yaml b/.github/workflows/static.yaml index 5eb066a9f1..a5bf918368 100644 --- a/.github/workflows/static.yaml +++ b/.github/workflows/static.yaml @@ -1,28 +1,15 @@ --- name: Build binary releases -concurrency: - cancel-in-progress: true - group: ${{ github.workflow }}-${{ github.ref }} - on: - pull_request: - types: [opened, synchronize, reopened, labeled] - branches: - - main - push: - branches: - - main - tags: - - v*.*.* - workflow_dispatch: + workflow_call: inputs: - #checkov:skip=CKV_GHA_7 version: description: "FrankenPHP version" required: false type: string - schedule: - - cron: "0 0 * * *" + secrets: + DOCKERHUB_TOKEN: + required: false permissions: contents: read diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index c45cfb1437..e3364d4ca1 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -1,29 +1,13 @@ --- name: Build Windows release -concurrency: - cancel-in-progress: true - group: ${{ github.workflow }}-${{ github.ref }} - on: - pull_request: - types: [opened, synchronize, reopened, labeled] - branches: - - main - push: - branches: - - main - tags: - - v*.*.* - workflow_dispatch: + workflow_call: inputs: - #checkov:skip=CKV_GHA_7 version: description: "FrankenPHP version" required: false type: string - schedule: - - cron: "0 8 * * *" permissions: contents: read From 1b6cc9d1babc050ee5a9235fb6e0413de85c62eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 24 Oct 2026 09:37:43 +0200 Subject: [PATCH 2/4] ci: sign attestations in dedicated jobs Build steps run third-party code (static-php-cli downloads, vcpkg, Go modules) and no longer get id-token: write, so they cannot sign arbitrary artifacts as the release workflow. --- .github/workflows/static.yaml | 55 +++++++++++++++++++--------------- .github/workflows/windows.yaml | 33 ++++++++++++++++---- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/.github/workflows/static.yaml b/.github/workflows/static.yaml index a5bf918368..cd2631f2e3 100644 --- a/.github/workflows/static.yaml +++ b/.github/workflows/static.yaml @@ -65,9 +65,6 @@ jobs: build-linux-musl: permissions: contents: write - id-token: write - attestations: write - artifact-metadata: write strategy: fail-fast: false matrix: @@ -189,7 +186,7 @@ jobs: env: BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }} - name: Upload artifact - if: ${{ !fromJson(needs.prepare.outputs.push) }} + if: ${{ !fromJson(needs.prepare.outputs.push) || needs.prepare.outputs.ref || github.ref_type == 'tag' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}${{ matrix.debug && '-debug' || '' }}${{ matrix.mimalloc && '-mimalloc' || '' }} @@ -201,10 +198,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REF: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }} - - if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag') - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 - with: - subject-path: ${{ github.workspace }}/frankenphp-linux-* - name: Run sanity checks run: | "${BINARY}" version @@ -221,9 +214,6 @@ jobs: build-linux-gnu: permissions: contents: write - id-token: write - attestations: write - artifact-metadata: write strategy: fail-fast: false matrix: @@ -363,7 +353,7 @@ jobs: env: BINARY: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }} - name: Upload artifact - if: ${{ !fromJson(needs.prepare.outputs.push) }} + if: ${{ !fromJson(needs.prepare.outputs.push) || needs.prepare.outputs.ref || github.ref_type == 'tag' }} uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: frankenphp-linux-${{ matrix.platform == 'linux/amd64' && 'x86_64' || 'aarch64' }}-gnu-files @@ -374,10 +364,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REF: ${{ (github.ref_type == 'tag' && github.ref_name) || needs.prepare.outputs.ref }} - - if: fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag') - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 - with: - subject-path: ${{ github.workspace }}/gh-output/frankenphp-linux-*-gnu - name: Run sanity checks run: | "${BINARY}" version @@ -451,9 +437,6 @@ jobs: build-mac: permissions: contents: write - id-token: write - attestations: write - artifact-metadata: write name: Build macOS arm64 binaries runs-on: macos-15 needs: [prepare] @@ -483,12 +466,7 @@ jobs: with: path: dist/log name: static-php-cli-log-arm64-${{ github.sha }} - - if: needs.prepare.outputs.ref || github.ref_type == 'tag' - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 - with: - subject-path: ${{ github.workspace }}/dist/frankenphp-mac-* - name: Upload artifact - if: github.ref_type == 'branch' uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: frankenphp-mac-arm64 @@ -506,3 +484,32 @@ jobs: "${BINARY}" php-cli -r "echo 'Sanity check passed';" env: BINARY: dist/frankenphp-mac-arm64 + + # Signs in its own job so build steps never get an OIDC token + attest: + if: ${{ !cancelled() && fromJson(needs.prepare.outputs.push) && (needs.prepare.outputs.ref || github.ref_type == 'tag') }} + needs: [prepare, build-linux-musl, build-linux-gnu, build-mac] + permissions: + id-token: write + attestations: write + artifact-metadata: write + strategy: + fail-fast: false + matrix: + binary: + - frankenphp-linux-x86_64 + - frankenphp-linux-x86_64-debug + - frankenphp-linux-x86_64-mimalloc + - frankenphp-linux-aarch64 + - frankenphp-linux-x86_64-gnu + - frankenphp-linux-aarch64-gnu + - frankenphp-mac-arm64 + name: Attest ${{ matrix.binary }} + runs-on: ubuntu-24.04 + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: ${{ endsWith(matrix.binary, '-gnu') && format('{0}-files', matrix.binary) || matrix.binary }} + - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: ${{ matrix.binary }} diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index e3364d4ca1..81eb19342e 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -25,12 +25,12 @@ jobs: build: permissions: contents: write - id-token: write - attestations: write - artifact-metadata: write + outputs: + ref: ${{ steps.ref.outputs.ref }} runs-on: windows-latest steps: - name: Determine ref + id: ref run: | $ref = $env:REF if (-not $ref -and $env:GITHUB_EVENT_NAME -eq "schedule") { @@ -38,6 +38,7 @@ jobs: } "REF=$ref" >> $env:GITHUB_ENV + "ref=$ref" >> $env:GITHUB_OUTPUT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REF: ${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }} @@ -214,10 +215,13 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - if: ${{ env.REF }} - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + - name: Upload Release Artifact + if: ${{ env.REF }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - subject-path: ${{ github.workspace }}\${{ env.DIR_NAME }}.zip + name: ${{ env.DIR_NAME }}.zip + path: ${{ env.DIR_NAME }}.zip + compression-level: 0 - name: Run Tests run: | @@ -231,3 +235,20 @@ jobs: gotestsum -- -race ./... if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } working-directory: ${{ github.workspace }}\frankenphp + + # Signs in its own job so build steps never get an OIDC token + attest: + if: ${{ !cancelled() && needs.build.outputs.ref }} + needs: build + permissions: + id-token: write + attestations: write + artifact-metadata: write + runs-on: ubuntu-24.04 + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: frankenphp-windows-x86_64.zip + - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: frankenphp-windows-x86_64.zip From 46512d51daa3836594d09206d310903a95b37be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 24 Oct 2026 09:38:14 +0200 Subject: [PATCH 3/4] ci: attest SPDX SBOMs for release binaries and Docker images --- .github/workflows/docker.yaml | 70 ++++++++++++++++++++++++++++++++++ .github/workflows/static.yaml | 11 ++++++ .github/workflows/windows.yaml | 12 ++++++ 3 files changed, 93 insertions(+) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index caa4795605..1332313cd5 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -243,3 +243,73 @@ jobs: subject-name: docker.io/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.inspect.outputs.digest }} push-to-registry: true + + # SBOMs are per platform image: architectures ship different packages + sbom: + runs-on: ubuntu-24.04 + needs: + - prepare + - build + if: fromJson(needs.prepare.outputs.push) + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare.outputs.matrix) }} + steps: + - name: Prepare + id: prepare + run: echo "sanitized_platform=${PLATFORM//\//-}" >> "${GITHUB_OUTPUT}" + env: + PLATFORM: ${{ matrix.platform }} + - name: Download builder metadata + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: metadata-builder-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }} + path: /tmp/metadata/builder + - name: Download runner metadata + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: metadata-runner-${{ matrix.variant }}-${{ steps.prepare.outputs.sanitized_platform }} + path: /tmp/metadata/runner + - name: Login to DockerHub + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} # zizmor: ignore[secrets-outside-env] TODO: drop once super-linter ships zizmor >=1.24.0, then the allowlist in zizmor.yaml takes over + - name: Read digests + id: digests + run: | + { + echo "builder=sha256:$(ls /tmp/metadata/builder)" + echo "runner=sha256:$(ls /tmp/metadata/runner)" + } >> "${GITHUB_OUTPUT}" + - name: Generate builder SBOM + uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 + with: + image: docker.io/${{ env.IMAGE_NAME }}@${{ steps.digests.outputs.builder }} + output-file: builder.spdx.json + upload-artifact: false + upload-release-assets: false + - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-name: docker.io/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.digests.outputs.builder }} + sbom-path: builder.spdx.json + push-to-registry: true + - name: Generate runner SBOM + uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 + with: + image: docker.io/${{ env.IMAGE_NAME }}@${{ steps.digests.outputs.runner }} + output-file: runner.spdx.json + upload-artifact: false + upload-release-assets: false + - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-name: docker.io/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.digests.outputs.runner }} + sbom-path: runner.spdx.json + push-to-registry: true diff --git a/.github/workflows/static.yaml b/.github/workflows/static.yaml index cd2631f2e3..3493fff04c 100644 --- a/.github/workflows/static.yaml +++ b/.github/workflows/static.yaml @@ -513,3 +513,14 @@ jobs: - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-path: ${{ matrix.binary }} + - name: Generate SBOM + uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 + with: + file: ${{ matrix.binary }} + output-file: sbom.spdx.json + upload-artifact: false + upload-release-assets: false + - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: ${{ matrix.binary }} + sbom-path: sbom.spdx.json diff --git a/.github/workflows/windows.yaml b/.github/workflows/windows.yaml index 81eb19342e..ecbd89c3cc 100644 --- a/.github/workflows/windows.yaml +++ b/.github/workflows/windows.yaml @@ -252,3 +252,15 @@ jobs: - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-path: frankenphp-windows-x86_64.zip + - run: unzip -q frankenphp-windows-x86_64.zip -d frankenphp-windows-x86_64 + - name: Generate SBOM + uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 + with: + path: frankenphp-windows-x86_64 + output-file: sbom.spdx.json + upload-artifact: false + upload-release-assets: false + - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: frankenphp-windows-x86_64.zip + sbom-path: sbom.spdx.json From 9ad86d8efac0f871bbb1abd5077906f683cf8d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 24 Oct 2026 09:38:14 +0200 Subject: [PATCH 4/4] chore: require admin review for workflow changes --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..42d272bc66 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# Release workflows sign attestations; changes need an admin's review +/.github/workflows/ @php/frankenphp-admin +/.github/actions/ @php/frankenphp-admin