Skip to content

Commit 494343f

Browse files
aksOpsclaude
andcommitted
feat(release): Phase 5 — Goreleaser + Sigstore + perf gate + Homebrew tap
Tag-triggered (v*.*.*) Go binary release pipeline. ## Pieces - **.goreleaser.yml** — multi-arch build (linux/amd64, linux/arm64, darwin/arm64) with `-trimpath -s -w` ldflags, version metadata injected into `internal/buildinfo`. SPDX SBOMs via Syft per archive. Checksum manifest keyless-signed via Cosign (no long-lived key). Homebrew tap publish opt-in via $HOMEBREW_TAP_GITHUB_TOKEN — skipped silently when absent so forks reuse the same config. - **.github/workflows/release-go.yml** — per-target build matrix (CGO + native kuzudb/sqlite means we can't cross-compile from one host), combined publish job, build-provenance attestations, `id-token: write` permission for Sigstore keyless OIDC. - **.github/workflows/perf-gate.yml** — regression gate on PRs and pushes to main. Indexes fixture-multi-lang, asserts: - wall-clock <= 8 s - >= 40 nodes (catches detector dispatch regressions) - <= 50% phantom-edge drop ratio (catches anchor-node anti-patterns) Tuned so the kind of regex pathology that pushed PSA from 0.1 s → 42 s mid-port would fail the gate immediately. - **shared/runbooks/release-go.md** — operator runbook: tag, verify draft release, publish; user-facing checksum + cosign verify recipe; Homebrew tap setup; local snapshot dry-run; failure recovery. - **CHANGELOG.md [Unreleased]** updated. ## Local validation yaml-lint of all three workflow files + .goreleaser.yml: ok. Tested goreleaser/cosign/syft installation isn't required locally — CI does the real release. ## Out of scope (deferred) - Smoke test of the published artifact post-release (no canary user yet). - Auto-bump of version (manual decision, not automated). - Homebrew tap repo creation itself — needs a repo admin to create `RandomCodeSpace/homebrew-codeiq` and set the PAT secret; the workflow gracefully no-ops until then. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c363727 commit 494343f

5 files changed

Lines changed: 529 additions & 0 deletions

File tree

