From 0657a9ecf1d8adbf3e67a42013ee5de536f4c974 Mon Sep 17 00:00:00 2001 From: BertMarti <107127310+BertMarti@users.noreply.github.com> Date: Thu, 5 Nov 2026 13:04:30 +0100 Subject: [PATCH 1/7] ci(release): atestaciones de procedencia de los binarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release.yml firma con actions/attest-build-provenance (fijada por SHA, v4.2.2) una atestación de procedencia de cada binario, con subject-checksums sobre el checksums.txt que --check acaba de comparar con release/checksums.txt y antes de subirlos. id-token: write y attestations: write solo en el job binaries. La Action no cambia: sigue confiando solo en release/checksums.txt de su árbol. TestReleaseAttestsTheBinaries vigila permisos, orden y que action.yml no use atestaciones. Hecho por Claude. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 20 ++++++++++++++++++ internal/gallery/action_test.go | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3541e7d..612607d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,6 +6,12 @@ name: Release # release/checksums.txt committed in that tag: that file is what the Action # trusts. Without it, nothing is uploaded and the Action keeps compiling with # go run. +# +# Each binary also gets a signed build provenance attestation (Sigstore, free +# for public repositories), only so that people can check a binary they +# downloaded by hand: gh attestation verify --repo BertMarti/commitling. +# The Action does not use attestations (they would need a token and gh): it +# still trusts only release/checksums.txt of its own tree. on: release: types: [published] @@ -20,8 +26,13 @@ jobs: binaries: name: binarios runs-on: ubuntu-latest + # contents: upload the binaries to the release; id-token: the OIDC token + # that Sigstore turns into the short-lived signing certificate; + # attestations: store the attestation in this repository. permissions: contents: write + id-token: write + attestations: write env: TAG: ${{ github.event.release.tag_name }} steps: @@ -55,6 +66,14 @@ jobs: if: steps.check.outputs.upload == 'true' run: scripts/build-release.sh "$RUNNER_TEMP/dist" --check + # Only after --check: what gets attested is exactly what + # release/checksums.txt lists (same names and SHA-256). + - name: Atestar la procedencia de los binarios + if: steps.check.outputs.upload == 'true' + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-checksums: ${{ runner.temp }}/dist/checksums.txt + - name: Subir los binarios a la release if: steps.check.outputs.upload == 'true' env: @@ -68,4 +87,5 @@ jobs: echo '```' cat "$RUNNER_TEMP/dist/checksums.txt" echo '```' + echo "Cada binario tiene una atestación de procedencia: \`gh attestation verify --repo $GH_REPO\`." } >> "$GITHUB_STEP_SUMMARY" diff --git a/internal/gallery/action_test.go b/internal/gallery/action_test.go index 2955006..559940e 100644 --- a/internal/gallery/action_test.go +++ b/internal/gallery/action_test.go @@ -436,3 +436,40 @@ func TestBuildReleaseResetsTheEnvironment(t *testing.T) { t.Error("build-release.sh must export GOFLAGS=-p=2 GOEXPERIMENT= GOFIPS140=off right after set -euo pipefail and before go build") } } + +// The release workflow attests the provenance of the binaries it uploads, so +// that people can check a downloaded binary with gh attestation verify. Only +// the binaries job may mint an OIDC token and write attestations, only after +// --check (what is attested is exactly release/checksums.txt) and before the +// upload. The Action itself keeps trusting only release/checksums.txt. +func TestReleaseAttestsTheBinaries(t *testing.T) { + yml := readRepoFile(t, ".github/workflows/release.yml") + if !regexp.MustCompile(`(?m)^permissions: \{\}$`).MatchString(yml) { + t.Error("release.yml must start from permissions: {}") + } + for _, perm := range []string{"id-token: write", "attestations: write"} { + if n := strings.Count(yml, perm); n != 1 { + t.Errorf("release.yml grants %q %d times, want once (binaries job)", perm, n) + } + job := strings.Index(yml, " binaries:\n") + if i := strings.Index(yml, " "+perm+"\n"); job < 0 || i < job { + t.Errorf("%q must be a permission of the binaries job", perm) + } + } + check := strings.Index(yml, `scripts/build-release.sh "$RUNNER_TEMP/dist" --check`) + attest := strings.Index(yml, "uses: actions/attest-build-provenance@") + upload := strings.Index(yml, "gh release upload ") + if check < 0 || attest < check || upload < attest { + t.Error("release.yml must attest after build-release.sh --check and before gh release upload") + } + if !strings.Contains(yml[attest:upload], "subject-checksums: ${{ runner.temp }}/dist/checksums.txt") || + !strings.Contains(yml[attest:upload], "if: steps.check.outputs.upload == 'true'") { + t.Error("the attestation must cover dist/checksums.txt and run only when binaries are uploaded") + } + action := readRepoFile(t, "action.yml") + for _, bad := range []string{"attestation", "attest-build-provenance", "id-token"} { + if strings.Contains(action, bad) { + t.Errorf("action.yml mentions %q: the Action must trust only release/checksums.txt", bad) + } + } +} From 1ab171343d2bb709bab8d8b80395931be4794416 Mon Sep 17 00:00:00 2001 From: BertMarti <107127310+BertMarti@users.noreply.github.com> Date: Thu, 5 Nov 2026 13:04:30 +0100 Subject: [PATCH 2/7] =?UTF-8?q?feat(web):=20filtro=20=C2=ABVer=20en=20la?= =?UTF-8?q?=20galer=C3=ADa=C2=BB=20y=20secci=C3=B3n=20=C2=ABBinarios=20ver?= =?UTF-8?q?ificables=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Selector de especie en la galería (Todas o una sola: fases, ánimos, accesorios y tarjeta compacta), progresivo: sin JavaScript se ve todo. Sin almacenamiento (origen compartido), sin tocar location.hash, valor validado contra la lista cerrada de la carga; los enlaces «Ir a:» y hashchange cambian el filtro si la especie estaba oculta; mensaje en role=status. - «Binarios verificables» en Privacidad (y enlace «Verificar» en la cabecera): cómo comprobar un binario con gh attestation verify. - Tests: page.test.js (Node, 7 escenarios), TestGalleryHasSpeciesFilter, TestPageExplainsAttestations; recuentos de botones de copia y opciones marcadas al día. Ninguna tarjeta SVG cambia. Hecho por GPT-5.6 Luna (delegado); revisado, corregido (lista cerrada fijada al cargar, bloques compactos en el test, gh auth login) y probado en Chromium por Claude. Co-Authored-By: Claude Opus 5.5 --- internal/gallery/gallery.go | 3 +- internal/gallery/gallery_test.go | 8 +- internal/gallery/generator_test.go | 6 +- internal/gallery/index.html.tmpl | 18 +++++ internal/gallery/page.js | 49 ++++++++++++ internal/gallery/page_test.go | 70 +++++++++++++++++ internal/gallery/style.css | 3 + internal/gallery/testdata/page.test.js | 101 +++++++++++++++++++++++++ 8 files changed, 250 insertions(+), 8 deletions(-) create mode 100644 internal/gallery/page_test.go create mode 100644 internal/gallery/testdata/page.test.js diff --git a/internal/gallery/gallery.go b/internal/gallery/gallery.go index 971f772..56d5f38 100644 --- a/internal/gallery/gallery.go +++ b/internal/gallery/gallery.go @@ -120,6 +120,7 @@ type speciesSection struct { // badgeSection is the compact badges of one species: one per stage. type badgeSection struct { + ID string Name string Figures []Figure } @@ -238,7 +239,7 @@ func Build(dir, version string) (int, error) { p.Species = append(p.Species, sec) // The compact badge of every stage, happy and with no accessories. - badges := badgeSection{Name: sp.Name()} + badges := badgeSection{ID: sp.Slug(), Name: sp.Name()} for _, st := range creature.Stages { f, err := card("compact-"+prefix+st.Slug(), sp, Sample(st, creature.Happy), render.Compact) if err != nil { diff --git a/internal/gallery/gallery_test.go b/internal/gallery/gallery_test.go index 083ad0b..dfdb9e3 100644 --- a/internal/gallery/gallery_test.go +++ b/internal/gallery/gallery_test.go @@ -188,8 +188,8 @@ func TestPageHeadForSharing(t *testing.T) { func TestCopyButtonsAreAccessible(t *testing.T) { page := buildPage(t) buttons := regexp.MustCompile(``).FindAllString(page, -1) - if len(buttons) != 4 { - t.Fatalf("found %d copy buttons, want 4", len(buttons)) + if len(buttons) != 5 { + t.Fatalf("found %d copy buttons, want 5", len(buttons)) } labels := map[string]bool{} for _, b := range buttons { @@ -210,8 +210,8 @@ func TestCopyButtonsAreAccessible(t *testing.T) { t.Errorf("copy button must be type=button: %s", b) } } - if got := strings.Count(page, `class="copy-msg note" role="status" aria-live="polite"`); got != 4 { - t.Errorf("found %d live regions for copy feedback, want 4", got) + if got := strings.Count(page, `class="copy-msg note" role="status" aria-live="polite"`); got != 5 { + t.Errorf("found %d live regions for copy feedback, want 5", got) } // If the clipboard fails the user gets a message and the text is selected. for _, want := range []string{"No se pudo copiar", "navigator.clipboard", "execCommand", "selectNodeContents"} { diff --git a/internal/gallery/generator_test.go b/internal/gallery/generator_test.go index 0072632..381cb55 100644 --- a/internal/gallery/generator_test.go +++ b/internal/gallery/generator_test.go @@ -77,9 +77,9 @@ func TestGeneratorFormIsAccessible(t *testing.T) { } // One species, one theme and one size in the generator, the same three // in the workflow generator of «Instálalo en tu perfil», and «Sistema» in - // the theme picker of the page. - if got := strings.Count(page, `checked>`); got != 7 { - t.Errorf("%d options checked by default, want 7 (species, theme and size, in each form, and the page theme)", got) + // the theme picker of the page, and «Todas» in the gallery filter. + if got := strings.Count(page, `checked>`); got != 8 { + t.Errorf("%d options checked by default, want 8 (species, theme and size, in each form, the page theme and the gallery filter)", got) } if !regexp.MustCompile(`]* checked>`).MatchString(page) { t.Error("the full card must be the default size") diff --git a/internal/gallery/index.html.tmpl b/internal/gallery/index.html.tmpl index c4448b5..55140c7 100644 --- a/internal/gallery/index.html.tmpl +++ b/internal/gallery/index.html.tmpl @@ -39,6 +39,7 @@ Galería Reglas Instalar + Verificar Código