Skip to content

Commit 7121de2

Browse files
stefanpennerclaude
andauthored
Add Bazel build system, update deps, error on missing URLs (#22)
* Add Bazel build system, update deps, error on missing URLs - Add Bazel 8.4 with bzlmod (rules_go 0.59.0, gazelle 0.47.0, hermetic_cc_toolchain 4.1.0) - Generate BUILD.bazel files for all 16 packages and 3 binaries - Update GHA workflow to use bazel test/build with setup-bazel caching - Update all Go dependencies to latest versions - Show helpful error when gha-analyzer is invoked without URLs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Expand shorthand URLs to full GitHub URLs in TUI hyperlinks Shorthand inputs like "nodejs/node/pull/60369" now get expanded to "nodejs/node#60369" in the OSC 8 href so the link is actually clickable. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Track cmd/gha-analyzer/args_test.go This file was previously untracked because the .gitignore pattern "gha-analyzer" matched the cmd/gha-analyzer/ directory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8359cbb commit 7121de2

File tree

31 files changed

+3627
-105
lines changed

31 files changed

+3627
-105
lines changed

.bazelignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
web/

.bazelrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
common --enable_platform_specific_config
2+
build:linux --sandbox_add_mount_pair=/tmp
3+
build:macos --sandbox_add_mount_pair=/var/tmp
4+
build --incompatible_strict_action_env
5+
build --@rules_go//go/config:pure
6+
test --test_output=errors

.bazelversion

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
8.4.0

.github/workflows/test.yml

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,25 @@ jobs:
3131
- name: Checkout code
3232
uses: actions/checkout@v4
3333

34+
- name: Setup Bazel
35+
uses: bazel-contrib/setup-bazel@0.14.0
36+
with:
37+
bazelisk-cache: true
38+
disk-cache: ${{ github.workflow }}
39+
repository-cache: true
40+
41+
- name: Run tests
42+
run: bazel test //...
43+
44+
- name: Build
45+
run: bazel build //cmd/...
46+
3447
- name: Setup Go
3548
uses: actions/setup-go@v5
3649
with:
3750
go-version: '1.25'
3851
cache: false
3952

40-
- name: Cache Go modules
41-
uses: actions/cache@v4
42-
with:
43-
path: |
44-
~/go/pkg/mod
45-
~/.cache/go-build
46-
key: v1-go-${{ hashFiles('**/go.sum') }}
47-
restore-keys: |
48-
v1-go-
49-
50-
- name: Install dependencies
51-
run: go mod download
52-
53-
- name: Run tests
54-
run: go test -v ./...
55-
5653
- name: Start Observability Stack
5754
run: ./run.sh up
5855

@@ -69,7 +66,4 @@ jobs:
6966
run: ./run.sh simulate
7067

7168
- name: Verify Stack Status
72-
run: ./run.sh status
73-
74-
- name: Build
75-
run: go build -v ./cmd/gha-analyzer ./cmd/gha-server
69+
run: ./run.sh status

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
node_modules
22
out.json
33
.gha-cache
4-
gha-analyzer
4+
/gha-analyzer
5+
bazel-*

BUILD.bazel

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
load("@gazelle//:def.bzl", "gazelle")
2+
3+
# gazelle:prefix github.com/stefanpenner/gha-analyzer
4+
gazelle(name = "gazelle")

MODULE.bazel

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module(
2+
name = "gha-analyzer",
3+
version = "0.0.0",
4+
)
5+
6+
# ── Go rules ─────────────────────────────────────────────────────────────────
7+
bazel_dep(name = "rules_go", version = "0.59.0")
8+
bazel_dep(name = "gazelle", version = "0.47.0")
9+
10+
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
11+
go_sdk.download(version = "1.25.7")
12+
13+
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
14+
go_deps.from_file(go_mod = "//:go.mod")
15+
use_repo(go_deps, "com_github_charmbracelet_bubbles", "com_github_charmbracelet_bubbletea", "com_github_charmbracelet_lipgloss", "com_github_cockroachdb_errors", "com_github_stretchr_testify", "io_opentelemetry_go_contrib_instrumentation_net_http_otelhttp", "io_opentelemetry_go_otel", "io_opentelemetry_go_otel_exporters_otlp_otlptrace_otlptracegrpc", "io_opentelemetry_go_otel_exporters_otlp_otlptrace_otlptracehttp", "io_opentelemetry_go_otel_exporters_stdout_stdouttrace", "io_opentelemetry_go_otel_sdk", "io_opentelemetry_go_otel_trace")
16+
17+
# ── Hermetic CC toolchain (zig) ──────────────────────────────────────────────
18+
bazel_dep(name = "hermetic_cc_toolchain", version = "4.1.0")
19+
20+
zig = use_extension(
21+
"@hermetic_cc_toolchain//toolchain:ext.bzl",
22+
"toolchains",
23+
)
24+
use_repo(zig, "zig_sdk")
25+
26+
register_toolchains("@zig_sdk//toolchain/...")

MODULE.bazel.lock

Lines changed: 2844 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/gha-analyzer/BUILD.bazel

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
load("@rules_go//go:def.bzl", "go_binary", "go_library", "go_test")
2+
3+
go_library(
4+
name = "gha-analyzer_lib",
5+
srcs = ["main.go"],
6+
importpath = "github.com/stefanpenner/gha-analyzer/cmd/gha-analyzer",
7+
visibility = ["//visibility:private"],
8+
deps = [
9+
"//pkg/analyzer",
10+
"//pkg/core",
11+
"//pkg/export/otel",
12+
"//pkg/export/perfetto",
13+
"//pkg/export/terminal",
14+
"//pkg/githubapi",
15+
"//pkg/ingest/polling",
16+
"//pkg/ingest/webhook",
17+
"//pkg/output",
18+
"//pkg/perfetto",
19+
"//pkg/tui",
20+
"//pkg/tui/results",
21+
"//pkg/utils",
22+
"@io_opentelemetry_go_otel//:otel",
23+
"@io_opentelemetry_go_otel_sdk//trace",
24+
],
25+
)
26+
27+
go_binary(
28+
name = "gha-analyzer",
29+
embed = [":gha-analyzer_lib"],
30+
visibility = ["//visibility:public"],
31+
)
32+
33+
go_test(
34+
name = "gha-analyzer_test",
35+
srcs = ["args_test.go"],
36+
embed = [":gha-analyzer_lib"],
37+
)

cmd/gha-analyzer/args_test.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestParseArgs(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
args []string
12+
isTerminal bool
13+
want config
14+
wantErr bool
15+
}{
16+
{
17+
name: "no args",
18+
args: []string{},
19+
isTerminal: false,
20+
want: config{},
21+
},
22+
{
23+
name: "URLs only",
24+
args: []string{"url1", "url2"},
25+
isTerminal: false,
26+
want: config{urls: []string{"url1", "url2"}},
27+
},
28+
{
29+
name: "tuiMode defaults to isTerminal true",
30+
args: []string{"url"},
31+
isTerminal: true,
32+
want: config{urls: []string{"url"}, tuiMode: true},
33+
},
34+
{
35+
name: "tuiMode defaults to isTerminal false",
36+
args: []string{"url"},
37+
isTerminal: false,
38+
want: config{urls: []string{"url"}},
39+
},
40+
{
41+
name: "--tui flag",
42+
args: []string{"url", "--tui"},
43+
isTerminal: false,
44+
want: config{urls: []string{"url"}, tuiMode: true},
45+
},
46+
{
47+
name: "--no-tui flag",
48+
args: []string{"url", "--no-tui"},
49+
isTerminal: true,
50+
want: config{urls: []string{"url"}},
51+
},
52+
{
53+
name: "--notui alias",
54+
args: []string{"url", "--notui"},
55+
isTerminal: true,
56+
want: config{urls: []string{"url"}},
57+
},
58+
{
59+
name: "bare --otel sets otelStdout",
60+
args: []string{"url", "--otel"},
61+
isTerminal: false,
62+
want: config{urls: []string{"url"}, otelStdout: true},
63+
},
64+
{
65+
name: "--otel=endpoint sets otelEndpoint",
66+
args: []string{"url", "--otel=host:4318"},
67+
isTerminal: false,
68+
want: config{urls: []string{"url"}, otelEndpoint: "host:4318"},
69+
},
70+
{
71+
name: "bare --otel-grpc defaults to localhost:4317",
72+
args: []string{"url", "--otel-grpc"},
73+
isTerminal: false,
74+
want: config{urls: []string{"url"}, otelGRPCEndpoint: "localhost:4317"},
75+
},
76+
{
77+
name: "--otel-grpc=endpoint sets custom endpoint",
78+
args: []string{"url", "--otel-grpc=host:9999"},
79+
isTerminal: false,
80+
want: config{urls: []string{"url"}, otelGRPCEndpoint: "host:9999"},
81+
},
82+
{
83+
name: "--window=2h",
84+
args: []string{"url", "--window=2h"},
85+
isTerminal: false,
86+
want: config{urls: []string{"url"}, window: 2 * time.Hour},
87+
},
88+
{
89+
name: "--window=bad returns error",
90+
args: []string{"url", "--window=bad"},
91+
isTerminal: false,
92+
wantErr: true,
93+
},
94+
{
95+
name: "--clear-cache flag",
96+
args: []string{"--clear-cache"},
97+
isTerminal: false,
98+
want: config{clearCache: true},
99+
},
100+
{
101+
name: "--help flag",
102+
args: []string{"--help"},
103+
isTerminal: false,
104+
want: config{showHelp: true},
105+
},
106+
{
107+
name: "-h flag",
108+
args: []string{"-h"},
109+
isTerminal: false,
110+
want: config{showHelp: true},
111+
},
112+
{
113+
name: "help word",
114+
args: []string{"help"},
115+
isTerminal: false,
116+
want: config{showHelp: true},
117+
},
118+
{
119+
name: "--perfetto=file.json",
120+
args: []string{"url", "--perfetto=trace.json"},
121+
isTerminal: false,
122+
want: config{urls: []string{"url"}, perfettoFile: "trace.json"},
123+
},
124+
{
125+
name: "--open-in-perfetto flag",
126+
args: []string{"url", "--open-in-perfetto"},
127+
isTerminal: false,
128+
want: config{urls: []string{"url"}, openInPerfetto: true},
129+
},
130+
{
131+
name: "--open-in-otel flag",
132+
args: []string{"url", "--open-in-otel"},
133+
isTerminal: false,
134+
want: config{urls: []string{"url"}, openInOTel: true},
135+
},
136+
{
137+
name: "unknown flags pass through as URLs",
138+
args: []string{"--unknown"},
139+
isTerminal: false,
140+
want: config{urls: []string{"--unknown"}},
141+
},
142+
{
143+
name: "multiple flags combined",
144+
args: []string{"url", "--otel", "--otel-grpc", "--no-tui"},
145+
isTerminal: true,
146+
want: config{
147+
urls: []string{"url"},
148+
otelStdout: true,
149+
otelGRPCEndpoint: "localhost:4317",
150+
},
151+
},
152+
{
153+
name: "--clear-cache with URL",
154+
args: []string{"--clear-cache", "url"},
155+
isTerminal: false,
156+
want: config{urls: []string{"url"}, clearCache: true},
157+
},
158+
}
159+
160+
for _, tt := range tests {
161+
t.Run(tt.name, func(t *testing.T) {
162+
got, err := parseArgs(tt.args, tt.isTerminal)
163+
if tt.wantErr {
164+
if err == nil {
165+
t.Fatal("expected error, got nil")
166+
}
167+
return
168+
}
169+
if err != nil {
170+
t.Fatalf("unexpected error: %v", err)
171+
}
172+
173+
if !slicesEqual(got.urls, tt.want.urls) {
174+
t.Errorf("urls = %v, want %v", got.urls, tt.want.urls)
175+
}
176+
if got.perfettoFile != tt.want.perfettoFile {
177+
t.Errorf("perfettoFile = %q, want %q", got.perfettoFile, tt.want.perfettoFile)
178+
}
179+
if got.openInPerfetto != tt.want.openInPerfetto {
180+
t.Errorf("openInPerfetto = %v, want %v", got.openInPerfetto, tt.want.openInPerfetto)
181+
}
182+
if got.openInOTel != tt.want.openInOTel {
183+
t.Errorf("openInOTel = %v, want %v", got.openInOTel, tt.want.openInOTel)
184+
}
185+
if got.otelEndpoint != tt.want.otelEndpoint {
186+
t.Errorf("otelEndpoint = %q, want %q", got.otelEndpoint, tt.want.otelEndpoint)
187+
}
188+
if got.otelStdout != tt.want.otelStdout {
189+
t.Errorf("otelStdout = %v, want %v", got.otelStdout, tt.want.otelStdout)
190+
}
191+
if got.otelGRPCEndpoint != tt.want.otelGRPCEndpoint {
192+
t.Errorf("otelGRPCEndpoint = %q, want %q", got.otelGRPCEndpoint, tt.want.otelGRPCEndpoint)
193+
}
194+
if got.tuiMode != tt.want.tuiMode {
195+
t.Errorf("tuiMode = %v, want %v", got.tuiMode, tt.want.tuiMode)
196+
}
197+
if got.clearCache != tt.want.clearCache {
198+
t.Errorf("clearCache = %v, want %v", got.clearCache, tt.want.clearCache)
199+
}
200+
if got.window != tt.want.window {
201+
t.Errorf("window = %v, want %v", got.window, tt.want.window)
202+
}
203+
if got.showHelp != tt.want.showHelp {
204+
t.Errorf("showHelp = %v, want %v", got.showHelp, tt.want.showHelp)
205+
}
206+
})
207+
}
208+
}
209+
210+
func slicesEqual(a, b []string) bool {
211+
if len(a) == 0 && len(b) == 0 {
212+
return true
213+
}
214+
if len(a) != len(b) {
215+
return false
216+
}
217+
for i := range a {
218+
if a[i] != b[i] {
219+
return false
220+
}
221+
}
222+
return true
223+
}

0 commit comments

Comments
 (0)