Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
name: Release (artifacts + signing)
# Builds release artifacts and signs them when a `v*` tag is pushed.
#
# The tag itself is cut by scripts/release.sh: it builds the React UI, makes a
# DETACHED commit embedding internal/ui/dist, tags it, and pushes ONLY the tag
# (main stays source-only). This workflow runs on that tag push and owns
# release ARTIFACTS + SIGNING — GoReleaser builds the cross-platform binaries,
# emits checksums + SBOMs, and cosign keyless-signs the checksums file so a
# .sig + .pem pair attaches to the release. That satisfies Scorecard's
# Packaging (a publishing workflow is detected) and Signed-Releases checks.
#
# release.sh embeds internal/ui/dist into the detached tagged commit, but this
# workflow rebuilds the UI (npm ci && npm run build) BEFORE invoking goreleaser
# anyway: `go build` only recompiles Go (never runs vite), so rebuilding here
# guarantees the embedded dist is present and current regardless of how the tag
# was produced.
#
# All third-party actions are SHA-pinned per Scorecard Pinned-Dependencies;
# pins are reused verbatim from security.yml / scorecard.yml where they overlap.
on:
push:
tags:
- 'v*'

# Workflow-level default: minimum needed for actions/checkout. The release job
# escalates to contents: write (create/append the GitHub release + upload
# assets) and id-token: write (cosign keyless OIDC).
permissions:
contents: read

jobs:
goreleaser:
name: GoReleaser (build · sbom · sign)
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: Harden runner egress
# step-security/harden-runner v2.19.0
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40
with:
egress-policy: audit

- name: Checkout tag
# actions/checkout v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
# Full history so goreleaser's changelog/version detection works.
fetch-depth: 0
persist-credentials: false

- name: Set up Go
# actions/setup-go v6.2.0
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5
with:
go-version-file: go.mod
cache: true

- name: Set up Node
# actions/setup-node v6.4.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
node-version: '24'

- name: Build UI (embedded into the binary)
# The Go binary embeds internal/ui/dist via //go:embed; build it before
# goreleaser compiles. `npm run build` emits into internal/ui/dist.
run: |
cd ui
npm ci
npm run build
test -f ../internal/ui/dist/index.html

- name: Install cosign
# sigstore/cosign-installer v3.9.2 — provides `cosign` for the keyless
# signing step in .goreleaser.yaml (signs: cmd: cosign).
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159

- name: Run GoReleaser
# goreleaser/goreleaser-action v6.2.1
uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3
with:
version: '~> v2'
args: release --clean
env:
# Pure-Go SQLite driver → cgo off (matches .goreleaser.yaml builds.env).
CGO_ENABLED: '0'
# Default token: goreleaser uses it to create/append the release.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# cosign keyless reads the OIDC token from these (set by id-token: write).
COSIGN_EXPERIMENTAL: '1'
83 changes: 83 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# GoReleaser config — release ARTIFACTS + signing for OtelContext.
# Triggered by the `release.yml` workflow on a `v*` tag push (the tag is cut by
# scripts/release.sh, which also builds + embeds the React UI into the tagged
# commit). See CLAUDE.md "Security & Supply Chain" for the split of duties.
#
# OSS-only (cost $0): GoReleaser OSS v2, cosign (keyless OIDC), syft for SBOMs.
# No Pro-only keys are used. Validate locally with `goreleaser check`.
version: 2

project_name: otelcontext

# The UI must already be built into internal/ui/dist before this runs — the
# binary embeds it via `//go:embed all:dist`. release.yml runs `npm ci &&
# npm run build` in ui/ before invoking goreleaser; this hook just fails loudly
# if that step was skipped (e.g. a local `goreleaser release` without the UI).
before:
hooks:
- sh -c 'test -f internal/ui/dist/index.html || { echo "internal/ui/dist not built — run (cd ui && npm ci && npm run build) first" >&2; exit 1; }'

builds:
- id: otelcontext
binary: otelcontext
main: .
# Pure-Go SQLite driver → no cgo; required for cross-compilation here.
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
flags:
- -trimpath
ldflags:
- -s -w

archives:
- id: otelcontext
formats: [tar.gz]
name_template: >-
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}
files:
- LICENSE.md
- README.md

checksum:
name_template: "checksums.txt"
algorithm: sha256

sboms:
- id: archive-sbom
artifacts: archive

# Sign the checksums file with cosign keyless (Sigstore OIDC). Signing the
# checksums file transitively covers every archive listed in it, so a single
# .sig + .pem pair attaches to the release and Scorecard's Signed-Releases
# check is satisfied. id-token: write must be granted to the release job.
signs:
- cmd: cosign
certificate: "${artifact}.pem"
args:
- sign-blob
- --yes
- --output-signature
- "${signature}"
- --output-certificate
- "${certificate}"
- "${artifact}"
artifacts: checksum
output: true

