Skip to content

Commit c630245

Browse files
aksOpsclaude
andauthored
feat(release): Phase 5 — Goreleaser + Sigstore + perf gate (#131)
* 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> * ci: SHA-pin all actions + drop ci-java path filter to unblock required check Two fixes flagged by code review of PR #131: 1. **Pin every `uses:` to a 40-char commit SHA** (Scorecard Pinned-Dependencies rule). New workflows landed with `@v4`/`@v5`/etc tags. Resolved each to the latest release commit SHA via `gh api repos/<owner>/<repo>/releases/latest`. Comments preserve the tag for readability: actions/checkout@de0fac2e... # v6.0.2 actions/setup-go@4a360112... # v6.4.0 actions/upload-artifact@043fb46d... # v7.0.1 actions/download-artifact@3e5f45b2... # v8.0.1 actions/attest-build-provenance@a2bbfa25... # v4.1.0 anchore/sbom-action@e22c3899... # v0.24.0 sigstore/cosign-installer@6f9f1778... # v4.1.2 goreleaser/goreleaser-action@1a80836c... # v7.2.1 2. **Remove `paths:` filter from ci-java.yml.** The `build` job name is a required check on main's branch protection. With the path filter, PRs that don't touch `src/**` or `pom.xml` (like the phase-5 release PR) cause the workflow to be skipped, which never reports a status — leaving "build" stuck at "Waiting for status to be reported" indefinitely and blocking merge. Java compile is ~1 minute with the Maven cache. The cost is worth the always-on signal until Phase 6 cutover deletes the Java tree. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c363727 commit c630245

6 files changed

Lines changed: 536 additions & 8 deletions

File tree

.github/workflows/ci-java.yml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,15 @@ name: Java CI
1414
on:
1515
push:
1616
branches: [main]
17-
paths:
18-
- 'src/**'
19-
- 'pom.xml'
20-
- '.github/workflows/ci-java.yml'
2117
pull_request:
2218
branches: [main]
23-
paths:
24-
- 'src/**'
25-
- 'pom.xml'
26-
- '.github/workflows/ci-java.yml'
19+
# NOTE: no `paths:` filter. The `build` job name is a required check
20+
# on main's branch protection, and a `paths:` filter would cause the
21+
# check to be skipped on PRs that don't touch Java — leaving the
22+
# required check stuck at "Waiting for status to be reported", which
23+
# blocks merge of every non-Java PR (e.g. PR #131 phase 5 release infra).
24+
# Java compile is ~1 minute; the cost is worth the always-on signal
25+
# until Phase 6 cutover deletes the Java tree entirely.
2726

2827
permissions: read-all
2928

.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
45+
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
49+
with:
50+
fetch-depth: 0
51+
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
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@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
61+
- name: Install Cosign (signing)
62+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
63+
- uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7.2.1
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
89+
with:
90+
fetch-depth: 0
91+
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
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@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
100+
- name: Install Cosign (signing)
101+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
102+
- name: Download pre-built binaries
103+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
104+
with:
105+
pattern: codeiq-*
106+
path: prebuilt
107+
- uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7.2.1
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@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
123+
with:
124+
subject-path: 'dist/codeiq_*.tar.gz'

0 commit comments

Comments
 (0)