Skip to content

Commit c4f73e7

Browse files
donislawdevclaude
andcommitted
release: every workflow names the predicate type the statement actually carries
The third thing the first real release found, and this refusal was correct. Phase C stopped with "the release notes tell people to pass --predicate-type https://spdx.dev/Document and the statement that was just made is https://spdx.dev/Document/v2.3". That check exists precisely because the provenance plan wrote the URI down as a promise taken from the action's documentation rather than from an attestation, and said out loud it was unmeasured. It has now been measured: the statement carries the versioned one. The value was written in five places across three files and only one of them was being checked. Three of the five are not checks at all - they are the commands a person copies out of the release notes, and with the wrong URI gh answers "no attestation found", which reads exactly like a release nobody attested. It cannot be written once: three workflows are three files and a workflow cannot read a constant out of another one. So the guard asks for agreement rather than for a single home - it collects every spdx.dev URI in .github/workflows and refuses when there are two different ones. The lesson worth keeping: read a predicate type out of a bundle, not out of documentation. base64 -d on dsseEnvelope.payload and predicateType is inside. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 7917adb commit c4f73e7

4 files changed

Lines changed: 63 additions & 5 deletions

File tree

‎.github/workflows/attest-release.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ jobs:
123123
# missing file rather than a wrong flag.
124124
run: |
125125
set -euo pipefail
126-
promised="https://spdx.dev/Document"
126+
promised="https://spdx.dev/Document/v2.3"
127127
actual="$(python3 - "$BUNDLE" <<'PY'
128128
import base64, json, sys
129129
bundle = json.load(open(sys.argv[1], encoding="utf-8"))

‎.github/workflows/release.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ jobs:
465465
echo
466466
echo "\`\`\`"
467467
echo "gh attestation verify <the file you downloaded> -R ${GITHUB_REPOSITORY} \\"
468-
echo " --predicate-type https://spdx.dev/Document"
468+
echo " --predicate-type https://spdx.dev/Document/v2.3"
469469
echo "\`\`\`"
470470
echo
471471
echo "The predicate type has to be given. \`gh\` asks for build provenance unless told"

‎.github/workflows/verify-release.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ jobs:
222222
gh attestation verify "$linux" -R "$GITHUB_REPOSITORY"
223223
echo "what is inside, on a signed file: $windows"
224224
gh attestation verify "$windows" -R "$GITHUB_REPOSITORY" \
225-
--predicate-type https://spdx.dev/Document
225+
--predicate-type https://spdx.dev/Document/v2.3
226226
227227
- name: the same two commands with no network to the API
228228
shell: bash
@@ -237,7 +237,7 @@ jobs:
237237
--bundle ./*.provenance.sigstore.json
238238
gh attestation verify "$windows" -R "$GITHUB_REPOSITORY" \
239239
--bundle ./*.sbom.sigstore.json \
240-
--predicate-type https://spdx.dev/Document
240+
--predicate-type https://spdx.dev/Document/v2.3
241241
242242
- name: every Windows program is signed, stamped, and by OUR certificate
243243
shell: pwsh
@@ -289,7 +289,7 @@ jobs:
289289
run: |
290290
set -euo pipefail
291291
gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body --jq .body > notes.md
292-
for promised in "gh attestation verify" "--predicate-type https://spdx.dev/Document" "verify-SHA256SUMS.txt"; do
292+
for promised in "gh attestation verify" "--predicate-type https://spdx.dev/Document/v2.3" "verify-SHA256SUMS.txt"; do
293293
grep -qF -- "$promised" notes.md || {
294294
echo "the release notes never mention: $promised"
295295
echo "This job just ran it, so the page and the check disagree about what a person should do."

‎internal/guard/attestation_test.go‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package guard
33
import (
44
"os"
55
"path/filepath"
6+
"regexp"
67
"strings"
78
"testing"
89

@@ -171,6 +172,63 @@ func TestTheAttestingHalfFetchesWhatTheChecksumsName(t *testing.T) {
171172
}
172173
}
173174

175+
// Every workflow that names the SBOM predicate type names the SAME one.
176+
//
177+
// This fact is written five times across three files, and it cannot be written
178+
// once: the notes tell a person what to type, the attesting half checks that
179+
// promise against the statement it just made, and the verifying half runs the
180+
// command for real. They are three different jobs in three different files and
181+
// a workflow cannot read a constant out of another one.
182+
//
183+
// 🔴 What it cost when they disagreed, on the first real release, on
184+
// 2026-08-28: the notes promised https://spdx.dev/Document, the statement
185+
// carried https://spdx.dev/Document/v2.3, and the attesting half stopped the
186+
// release. It was RIGHT to - the promise was written from the action's
187+
// documentation rather than from an attestation, and the provenance plan said
188+
// out loud that it was unmeasured - but the value that was wrong was written in
189+
// five places and only one of them was checked.
190+
//
191+
// The one that matters most is not the check. It is the pair of commands a
192+
// person types out of the release notes: with the wrong URI, gh answers "no
193+
// attestation found", which reads exactly like a release nobody attested.
194+
func TestEveryWorkflowNamesTheSamePredicateType(t *testing.T) {
195+
dir := filepath.Join(repoRoot(t), ".github", "workflows")
196+
entries, err := os.ReadDir(dir)
197+
if err != nil {
198+
t.Skipf("no workflows here: %v", err)
199+
}
200+
201+
uri := regexp.MustCompile(`https://spdx\.dev/[A-Za-z0-9./-]*`)
202+
found := map[string][]string{}
203+
for _, entry := range entries {
204+
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yml") {
205+
continue
206+
}
207+
body, err := os.ReadFile(filepath.Join(dir, entry.Name()))
208+
if err != nil {
209+
t.Fatalf("reading %s: %v", entry.Name(), err)
210+
}
211+
for _, match := range uri.FindAllString(string(body), -1) {
212+
found[match] = append(found[match], entry.Name())
213+
}
214+
}
215+
216+
if len(found) == 0 {
217+
t.Fatal("no workflow names the SBOM predicate type, so this guard checked nothing")
218+
}
219+
if len(found) > 1 {
220+
for value, files := range found {
221+
t.Errorf("%q is named in %v", value, files)
222+
}
223+
t.Error("the workflows disagree about the SBOM predicate type. One of them tells a " +
224+
"person what to type, one checks that promise against the statement, and one " +
225+
"runs the command for real - so a disagreement here is a release page whose " +
226+
"own instructions answer \"no attestation found\".\n" +
227+
"What to do: the value is whatever the statement actually carries. Read it out " +
228+
"of a bundle rather than out of documentation.")
229+
}
230+
}
231+
174232
func TestTheReleaseNotesSayHowToCheckWhatWasDownloaded(t *testing.T) {
175233
job := releasePublishJob(t)
176234
var notes string

0 commit comments

Comments
 (0)