release:
# scripts/release.sh --release may already have created the GitHub release for
# this tag (with notes, no artifacts). `append` adds goreleaser's artifacts to
# that existing release instead of failing on "release already exists". If the
# release does not yet exist, goreleaser creates it.
mode: append

changelog:
# release.sh owns the human-facing release notes; keep goreleaser's autogenerated
# changelog out of the way so the two don't fight over the release body.
disable: true
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,14 @@ All actions are SHA-pinned per Scorecard `Pinned-Dependencies`. Top-level `permi
- **Baseline:** to be measured after first push to `main`. Track via the Scorecard dashboard linked from the README badge.
- **Stretch target:** ≥ 8.0/10. Best-effort — Scorecard does **not** gate merge per the board ruling. The `passing` Best Practices badge is the only hard supply-chain gate.

### Release artifacts & signing (`.github/workflows/release.yml` + `.goreleaser.yaml`)

Triggered by a `v*` tag push (the tag is cut by `scripts/release.sh`). GoReleaser OSS v2 builds cross-platform binaries (linux/darwin × amd64/arm64, `CGO_ENABLED=0` for the pure-Go SQLite driver, `-trimpath -s -w`), emits `tar.gz` archives (incl. LICENSE.md + README.md), a sha256 `checksums.txt`, and per-archive SBOMs via syft. **cosign keyless** (Sigstore OIDC; the release job grants `id-token: write`) signs the checksums file, attaching `checksums.txt.sig` + `checksums.txt.pem` to the release. This is what makes Scorecard's **Packaging** (a publishing workflow is detected) and **Signed-Releases** checks score.

Division of labour: `scripts/release.sh` builds the UI + pushes the tag (and, with `--release`, creates the GitHub release shell with notes but **no artifacts**); `release.yml` then runs GoReleaser with `release.mode: append`, which adds the signed artifacts to that same release without racing on "release already exists". All third-party actions are SHA-pinned per Scorecard `Pinned-Dependencies` (pins reused from `security.yml`/`scorecard.yml` where they overlap). $0/OSS-only: GoReleaser OSS, cosign (Apache-2.0), syft (Apache-2.0) — no Pro-only config keys.

> Caveat: Packaging/Signed-Releases only **score** after the next `v*` tag-push actually runs `release.yml` and produces a signed release.

### Vulnerability reporting

See [`SECURITY.md`](SECURITY.md). Preferred channel: GitHub Security Advisories at `https://github.com/RandomCodeSpace/otelcontext/security/advisories/new`. Email fallback: `ak.nitrr13@gmail.com` with subject prefix `[otelcontext security]`.
Expand Down
23 changes: 20 additions & 3 deletions scripts/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,23 @@
# the release commit is reachable solely through the tag, so no build artifact
# ever lands on the branch.
#
# Division of labour with .github/workflows/release.yml
# -----------------------------------------------------
# Pushing the tag (below) triggers release.yml, which owns release ARTIFACTS +
# SIGNING: GoReleaser builds the cross-platform binaries, emits checksums +
# SBOMs, and cosign keyless-signs the checksums file (.sig + .pem). This script
# only builds the UI and pushes the tag; it does NOT upload any binaries.
#
# `--release` here creates the GitHub release shell EARLY (title + install
# notes, NO artifacts) so humans see notes immediately. GoReleaser is configured
# with `release.mode: append` (.goreleaser.yaml), so when its run finishes it
# ADDS its signed artifacts to this same release rather than failing on
# "release already exists" — no race. Omit `--release` and GoReleaser creates
# the release itself from scratch.
#
# Usage:
# scripts/release.sh vX.Y.Z[-pre] # build + push tag
# scripts/release.sh vX.Y.Z[-pre] --release # also create a GitHub pre-release
# scripts/release.sh vX.Y.Z[-pre] # build + push tag (release.yml does the rest)
# scripts/release.sh vX.Y.Z[-pre] --release # also create the GitHub release shell (notes only)
#
set -euo pipefail

Expand Down Expand Up @@ -69,7 +83,10 @@ echo "✓ pushed tag $VER -> $(git rev-parse --short "$VER")"

# (the EXIT trap now restores main to the source-only state)

# --- Optional GitHub pre-release --------------------------------------------
# --- Optional GitHub pre-release (shell only; release.yml appends artifacts) -
# Creates the release with title + notes and NO artifacts. The tag push above
# triggers .github/workflows/release.yml, whose GoReleaser run (release.mode:
# append) attaches the signed binaries, checksums, and SBOMs to this release.
if [ "$MAKE_RELEASE" = "--release" ]; then
prev="$(git describe --tags --abbrev=0 "${VER}^" 2>/dev/null || true)"
range="${prev:+${prev}..}${VER}"
Expand Down