.github/workflows/perf-gate.yml

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
name: perf-gate
2+
3+
# Performance regression gate. Runs `codeiq index` against fixture-multi-lang
4+
# and asserts wall-clock + node-count budgets. Catches regressions like:
5+
# - Regex pathology re-introduced (e.g. the CertificateAuthDetector
6+
# pre-screen miss that pushed indexing from 0.1s → 42s on PSA).
7+
# - Detector over-emission past the dedup budget.
8+
#
9+
# Trigger: push to main + PRs that touch go/**. Manual via workflow_dispatch.
10+
# Failure is informational on PRs (`continue-on-error`) until the threshold
11+
# is curated against real-world load; once stable, set strict gate.
12+
13+
on:
14+
push:
15+
branches: [main]
16+
paths:
17+
- 'go/**'
18+
- '.github/workflows/perf-gate.yml'
19+
pull_request:
20+
branches: [main]
21+
paths:
22+
- 'go/**'
23+
- '.github/workflows/perf-gate.yml'
24+
workflow_dispatch:
25+
26+
permissions:
27+
contents: read
28+
29+
jobs:
30+
bench:
31+
name: index perf gate (fixture-multi-lang)
32+
runs-on: ubuntu-latest
33+
env:
34+
CGO_ENABLED: '1'
35+
# Per-target budgets. Tune as the fixture grows. Current
36+
# fixture-multi-lang sits at ~50 files; an 8 s ceiling leaves
37+
# headroom over the observed ~0.3 s without hiding obvious
38+
# regressions (10x cushion catches the kinds of regex pathology
39+
# that pushed PSA from 0.1 s → 42 s mid-port).
40+
MAX_INDEX_SECONDS: '8'
41+
MIN_NODES: '40'
42+
MAX_PHANTOM_DROP_RATIO: '50'
43+
steps:
44+
- uses: actions/checkout@v4
45+
- uses: actions/setup-go@v5
46+
with:
47+
go-version: '1.25.10'
48+
cache: true
49+
cache-dependency-path: go/go.sum
50+
- name: Install C toolchain
51+
run: sudo apt-get update -y && sudo apt-get install -y build-essential
52+
- name: Build codeiq
53+
working-directory: go
54+
run: go build -o /tmp/codeiq ./cmd/codeiq
55+
- name: Stage fixture (separate copy so cache writes don't dirty git)
56+
run: cp -r go/testdata/fixture-multi-lang /tmp/fm-perf
57+
- name: Run + measure
58+
id: bench
59+
run: |
60+
set -euo pipefail
61+
START=$(date +%s.%N)
62+
/tmp/codeiq index /tmp/fm-perf > /tmp/perf.log 2>&1
63+
END=$(date +%s.%N)
64+
ELAPSED=$(awk "BEGIN{printf \"%.3f\", $END - $START}")
65+
66+
# Parse the "Files: F Nodes: N Edges: E ..." summary line.
67+
NODES=$(awk -F'[ ]+' '/^Files:/ {print $4}' /tmp/perf.log)
68+
EDGES=$(awk -F'[ ]+' '/^Files:/ {print $6}' /tmp/perf.log)
69+
# Optional "Deduped: D nodes, ... Dropped: P phantom edges"
70+
# line; absence is fine, defaults to 0.
71+
DEDUP_NODES=$(awk -F'[ ,]+' '/^Deduped:/ {print $2}' /tmp/perf.log)
72+
DEDUP_NODES=${DEDUP_NODES:-0}
73+
DROPPED=$(awk -F'[ ]+' '/^Deduped:/ {for(i=1;i<=NF;i++) if($i=="Dropped:") print $(i+1)}' /tmp/perf.log)
74+
DROPPED=${DROPPED:-0}
75+
76+
echo "elapsed=$ELAPSED" >> "$GITHUB_OUTPUT"
77+
echo "nodes=$NODES" >> "$GITHUB_OUTPUT"
78+
echo "edges=$EDGES" >> "$GITHUB_OUTPUT"
79+
echo "dropped=$DROPPED" >> "$GITHUB_OUTPUT"
80+
81+
{
82+
echo "## codeiq perf gate"
83+
echo ""
84+
echo "| metric | value | budget |"
85+
echo "|---|---:|---:|"
86+
echo "| wall-clock (s) | $ELAPSED | $MAX_INDEX_SECONDS |"
87+
echo "| nodes | $NODES | >= $MIN_NODES |"
88+
echo "| edges | $EDGES | — |"
89+
echo "| deduped nodes | $DEDUP_NODES | — |"
90+
echo "| dropped phantom edges | $DROPPED | ratio gated |"
91+
} >> "$GITHUB_STEP_SUMMARY"
92+
93+
cat /tmp/perf.log >> "$GITHUB_STEP_SUMMARY"
94+
95+
# --- Hard gates ---
96+
fail=0
97+
if awk "BEGIN{exit !($ELAPSED > $MAX_INDEX_SECONDS)}"; then
98+
echo "::error::wall-clock $ELAPSED s exceeds budget $MAX_INDEX_SECONDS s"
99+
fail=1
100+
fi
101+
if [ "${NODES:-0}" -lt "$MIN_NODES" ]; then
102+
echo "::error::node count $NODES below minimum $MIN_NODES"
103+
fail=1
104+
fi
105+
if [ "${EDGES:-0}" -gt 0 ] && [ "${DROPPED:-0}" -gt 0 ]; then
106+
RATIO=$(( DROPPED * 100 / (EDGES + DROPPED) ))
107+
if [ "$RATIO" -gt "$MAX_PHANTOM_DROP_RATIO" ]; then
108+
echo "::error::phantom-edge drop ratio ${RATIO}% exceeds ${MAX_PHANTOM_DROP_RATIO}%"
109+
fail=1
110+
fi
111+
fi
112+
exit $fail

.github/workflows/release-go.yml

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
name: release-go
2+
3+
# Tag-triggered release pipeline for the codeiq Go binary.
4+
#
5+
# Trigger: push a tag matching `v*.*.*` (e.g. `git tag v1.0.0 && git push --tags`).
6+
# Cross-OS build via per-runner matrix (CGO + native kuzudb/sqlite means
7+
# we can't cross-compile cleanly from a single host).
8+
#
9+
# Phase 5 of the Java→Go port. Replaces release-java.yml (kept around
10+
# until Phase 6 cutover for any emergency Java release).
11+
12+
on:
13+
push:
14+
tags:
15+
- 'v*.*.*'
16+
workflow_dispatch:
17+
inputs:
18+
tag:
19+
description: 'Tag to release (e.g. v1.0.0). Must already exist.'
20+
required: true
21+
22+
permissions:
23+
contents: write
24+
id-token: write # Sigstore keyless via GitHub OIDC
25+
packages: write
26+
attestations: write
27+
28+
jobs:
29+
# Per-target release. Runs the same .goreleaser.yml on each runner;
30+
# archives are merged in the publish job below.
31+
build:
32+
name: build (${{ matrix.os }} / ${{ matrix.goarch }})
33+
runs-on: ${{ matrix.runner }}
34+
strategy:
35+
fail-fast: false
36+
matrix:
37+
include:
38+
- os: linux
39+
goarch: amd64
40+
runner: ubuntu-latest
41+
- os: linux
42+
goarch: arm64
43+
runner: ubuntu-24.04-arm
44+
- os: darwin
45+
goarch: arm64
46+
runner: macos-14
47+
steps:
48+
- uses: actions/checkout@v4
49+
with:
50+
fetch-depth: 0
51+
- uses: actions/setup-go@v5
52+
with:
53+
go-version: '1.25.10'
54+
cache: true
55+
cache-dependency-path: go/go.sum
56+
- name: Install build deps (linux)
57+
if: runner.os == 'Linux'
58+
run: sudo apt-get update -y && sudo apt-get install -y build-essential
59+
- name: Install Syft (SBOM)
60+
uses: anchore/sbom-action/download-syft@v0
61+
- name: Install Cosign (signing)
62+
uses: sigstore/cosign-installer@v3
63+
- uses: goreleaser/goreleaser-action@v6
64+
with:
65+
distribution: goreleaser
66+
version: '~> v2'
67+
# Single-target build per runner; combined publish runs in a
68+
# separate job that consumes all three artifact bundles.
69+
args: build --single-target --clean --snapshot
70+
env:
71+
GOOS: ${{ matrix.os }}
72+
GOARCH: ${{ matrix.goarch }}
73+
- name: Upload binary artifact
74+
uses: actions/upload-artifact@v4
75+
with:
76+
name: codeiq-${{ matrix.os }}-${{ matrix.goarch }}
77+
path: dist/codeiq_*/codeiq*
78+
retention-days: 1
79+
80+
# Combined publish: pulls the three binaries built above, packages
81+
# them with SBOMs, signs the checksum manifest via Sigstore keyless,
82+
# and uploads the GitHub Release. Runs on linux only.
83+
release:
84+
name: publish release
85+
needs: build
86+
runs-on: ubuntu-latest
87+
steps:
88+
- uses: actions/checkout@v4
89+
with:
90+
fetch-depth: 0
91+
- uses: actions/setup-go@v5
92+
with:
93+
go-version: '1.25.10'
94+
cache: true
95+
cache-dependency-path: go/go.sum
96+
- name: Install build deps
97+
run: sudo apt-get update -y && sudo apt-get install -y build-essential
98+
- name: Install Syft (SBOM)
99+
uses: anchore/sbom-action/download-syft@v0
100+
- name: Install Cosign (signing)
101+
uses: sigstore/cosign-installer@v3
102+
- name: Download pre-built binaries
103+
uses: actions/download-artifact@v4
104+
with:
105+
pattern: codeiq-*
106+
path: prebuilt
107+
- uses: goreleaser/goreleaser-action@v6
108+
with:
109+
distribution: goreleaser
110+
version: '~> v2'
111+
# Full release: archives + SBOMs + cosign sigs + GitHub Release
112+
# draft + (optional) Homebrew tap. The owning org sets
113+
# HOMEBREW_TAP_GITHUB_TOKEN to publish to homebrew-codeiq;
114+
# forks leave it unset and the brew step skips silently.
115+
args: release --clean
116+
env:
117+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
118+
HOMEBREW_TAP_OWNER: RandomCodeSpace
119+
HOMEBREW_TAP_REPO: homebrew-codeiq
120+
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
121+
- name: Attest release artifacts (build provenance)
122+
uses: actions/attest-build-provenance@v2
123+
with:
124+
subject-path: 'dist/codeiq_*.tar.gz'

.goreleaser.yml

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Goreleaser config for codeiq Go binary releases.
2+
#
3+
# Trigger: tag push `vX.Y.Z` → .github/workflows/release-go.yml fires
4+
# this config. Local dry runs: `goreleaser release --snapshot --clean`.
5+
#
6+
# CGO is required (kuzudb + go-sqlite3 native deps), so we cross-compile
7+
# via per-target runners — see the release workflow matrix. This file
8+
# is consumed once per target OS.
9+
10+
version: 2
11+
project_name: codeiq
12+
13+
env:
14+
- CGO_ENABLED=1
15+
- GO_VERSION=1.25.10
16+
17+
before:
18+
hooks:
19+
# Sanity gate. Failing here aborts the release before any binary
20+
# leaves the runner.
21+
- cd go && go mod download
22+
- cd go && go test ./... -count=1
23+
24+
builds:
25+
- id: codeiq
26+
main: ./cmd/codeiq
27+
dir: go
28+
binary: codeiq
29+
env:
30+
- CGO_ENABLED=1
31+
flags:
32+
- -trimpath
33+
ldflags:
34+
- -s -w
35+
- -X 'github.com/randomcodespace/codeiq/go/internal/buildinfo.Version={{.Version}}'
36+
- -X 'github.com/randomcodespace/codeiq/go/internal/buildinfo.Commit={{.ShortCommit}}'
37+
- -X 'github.com/randomcodespace/codeiq/go/internal/buildinfo.Date={{.Date}}'
38+
- -X 'github.com/randomcodespace/codeiq/go/internal/buildinfo.Dirty={{.IsGitDirty}}'
39+
# CGO + kuzudb makes cross-arch fragile from a single host; the
40+
# release workflow runs this config once per (OS, arch) runner.
41+
goos:
42+
- linux
43+
- darwin
44+
goarch:
45+
- amd64
46+
- arm64
47+
ignore:
48+
# darwin/amd64 needs a darwin runner — skip when this config is
49+
# consumed on a linux runner. The release workflow re-runs the
50+
# darwin builds on macOS runners.
51+
- goos: darwin
52+
goarch: amd64
53+
54+
archives:
55+
- id: codeiq
56+
formats: [tar.gz]
57+
name_template: >-
58+
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}
59+
files:
60+
- LICENSE*
61+
- README.md
62+
- CHANGELOG.md
63+
64+
checksum:
65+
name_template: 'checksums.sha256'
66+
algorithm: sha256
67+
68+
snapshot:
69+
version_template: '{{ incpatch .Version }}-next'
70+
71+
# SBOM generation — Plan §5 SBOM signing requirement. Syft is the
72+
# OSS-CLI choice (matches the existing security.yml stack).
73+
sboms:
74+
- id: codeiq-sbom
75+
artifacts: archive
76+
documents:
77+
- '{{ .ArtifactName }}.sbom.spdx.json'
78+
cmd: syft
79+
args:
80+
- '$artifact'
81+
- --output
82+
- 'spdx-json={{ .ArtifactName }}.sbom.spdx.json'
83+
84+
# Cosign keyless signing of the checksum manifest. The release workflow
85+
# supplies the OIDC token via `id-token: write`; cosign records the
86+
# signature transparency entry in Rekor (public Sigstore log). No
87+
# long-lived signing key required.
88+
signs:
89+
- id: cosign
90+
cmd: cosign
91+
args:
92+
- sign-blob
93+
- '--yes'
94+
- '--output-signature=${signature}'
95+
- '--output-certificate=${certificate}'
96+
- '${artifact}'
97+
artifacts: checksum
98+
output: true
99+
certificate: '${artifact}.pem'
100+
signature: '${artifact}.sig'
101+
102+
# Homebrew tap publish — opt-in via $HOMEBREW_TAP_GITHUB_TOKEN. When the
103+
# env var is empty (forks, dry runs), the upload is skipped so the same
104+
# .goreleaser.yml works for the owning org and downstream forks alike.
105+
brews:
106+
- name: codeiq
107+
repository:
108+
owner: '{{ envOrDefault "HOMEBREW_TAP_OWNER" "RandomCodeSpace" }}'
109+
name: '{{ envOrDefault "HOMEBREW_TAP_REPO" "homebrew-codeiq" }}'
110+
token: '{{ envOrDefault "HOMEBREW_TAP_GITHUB_TOKEN" "" }}'
111+
skip_upload: '{{ if eq (envOrDefault "HOMEBREW_TAP_GITHUB_TOKEN" "") "" }}true{{ else }}false{{ end }}'
112+
commit_author:
113+
name: codeiq-bot
114+
email: noreply@github.com
115+
directory: Formula
116+
homepage: 'https://github.com/RandomCodeSpace/codeiq'
117+
description: 'Deterministic code knowledge graph + MCP server'
118+
license: 'Apache-2.0'
119+
install: |
120+
bin.install "codeiq"
121+
test: |
122+
assert_match "codeiq", shell_output("#{bin}/codeiq --version")
123+
124+
release:
125+
github:
126+
owner: RandomCodeSpace
127+
name: codeiq
128+
draft: true
129+
prerelease: auto
130+
mode: replace
131+
name_template: 'v{{ .Version }}'
132+
header: |
133+
## codeiq v{{ .Version }}
134+
135+
Deterministic code knowledge graph — Go single-binary release.
136+
137+
Verify the download:
138+
139+
# Checksum
140+
sha256sum -c checksums.sha256
141+
142+
# Signature (Sigstore keyless)
143+
cosign verify-blob \
144+
--certificate checksums.sha256.pem \
145+
--signature checksums.sha256.sig \
146+
--certificate-identity-regexp 'https://github.com/RandomCodeSpace/codeiq/.github/workflows/release-go.yml@.*' \
147+
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
148+
checksums.sha256
149+
footer: |
150+
---
151+
Built with Go {{ envOrDefault "GO_VERSION" "1.25.10" }} via Goreleaser.

0 commit comments

Comments
 (0)