From 0ecee05215b26c4d4831f965d024a15aa7922044 Mon Sep 17 00:00:00 2001 From: Jon Bogaty Date: Mon, 24 Aug 2026 15:24:19 -0500 Subject: [PATCH 1/2] fix(release): authorize sealed draft verifier --- .github/workflows/release.yml | 5 ++++- tests/releasecontrol/release_control_test.go | 9 +++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e209b1d..ca165c39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -430,7 +430,10 @@ jobs: - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Verify exact sealed release bytes env: - RELEASE_GH_TOKEN: ${{ github.token }} + # Draft releases are intentionally invisible to contents:read tokens. + # Keep the verifier read-only while using the organization release + # credential that is authorized to inspect the draft. + RELEASE_GH_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} run: bash scripts/ci/verify_release_assets.sh "${GITHUB_REF_NAME#v}" package-manifests: diff --git a/tests/releasecontrol/release_control_test.go b/tests/releasecontrol/release_control_test.go index 763e6e1e..ef94615d 100644 --- a/tests/releasecontrol/release_control_test.go +++ b/tests/releasecontrol/release_control_test.go @@ -580,11 +580,12 @@ func TestReleaseToolingIsPinnedAndPermissionsAreLeastPrivilege(t *testing.T) { requireContains(t, signer, "contents: write", path) requireContains(t, signer, "id-token: write", path) requireContains(t, signer, `gh release upload "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY"`, path) - // 5 references: the admission presence check, the three immutable-release - // gates, and the admission draft read (which uses CI_GITHUB_TOKEN because the + // 6 references: the admission presence check, the three immutable-release + // gates, the admission draft read, and the sealed-draft verifier (which use + // CI_GITHUB_TOKEN because the // built-in token is contents: read and cannot see draft releases). - if got := strings.Count(workflow, "secrets.CI_GITHUB_TOKEN"); got != 5 { - t.Errorf("%s CI_GITHUB_TOKEN secret references = %d, want 5", path, got) + if got := strings.Count(workflow, "secrets.CI_GITHUB_TOKEN"); got != 6 { + t.Errorf("%s CI_GITHUB_TOKEN secret references = %d, want 6", path, got) } requireNotContains(t, workflow, "\nenv:\n CI_GITHUB_TOKEN:", path) From 0e01c4b20b68d7728a5e8d93f19fbc5d9901e0f8 Mon Sep 17 00:00:00 2001 From: Jon Bogaty Date: Mon, 24 Aug 2026 15:43:30 -0500 Subject: [PATCH 2/2] fix(release): scope draft verifier credential --- .github/workflows/release.yml | 10 +++++----- tests/releasecontrol/release_control_test.go | 13 ++++++++----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ca165c39..f75c829a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -424,16 +424,16 @@ jobs: needs.release-seal.result == 'success')) runs-on: ubuntu-latest permissions: - contents: read + # GitHub hides drafts from contents:read tokens. This short-lived, + # repository-scoped token can inspect the sealed draft without exposing + # the organization credential used by release publication and automerge. + contents: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: Verify exact sealed release bytes env: - # Draft releases are intentionally invisible to contents:read tokens. - # Keep the verifier read-only while using the organization release - # credential that is authorized to inspect the draft. - RELEASE_GH_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} + RELEASE_GH_TOKEN: ${{ github.token }} run: bash scripts/ci/verify_release_assets.sh "${GITHUB_REF_NAME#v}" package-manifests: diff --git a/tests/releasecontrol/release_control_test.go b/tests/releasecontrol/release_control_test.go index ef94615d..66c26871 100644 --- a/tests/releasecontrol/release_control_test.go +++ b/tests/releasecontrol/release_control_test.go @@ -580,13 +580,16 @@ func TestReleaseToolingIsPinnedAndPermissionsAreLeastPrivilege(t *testing.T) { requireContains(t, signer, "contents: write", path) requireContains(t, signer, "id-token: write", path) requireContains(t, signer, `gh release upload "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY"`, path) - // 6 references: the admission presence check, the three immutable-release - // gates, the admission draft read, and the sealed-draft verifier (which use - // CI_GITHUB_TOKEN because the + // 5 references: the admission presence check, the three immutable-release + // gates, and the admission draft read (which use CI_GITHUB_TOKEN because the // built-in token is contents: read and cannot see draft releases). - if got := strings.Count(workflow, "secrets.CI_GITHUB_TOKEN"); got != 6 { - t.Errorf("%s CI_GITHUB_TOKEN secret references = %d, want 6", path, got) + if got := strings.Count(workflow, "secrets.CI_GITHUB_TOKEN"); got != 5 { + t.Errorf("%s CI_GITHUB_TOKEN secret references = %d, want 5", path, got) } + verify := requireWorkflowJob(t, workflow, "verify-sealed-release", path) + requireContains(t, verify, "contents: write", path) + requireContains(t, verify, "RELEASE_GH_TOKEN: ${{ github.token }}", path) + requireNotContains(t, verify, "secrets.CI_GITHUB_TOKEN", path) requireNotContains(t, workflow, "\nenv:\n CI_GITHUB_TOKEN:", path) const cdPath = ".github/workflows/cd.yml"