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'] diff --git a/skills/github-release/SKILL.md b/skills/github-release/SKILL.md index 4aed907..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 -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 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 2aa6d93..43e7955 100644 --- a/skills/github-release/checkpoints.yaml +++ b/skills/github-release/checkpoints.yaml @@ -88,14 +88,18 @@ 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(\"\\\\.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: - 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 — 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. - 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/references/release-process.md b/skills/github-release/references/release-process.md index b0a2903..10c8733 100644 --- a/skills/github-release/references/release-process.md +++ b/skills/github-release/references/release-process.md @@ -45,15 +45,25 @@ 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 # 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 branch, 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 `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. ### 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 ``` 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