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 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..1332313cd5 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: @@ -255,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/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..3493fff04c 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 @@ -78,9 +65,6 @@ jobs: build-linux-musl: permissions: contents: write - id-token: write - attestations: write - artifact-metadata: write strategy: fail-fast: false matrix: @@ -202,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' || '' }} @@ -214,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 @@ -234,9 +214,6 @@ jobs: build-linux-gnu: permissions: contents: write - id-token: write - attestations: write - artifact-metadata: write strategy: fail-fast: false matrix: @@ -376,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 @@ -387,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 @@ -464,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] @@ -496,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 @@ -519,3 +484,43 @@ 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 }} + - 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 c45cfb1437..ecbd89c3cc 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 @@ -41,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") { @@ -54,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) || '' }} @@ -230,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: | @@ -247,3 +235,32 @@ 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 + - 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