Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/stage_ghd_release_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def validate_ghd_config(config: dict[str, Any], *, binary_name: str, repository:
if not isinstance(provenance, dict):
raise StageError("ghd.toml must define a [provenance] table")

expected_signer = f"{repository}/.github/workflows/release.yml"
expected_signer = f"{repository}/.github/workflows/attest.yml"
actual_signer = provenance.get("signer_workflow")
if actual_signer != expected_signer:
raise StageError(
Expand Down
2 changes: 1 addition & 1 deletion .github/scripts/test_stage_ghd_release_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def run_script(root: Path) -> tuple[int, str, str]:
@contextlib.contextmanager
def fixture(
*,
signer: str = "meigma/template-go-api/.github/workflows/release.yml",
signer: str = "meigma/template-go-api/.github/workflows/attest.yml",
missing_checksum: str | None = None,
checksum_override: tuple[str, str] | None = None,
omit_artifact: tuple[str, str, str] | None = None,
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/attest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Reusable workflow that generates artifact PROVENANCE in isolation from the build
# jobs that call it. A reusable workflow runs in its own execution context with its
# own OIDC identity — the caller can inject neither steps nor secrets — so the
# signing material is unreachable by the build's user-defined steps. That isolation
# is the SLSA Build L3 requirement; GitHub artifact attestations generated in-job
# are only L2. Provenance is still written to GitHub's attestation API, so
# `gh attestation verify` keeps working — but the signer-workflow is now this file:
#
# gh attestation verify <artifact-or-oci-ref> --repo <repo> \
# --signer-workflow <repo>/.github/workflows/attest.yml --source-ref refs/tags/<tag>
#
# SLSA note: L3 makes the signed provenance unforgeable; it does not, by itself,
# make the build hermetic. The build job still computes the checksums/digest it
# passes in — L3's guarantee is that the signature cannot be tampered with because
# the key lives only in this isolated workflow.

name: Attest (reusable)

on:
workflow_call:
inputs:
checksums-artifact:
description: Name of an uploaded artifact containing checksums.txt (binary provenance).
type: string
required: false
default: ''
subject-name:
description: Fully-qualified image name without tag/digest (container provenance).
type: string
required: false
default: ''
subject-digest:
description: Image digest in sha256:... form (container provenance).
type: string
required: false
default: ''
push-to-registry:
description: Attach the image provenance attestation to the registry.
type: boolean
required: false
default: false

permissions: {}

jobs:
attest:
name: Attest
runs-on: ubuntu-24.04
permissions:
id-token: write # OIDC token minted HERE, isolated from the build job
attestations: write # write provenance to GitHub's attestation API
contents: read
packages: write # used only for the image attestation (push-to-registry)
steps:
- name: Download checksums
if: ${{ inputs.checksums-artifact != '' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.checksums-artifact }}

- name: Attest binary checksums
if: ${{ inputs.checksums-artifact != '' }}
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
subject-checksums: checksums.txt

- name: Attest image provenance
if: ${{ inputs.subject-digest != '' }}
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-name: ${{ inputs.subject-name }}
subject-digest: ${{ inputs.subject-digest }}
push-to-registry: ${{ inputs.push-to-registry }}
2 changes: 1 addition & 1 deletion .github/workflows/release-dry-run.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ jobs:
version="0.0.0-dryrun.${GITHUB_RUN_ID}.${GITHUB_RUN_ATTEMPT}"
binary_name="template-go-api"
asset_prefix="${binary_name}_"
expected_signer="${GITHUB_REPOSITORY}/.github/workflows/release.yml"
expected_signer="${GITHUB_REPOSITORY}/.github/workflows/attest.yml"
actual_signer="$(awk -F'"' '/signer_workflow/ { print $2; exit }' ghd.toml)"

if [ "$actual_signer" != "$expected_signer" ]; then
Expand Down
52 changes: 38 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ jobs:
- resolve-release
permissions:
contents: write
id-token: write
attestations: write
steps:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand Down Expand Up @@ -148,10 +146,26 @@ jobs:
RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }}
run: gh release upload "$RELEASE_TAG" dist/release-assets/* --clobber

- name: Attest release checksums
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
- name: Upload checksums for isolated attestation
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
subject-checksums: dist/release-assets/checksums.txt
name: release-checksums
path: dist/release-assets/checksums.txt
if-no-files-found: error
retention-days: 1

# Generate binary provenance in an ISOLATED reusable workflow (SLSA L3): the
# signing OIDC token is minted in attest.yml, unreachable by the build steps.
attest-binaries:
needs:
- binary-release-assets
permissions:
id-token: write
attestations: write
contents: read
uses: ./.github/workflows/attest.yml
with:
checksums-artifact: release-checksums

# Build the per-arch signed apk on a native runner (no QEMU). Each runner mints
# its own ephemeral melange key (distinct filename) and uploads its apk plus its
Expand Down Expand Up @@ -346,13 +360,6 @@ jobs:
IMAGE_REF: ${{ steps.publish.outputs.ref }}
run: cosign sign --yes "$IMAGE_REF"

- name: Attest container image provenance
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-name: ${{ steps.publish.outputs.name }}
subject-digest: ${{ steps.publish.outputs.digest }}
push-to-registry: true

- name: Generate image SBOM (Go module granularity)
env:
IMAGE_REF: ${{ steps.publish.outputs.ref }}
Expand All @@ -366,13 +373,30 @@ jobs:
sbom-path: image.spdx.json
push-to-registry: true

# Generate container provenance in the ISOLATED reusable workflow (SLSA L3).
attest-image:
needs:
- container-image-release
permissions:
id-token: write
attestations: write
packages: write
contents: read
uses: ./.github/workflows/attest.yml
with:
subject-name: ${{ needs.container-image-release.outputs.image-name }}
subject-digest: ${{ needs.container-image-release.outputs.image-digest }}
push-to-registry: true

release-inspection-summary:
name: Release Inspection Summary
runs-on: ubuntu-24.04
needs:
- resolve-release
- binary-release-assets
- container-image-release
- attest-binaries
- attest-image
permissions: {}
steps:
- name: Write inspection summary
Expand All @@ -392,7 +416,7 @@ jobs:
echo '```sh'
echo "gh release view $RELEASE_TAG --repo $GITHUB_REPOSITORY --json isDraft,assets"
echo "asset=\"template-go-api_${RELEASE_VERSION}_\$(go env GOOS)_\$(go env GOARCH)\""
echo "gh attestation verify \"dist/release-assets/\${asset}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/release.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners"
echo "gh attestation verify \"dist/release-assets/\${asset}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/attest.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners"
echo "ghd download \"$GITHUB_REPOSITORY/template-go-api@${RELEASE_VERSION}\" --output \"\$(mktemp -d)\""
echo '```'
echo
Expand All @@ -402,7 +426,7 @@ jobs:
echo "docker login ghcr.io"
echo "docker pull \"${IMAGE_NAME}:${RELEASE_TAG}\""
echo "docker run --rm \"${IMAGE_NAME}:${RELEASE_TAG}\" --version"
echo "gh attestation verify \"oci://${IMAGE_NAME}@${IMAGE_DIGEST}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/release.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners"
echo "gh attestation verify \"oci://${IMAGE_NAME}@${IMAGE_DIGEST}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/attest.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners"
echo "cosign verify \"${IMAGE_NAME}@${IMAGE_DIGEST}\" --certificate-identity-regexp \"^https://github.com/${GITHUB_REPOSITORY}/.github/workflows/release.yml@.*\" --certificate-oidc-issuer https://token.actions.githubusercontent.com"
echo '```'
echo
Expand Down
2 changes: 1 addition & 1 deletion DELETE_ME.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ The nominal generated-project path is an HTTP service with both a downloadable b
For library-only projects:

- Keep Release Please if version tags and changelogs are useful.
- Delete `.github/workflows/release.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/security-scan.yml`, `.goreleaser.yaml`, `ghd.toml`, `melange.yaml`, and `apko.yaml` unless the library publishes some other artifact.
- Delete `.github/workflows/release.yml`, `.github/workflows/release-dry-run.yml`, `.github/workflows/attest.yml`, `.github/workflows/security-scan.yml`, `.goreleaser.yaml`, `ghd.toml`, `melange.yaml`, and `apko.yaml` unless the library publishes some other artifact.
- Remove release dry-run checks from `.github/repository-settings.toml`.
- If the library should not create releases at all, delete `.github/workflows/release-please.yml`, `release-please-config.json`, `.release-please-manifest.json`, and `CHANGELOG.md`.

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,10 +706,11 @@ The release path is:

- Release Please opens and maintains the release PR.
- Release Please creates a draft GitHub release and tag after merge.
- Release Dry Run rehearses the GoReleaser binary path and native-runner Docker container build path on pull requests.
- Release Dry Run rehearses the GoReleaser binary path and the native-runner melange/apko container build path on pull requests.
- GoReleaser builds binaries, checksums, and SBOMs without publishing directly.
- The release workflow uploads assets to the draft release and creates a GitHub-hosted attestation for `checksums.txt`.
- The release workflow builds amd64 and arm64 container images on native GitHub-hosted runners, publishes `ghcr.io/meigma/template-go-api:vX.Y.Z` as a multi-platform manifest, attaches BuildKit provenance and SBOM metadata, and creates a GitHub-native attestation for the manifest digest.
- The release workflow uploads assets to the draft release; a separate, isolated reusable workflow (`attest.yml`) generates the GitHub-hosted provenance attestation for the binary checksums.
- The release workflow builds amd64 and arm64 apks with melange on native GitHub-hosted runners, assembles and publishes `ghcr.io/meigma/template-go-api:vX.Y.Z` as a multi-platform manifest with apko, signs it with keyless cosign, and attaches a syft SBOM attestation; the isolated `attest.yml` workflow then creates the GitHub-native provenance attestation for the manifest digest.
- Generating both provenance attestations in the isolated `attest.yml` reusable workflow (not in the build job) keeps the signing identity unreachable by build steps — the SLSA Build L3 isolation requirement — while staying on GitHub's attestation API (verify with `gh attestation verify --signer-workflow …/attest.yml`).
- A human inspects the draft release before publication.

The root `ghd.toml` matches the default GoReleaser output so generated projects can be installed with `ghd` once the release workflow runs.
Expand Down
2 changes: 1 addition & 1 deletion ghd.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version = 1

[provenance]
signer_workflow = "meigma/template-go-api/.github/workflows/release.yml"
signer_workflow = "meigma/template-go-api/.github/workflows/attest.yml"

[[packages]]
name = "template-go-api"
Expand Down