From 7f04b9add1ca2455410f56b9248fa8fef7c63f76 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Tue, 5 May 2026 20:21:05 +0200 Subject: [PATCH 1/4] fix(release): use .sigstore.json extension for cosign output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OSSF Scorecard's Signed-Releases check pattern-matches against a fixed allowlist of signature extensions: .sig, .asc, .minisig, .sigstore, .sigstore.json, .intoto.jsonl. The .bundle extension that cosign writes by default is NOT on that list, so cosign-signed releases using .bundle score 0/10 on Signed-Releases. The bytes inside the file are identical to the Sigstore bundle JSON format — only the filename matters for tooling detection. cosign verify-blob --bundle works identically with either extension. Changes: - templates/release-generic.yml: emit ${file}.sigstore.json instead of ${file}.bundle, with an inline comment explaining why - checkpoints.yaml GR-10: accept .sigstore.json (preferred) and keep .bundle (legacy) so existing releases don't fail; describe the Scorecard implication in the desc - evals/evals.json scenario 28: update expected_output and expectations to reflect both extensions and the Scorecard rule Reference upstream change to the netresearch shared workflows: https://github.com/netresearch/typo3-ci-workflows/pull/84 Signed-off-by: Sebastian Mendel --- skills/github-release/checkpoints.yaml | 18 ++++++++++++------ skills/github-release/evals/evals.json | 7 ++++--- .../templates/release-generic.yml | 11 ++++++++++- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/skills/github-release/checkpoints.yaml b/skills/github-release/checkpoints.yaml index 2aa6d93..12b3e11 100644 --- a/skills/github-release/checkpoints.yaml +++ b/skills/github-release/checkpoints.yaml @@ -88,14 +88,20 @@ mechanical: - id: GR-10 type: command - pattern: "gh release view --json assets --jq -e '[.assets[].name] as $n | ($n|any(test(\"\\\\.bundle$\"))) or ($n|any(test(\"\\\\.sig$\")) and ($n|any(test(\"\\\\.pem$\"))))' >/dev/null 2>&1" + pattern: "gh release view --json assets --jq -e '[.assets[].name] as $n | ($n|any(test(\"\\\\.sigstore\\\\.json$\"))) or ($n|any(test(\"\\\\.bundle$\"))) or ($n|any(test(\"\\\\.sig$\")) and ($n|any(test(\"\\\\.pem$\"))))' >/dev/null 2>&1" severity: warning desc: >- - Latest published release should have cosign signature artefacts: - either a modern `.bundle` (Sigstore bundle), or BOTH a detached `.sig` - and matching `.pem` certificate (used by netresearch/skill-repo-skill - release workflow). A lone `.sig` or lone `.pem` is incomplete and - fails. `jq -e` ensures the boolean is reflected in the exit code. + Latest published release should have cosign signature artefacts. + Accepted (in priority order): `.sigstore.json` (preferred — the + OSSF Scorecard Signed-Releases check pattern-matches against a fixed + allowlist that includes `.sigstore.json` but NOT `.bundle`, so + `.sigstore.json` is required for a non-zero Signed-Releases score), + `.bundle` (legacy cosign default — bytes are identical to + `.sigstore.json`, only the filename differs), or BOTH a detached + `.sig` and matching `.pem` certificate (used by + netresearch/skill-repo-skill release workflow). A lone `.sig` or + lone `.pem` is incomplete and fails. `jq -e` ensures the boolean + is reflected in the exit code. - id: GR-11 type: command diff --git a/skills/github-release/evals/evals.json b/skills/github-release/evals/evals.json index d19cacd..1791e19 100644 --- a/skills/github-release/evals/evals.json +++ b/skills/github-release/evals/evals.json @@ -752,14 +752,15 @@ { "id": 28, "prompt": "Is the latest release signed with cosign?", - "expected_output": "Query the latest GitHub release assets. Check for cosign signature bundles (.bundle files) and attestation artifacts. Report signing status. If missing, recommend adding cosign signing to the release workflow.", + "expected_output": "Query the latest GitHub release assets. Check for cosign signature bundles (preferred extension `.sigstore.json`, with `.bundle` accepted as legacy) and attestation artifacts. Report signing status. If missing or using only `.bundle`, recommend `.sigstore.json` for OSSF Scorecard Signed-Releases compliance.", "expectations": [ "Queries release assets", - "Checks for .bundle files", + "Checks for .sigstore.json or .bundle signature files", "Reports signing status", "Recommends fix if missing", + "Notes OSSF Scorecard requires .sigstore.json (or another allowlisted extension) — .bundle alone scores 0/10", "Does NOT: Assume signing exists without checking", - "Does NOT: Skip bundle file inspection" + "Does NOT: Skip signature file inspection" ], "assertions": [ { diff --git a/skills/github-release/templates/release-generic.yml b/skills/github-release/templates/release-generic.yml index 2559371..2d54045 100644 --- a/skills/github-release/templates/release-generic.yml +++ b/skills/github-release/templates/release-generic.yml @@ -40,8 +40,17 @@ jobs: uses: sigstore/cosign-installer@v3 - name: Sign artifacts run: | + # Use .sigstore.json extension (NOT cosign's default .bundle). + # OSSF Scorecard's Signed-Releases check pattern-matches against a + # fixed allowlist of signature extensions (.sig, .asc, .minisig, + # .sigstore, .sigstore.json, .intoto.jsonl). The .bundle extension + # cosign writes by default is NOT on that list, so cosign-signed + # releases score 0/10. The bytes inside the bundle file ARE the + # Sigstore bundle JSON format — the rename is purely cosmetic for + # tooling detection. `cosign verify-blob --bundle file.sigstore.json` + # works identically. for f in dist/*; do - cosign sign-blob "$f" --bundle "${f}.bundle" --yes + cosign sign-blob "$f" --bundle "${f}.sigstore.json" --yes done - name: Generate attestation uses: actions/attest-build-provenance@v2 From 51dec62df26e7f2d103bd18fc43fad0a3699065f Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Tue, 5 May 2026 20:21:24 +0200 Subject: [PATCH 2/4] docs(release): capture sigstore extension and merge-commit tagging lessons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two lessons surfaced during the netresearch/t3x-nr-passkeys-be v0.9.0 release that the skill was missing: 1. .sigstore.json vs .bundle for cosign output OSSF Scorecard's Signed-Releases check only recognizes a fixed allowlist of signature extensions; .bundle (cosign's default) is not on it. Add a dedicated subsection to references/supply-chain-security.md explaining the allowlist, why the rename is purely cosmetic (bytes are the Sigstore bundle JSON regardless of filename), the concrete workflow snippet, and the "past releases can't be retroactively fixed" caveat (Scorecard averages over the last 4 releases, so the score climbs gradually). References upstream PR netresearch/typo3-ci-workflows#84. 2. Tag the merge commit, not the release branch tip The release-prep PR pattern was already documented across SKILL.md, release-prepare.md, and recovery-procedures.md, but none of them spelled out *which commit* to tag after the PR merges. Add the "checkout main && pull, then tag" reasoning to release-process.md Phase 3, and a brief affirmative note to SKILL.md step 5 stating that a release/vX.Y.Z PR is preferred over direct main pushes (even though many tutorials show direct pushes — branch protection often blocks them anyway, and the PR gives CI one more chance to verify the version bump). The companion commit fixes the template, checkpoint, and eval that embodied the .bundle pattern — these docs explain the *why*. Signed-off-by: Sebastian Mendel --- skills/github-release/SKILL.md | 4 +- .../references/release-process.md | 12 ++++-- .../references/supply-chain-security.md | 38 ++++++++++++++++++- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/skills/github-release/SKILL.md b/skills/github-release/SKILL.md index 4aed907..9208d95 100644 --- a/skills/github-release/SKILL.md +++ b/skills/github-release/SKILL.md @@ -26,8 +26,8 @@ These commands are blocked by hooks. GitHub immutable releases (GA Oct 2025) mak 2. **Determine next version** — based on conventional commits or user input (major/minor/patch) 3. **Bump version files** — update all ecosystem-specific version files consistently 4. **Update CHANGELOG.md** — add release section with date and changes -5. **Create release branch and PR** — `release/vX.Y.Z` branch, open PR for review -6. **After PR merge** — create signed annotated tag: `git tag -s vX.Y.Z -m "vX.Y.Z"` +5. **Create release branch and PR** — `release/vX.Y.Z` branch, open PR for review (always prefer a PR over pushing version bumps directly to `main`, even though many tutorials show direct pushes — branch protection often blocks them anyway, and the PR gives CI one final chance to verify the version bump didn't break anything) +6. **After PR merge** — `git checkout main && git pull`, then create signed annotated tag on the **merge commit**: `git tag -s vX.Y.Z -m "vX.Y.Z"` (NOT from the `release/vX.Y.Z` branch tip — see `references/release-process.md` Phase 3) 7. **Push tag** — `git push origin vX.Y.Z` triggers CI workflow 8. **CI publishes release** — with artifacts, checksums, and auto-generated release notes 9. **Overhaul release description** — rewrite the auto-generated notes into a narrative summary in a local file, then apply with `gh release edit vX.Y.Z --notes-file notes.md`. Use `--notes-file` (not `--notes "..."`) so multi-line Markdown doesn't trip over shell quoting. diff --git a/skills/github-release/references/release-process.md b/skills/github-release/references/release-process.md index b0a2903..70fe492 100644 --- a/skills/github-release/references/release-process.md +++ b/skills/github-release/references/release-process.md @@ -45,15 +45,19 @@ The hooks in this repository block `gh release create` and `gh release delete` t After the PR is merged into main: ``` -1. git checkout main && git pull -2. git tag -s vX.Y.Z -m "vX.Y.Z" -3. git push origin vX.Y.Z +1. git checkout main && git pull # fast-forward to the merge commit +2. git tag -s vX.Y.Z -m "vX.Y.Z" # tags HEAD == the merge commit +3. git push origin vX.Y.Z # release orchestrator picks it up ``` The tag MUST be: - **Annotated** (`-a` or `-s`), never lightweight - **Signed** (`-s` for GPG/SSH signing) — required for SLSA L1+ -- **On the merge commit** — not on the branch, not on an older commit +- **On the merge commit** — not on the `release/vX.Y.Z` branch tip, not on an older commit + +**Why the merge commit and not the branch HEAD:** the `release/vX.Y.Z` branch's tip is the version-bump commit *before* it was merged. After the PR merges, `main` advances to either that same commit (fast-forward / squash) or a new merge commit (merge-commit strategy). The tag must point to whatever is now the tip of `main` — that is what consumers will check out, what CI will build artifacts from, and what shows up as "the release commit" on the GitHub release page. Tagging the branch tip directly skips the merge commit on merge-commit-strategy repos and produces a tag that points to a commit that's not reachable from `main` cleanly. + +In practice: do NOT `git tag` from inside the worktree on the `release/vX.Y.Z` branch. Switch to `main`, pull, then tag — the steps above already enforce this order. ### Phase 4: CI Release Workflow diff --git a/skills/github-release/references/supply-chain-security.md b/skills/github-release/references/supply-chain-security.md index 0cd32e9..d02f7eb 100644 --- a/skills/github-release/references/supply-chain-security.md +++ b/skills/github-release/references/supply-chain-security.md @@ -35,15 +35,51 @@ GitHub Actions natively supports SLSA L1 via `actions/attest-build-provenance`. ```yaml - uses: sigstore/cosign-installer@v3 -- run: cosign sign-blob --yes --oidc-issuer https://token.actions.githubusercontent.com artifact.tar.gz +- run: | + cosign sign-blob --yes \ + --oidc-issuer https://token.actions.githubusercontent.com \ + --bundle artifact.tar.gz.sigstore.json \ + artifact.tar.gz ``` +### Output extension matters for OSSF Scorecard + +**Use `.sigstore.json` for the cosign bundle output, NOT cosign's default `.bundle`.** + +OSSF Scorecard's [Signed-Releases](https://github.com/ossf/scorecard/blob/main/docs/checks.md#signed-releases) check pattern-matches release-asset filenames against a fixed allowlist of signature extensions: + +- `.sig` +- `.asc` +- `.minisig` +- `.sigstore` +- `.sigstore.json` +- `.intoto.jsonl` + +The `.bundle` extension that `cosign sign-blob --bundle` writes by default is **not** on that list, so cosign-signed releases that use `.bundle` are reported as unsigned (`Signed-Releases` score `0/10`). + +**The bytes inside the file are identical** — cosign's bundle format IS the Sigstore bundle JSON. Only the filename matters for tooling detection. `cosign verify-blob --bundle file.sigstore.json` works exactly the same as `--bundle file.bundle`. + +**Concrete fix in a workflow:** + +```yaml +# Wrong — Scorecard sees this as unsigned +cosign sign-blob --yes "$file" --bundle "${file}.bundle" + +# Right — Scorecard recognizes this as signed +cosign sign-blob --yes "$file" --bundle "${file}.sigstore.json" +``` + +**Past releases cannot be retroactively fixed.** GitHub releases are immutable once assets are attached, so renaming or replacing assets on already-published releases is not possible. Only future releases benefit from the fix. Scorecard averages the Signed-Releases score over the **last 4 releases**, so the score climbs gradually as new releases ship. + +Reference upstream change for the netresearch shared workflows: [netresearch/typo3-ci-workflows#84](https://github.com/netresearch/typo3-ci-workflows/pull/84) — applied to both `release.yml` and `release-typo3-extension.yml`. + ### Verification ```bash cosign verify-blob \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp "github.com/org/repo" \ + --bundle artifact.tar.gz.sigstore.json \ artifact.tar.gz ``` From 8f217b079adcf0ca542b4d7cb4e8fd7a10ad5479 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Tue, 5 May 2026 23:08:25 +0200 Subject: [PATCH 3/4] docs(release): trim SKILL.md, generalize merge-strategy phrasing, add .sigstore to checkpoint - SKILL.md: trim to 490 words (was 547, max 500); make step 6 strategy-agnostic by tagging main's HEAD rather than asserting "merge commit" - references/release-process.md: clarify that main's post-merge HEAD differs by merge strategy (fast-forward / squash / merge-commit); fix incorrect claim that release branch tip is "not reachable from main cleanly" - checkpoints.yaml: extend GR-10 to accept bare .sigstore extension (also on the OSSF Scorecard Signed-Releases allowlist), alongside .sigstore.json, .bundle, and .sig+.pem Signed-off-by: Sebastian Mendel --- skills/github-release/SKILL.md | 12 ++++++------ skills/github-release/checkpoints.yaml | 14 ++++++-------- .../github-release/references/release-process.md | 14 ++++++++++---- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/skills/github-release/SKILL.md b/skills/github-release/SKILL.md index 9208d95..b399c15 100644 --- a/skills/github-release/SKILL.md +++ b/skills/github-release/SKILL.md @@ -26,12 +26,12 @@ These commands are blocked by hooks. GitHub immutable releases (GA Oct 2025) mak 2. **Determine next version** — based on conventional commits or user input (major/minor/patch) 3. **Bump version files** — update all ecosystem-specific version files consistently 4. **Update CHANGELOG.md** — add release section with date and changes -5. **Create release branch and PR** — `release/vX.Y.Z` branch, open PR for review (always prefer a PR over pushing version bumps directly to `main`, even though many tutorials show direct pushes — branch protection often blocks them anyway, and the PR gives CI one final chance to verify the version bump didn't break anything) -6. **After PR merge** — `git checkout main && git pull`, then create signed annotated tag on the **merge commit**: `git tag -s vX.Y.Z -m "vX.Y.Z"` (NOT from the `release/vX.Y.Z` branch tip — see `references/release-process.md` Phase 3) +5. **Create release branch and PR** — `release/vX.Y.Z` branch, open PR for review (always use a PR; branch protection typically blocks direct pushes anyway, and CI gets one last chance to validate) +6. **After PR merge** — `git checkout main && git pull`, then tag `main`'s HEAD: `git tag -s vX.Y.Z -m "vX.Y.Z"`. Tag from `main`, not from the `release/vX.Y.Z` branch tip — see `references/release-process.md` Phase 3. 7. **Push tag** — `git push origin vX.Y.Z` triggers CI workflow 8. **CI publishes release** — with artifacts, checksums, and auto-generated release notes -9. **Overhaul release description** — rewrite the auto-generated notes into a narrative summary in a local file, then apply with `gh release edit vX.Y.Z --notes-file notes.md`. Use `--notes-file` (not `--notes "..."`) so multi-line Markdown doesn't trip over shell quoting. -10. **Do NOT re-run the release workflow after step 9** — many release workflows (e.g. `softprops/action-gh-release`) regenerate the body from the commit log on every run and will overwrite the manual overhaul. If a downstream job (TER publish, artifact upload) needs a retry, use a dedicated dispatcher workflow (see `references/ter-republish.md` for the TYPO3 pattern). +9. **Overhaul release description** — rewrite auto-generated notes into a narrative summary, apply with `gh release edit vX.Y.Z --notes-file notes.md` (use `--notes-file`, not `--notes "..."`, to avoid shell quoting issues with multi-line Markdown) +10. **Do NOT re-run the release workflow after step 9** — many workflows (e.g. `softprops/action-gh-release`) regenerate the body each run and will overwrite the overhaul. For downstream retries (TER publish, artifact upload), use a dedicated dispatcher workflow — see `references/ter-republish.md`. ## Commands @@ -50,6 +50,6 @@ These commands are blocked by hooks. GitHub immutable releases (GA Oct 2025) mak - `references/ecosystem-detection.md` — version file patterns per ecosystem - `references/immutable-releases.md` — GitHub immutable releases and tag burning - `references/supply-chain-security.md` — SLSA, Sigstore, SBOMs, attestations -- `references/recovery-procedures.md` — burned tags, stuck drafts, version drift, release-body clobbering after manual overhaul, mis-tagged SemVer releases, branch-protection gotchas -- `references/ter-republish.md` — TYPO3 Extension Repository re-publish patterns (workflow_dispatch-only caller, codepoint-safe comment truncation, v-prefix + bare-version tag compatibility) +- `references/recovery-procedures.md` — burned tags, stuck drafts, version drift, release-body clobbering, mis-tagged SemVer releases, branch-protection gotchas +- `references/ter-republish.md` — TYPO3 Extension Repository re-publish patterns - `references/ci-workflow-templates.md` — CI workflow structure and templates diff --git a/skills/github-release/checkpoints.yaml b/skills/github-release/checkpoints.yaml index 12b3e11..43e7955 100644 --- a/skills/github-release/checkpoints.yaml +++ b/skills/github-release/checkpoints.yaml @@ -88,17 +88,15 @@ mechanical: - id: GR-10 type: command - pattern: "gh release view --json assets --jq -e '[.assets[].name] as $n | ($n|any(test(\"\\\\.sigstore\\\\.json$\"))) or ($n|any(test(\"\\\\.bundle$\"))) or ($n|any(test(\"\\\\.sig$\")) and ($n|any(test(\"\\\\.pem$\"))))' >/dev/null 2>&1" + pattern: "gh release view --json assets --jq -e '[.assets[].name] as $n | ($n|any(test(\"\\\\.sigstore\\\\.json$\"))) or ($n|any(test(\"\\\\.sigstore$\"))) or ($n|any(test(\"\\\\.bundle$\"))) or ($n|any(test(\"\\\\.sig$\")) and ($n|any(test(\"\\\\.pem$\"))))' >/dev/null 2>&1" severity: warning desc: >- Latest published release should have cosign signature artefacts. - Accepted (in priority order): `.sigstore.json` (preferred — the - OSSF Scorecard Signed-Releases check pattern-matches against a fixed - allowlist that includes `.sigstore.json` but NOT `.bundle`, so - `.sigstore.json` is required for a non-zero Signed-Releases score), - `.bundle` (legacy cosign default — bytes are identical to - `.sigstore.json`, only the filename differs), or BOTH a detached - `.sig` and matching `.pem` certificate (used by + Accepted (in priority order): `.sigstore.json` (preferred — on the + OSSF Scorecard Signed-Releases allowlist), `.sigstore` (also on the + Scorecard allowlist), `.bundle` (legacy cosign default — bytes are + identical to `.sigstore.json`, only the filename differs), or BOTH + a detached `.sig` and matching `.pem` certificate (used by netresearch/skill-repo-skill release workflow). A lone `.sig` or lone `.pem` is incomplete and fails. `jq -e` ensures the boolean is reflected in the exit code. diff --git a/skills/github-release/references/release-process.md b/skills/github-release/references/release-process.md index 70fe492..10c8733 100644 --- a/skills/github-release/references/release-process.md +++ b/skills/github-release/references/release-process.md @@ -45,17 +45,23 @@ The hooks in this repository block `gh release create` and `gh release delete` t After the PR is merged into main: ``` -1. git checkout main && git pull # fast-forward to the merge commit -2. git tag -s vX.Y.Z -m "vX.Y.Z" # tags HEAD == the merge commit +1. git checkout main && git pull # advance to main's post-merge HEAD +2. git tag -s vX.Y.Z -m "vX.Y.Z" # tags main's HEAD 3. git push origin vX.Y.Z # release orchestrator picks it up ``` The tag MUST be: - **Annotated** (`-a` or `-s`), never lightweight - **Signed** (`-s` for GPG/SSH signing) — required for SLSA L1+ -- **On the merge commit** — not on the `release/vX.Y.Z` branch tip, not on an older commit +- **On `main`'s HEAD after the PR merges** — not on the `release/vX.Y.Z` branch tip, not on an older commit -**Why the merge commit and not the branch HEAD:** the `release/vX.Y.Z` branch's tip is the version-bump commit *before* it was merged. After the PR merges, `main` advances to either that same commit (fast-forward / squash) or a new merge commit (merge-commit strategy). The tag must point to whatever is now the tip of `main` — that is what consumers will check out, what CI will build artifacts from, and what shows up as "the release commit" on the GitHub release page. Tagging the branch tip directly skips the merge commit on merge-commit-strategy repos and produces a tag that points to a commit that's not reachable from `main` cleanly. +**Why `main`'s post-merge HEAD and not the release branch tip:** depending on the project's merge strategy, `main`'s HEAD after merge is one of: + +- The original branch-tip commit, if the PR was fast-forwarded; +- A new squash commit, if the PR was squash-merged; +- A new merge commit, if the PR was merge-committed. + +The tag must point to whatever is now the tip of `main` — that is what consumers will check out, what CI will build artifacts from, and what shows up as "the release commit" on the GitHub release page. With squash- or merge-commit strategies, the `release/vX.Y.Z` branch tip is *not* on `main`'s first-parent history, so tagging it produces a tag that doesn't correspond to any commit on `main`. In practice: do NOT `git tag` from inside the worktree on the `release/vX.Y.Z` branch. Switch to `main`, pull, then tag — the steps above already enforce this order. From 87e52c69459b7366ad75ee3fb9cce7a2353ab8a1 Mon Sep 17 00:00:00 2001 From: Sebastian Mendel Date: Tue, 5 May 2026 23:11:22 +0200 Subject: [PATCH 4/4] chore(lint): add repo-local yamllint config with 300-char line limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reusable validate workflow's inline yamllint default sets line-length.max to 200, which rejects the GR-10 checkpoint pattern (now 280 chars after adding the .sigstore branch). Add a repo-local .yamllint.yml so the workflow uses the project's config instead of the inline default — long single-line jq patterns in checkpoints.yaml are intentional and shouldn't be split. Signed-off-by: Sebastian Mendel --- .yamllint.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .yamllint.yml diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..ce0e69d --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,13 @@ +extends: default + +rules: + comments: + min-spaces-from-content: 1 + document-start: disable + indentation: disable + # Checkpoint patterns embed jq expressions that can be long single-line strings; + # raise the limit to accommodate them without splitting and risking regex changes. + line-length: + max: 300 + truthy: + allowed-values: ['true', 'false', 'on']