Skip to content

Commit 4fc523e

Browse files
ci: add golangci-lint + actionlint as PR gates (#370)
* ci: add golangci-lint + actionlint as PR gates CI currently builds and tests but no static lint runs as a merge gate. This PR adds two minimal lint workflows: 1. .golangci.yml + .github/workflows/go-lint.yml — golangci-lint v2 on Go-source PRs. Initial set: errcheck, govet, ineffassign, staticcheck, unused. Runs cleanly on the current codebase. 2. .github/workflows/lint.yml — actionlint on workflow YAML PRs. Catches GitHub Actions specific issues plus shellcheck-detectable bash issues in run: blocks. Deferred to follow-up PRs (so this one merges cleanly): - gosec: 8 findings on existing code need owner triage before fixing or suppressing (most look like false positives in build-time generators / scripts). See PR description for the list. - revive (exported godoc): backfill is single-purpose work better reviewed on its own. - depguard for DDD layers (domain MUST NOT import application/...): rule design deserves owner review before going live. Workflow details: - paths filter so docs-only PRs don't trigger unrelated lint runs - All actions SHA-pinned (golangci-lint-action v9.2.0, reviewdog/action-actionlint v1.72.0, actions/checkout v6.0.2, actions/setup-go v6.4.0) - permissions: minimal (contents/pull-requests read only) Verified locally: golangci-lint run ./... # 0 issues actionlint .github/workflows/{lint,go-lint}.yml # 0 findings go build ./... # clean * chore: update Copilot pattern log and coding rules - Record 3 new pattern(s) from PR #370 (ci-path-filters, dependency-pinning, ci-permissions) - No promotions this cycle (new categories below 2-PR threshold) Co-authored-by: Kota Kanbe <kotakanbe@users.noreply.github.com> * chore: update Copilot pattern log and coding rules - Promote defensive-coding (PRs #345, #366) to rule: use dedicated predicates and full renderers for sentinel/composed values - Remove 4 promoted entries from pending_patterns Co-authored-by: Kota Kanbe <kotakanbe@users.noreply.github.com> --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
1 parent 07a6619 commit 4fc523e

5 files changed

Lines changed: 173 additions & 42 deletions

File tree

.claude/rules/copilot-learned-coding.md

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Rules extracted from recurring Copilot review patterns on coding-standards topic
5050
- **Respect Context Lifecycle in Concurrent and Delegated I/O**: (1) When a function issues I/O (HTTP, database, external API) on behalf of a caller that provides a `context.Context`, thread that context through every intermediate function in the call chain — never substitute `context.Background()` at an internal callsite. `context.Background()` ignores the caller's cancellation and deadline signals, causing orphaned requests that persist after cancellation or exceed timeout budgets. (2) When dispatching goroutines under bounded concurrency, acquire the semaphore before launching the goroutine (not inside it) and `select` on `ctx.Done()` alongside the semaphore send to stop dispatch on cancellation — this avoids spawning parked goroutines that outlive the context.
5151
- **Guard Nil Structs Consistently Across Output Formats**: When a struct field may be nil (e.g., `ReleaseInfo`), apply the nil guard in every output renderer that accesses it (text, CSV, JSON). If one renderer has the guard and another does not, the unguarded path will panic on nil input.
5252
- **Gate Fallback Logic on Error, Not Result Nilness**: When deciding whether to trigger fallback or retry logic, check the error value — not whether the result is nil. A nil result with nil error is a valid success case (e.g., zero matches found), and treating it as a failure triggers unnecessary retries or incorrect fallback paths.
53+
- **Use Dedicated Predicates and Full Renderers for Sentinel/Composed Values**: When a domain type uses sentinel values (e.g., NOASSERTION), use dedicated predicate methods (e.g., `IsUsableSPDX()`) in all guard checks — not ad-hoc field comparisons that miss sentinel states. Canonicalize sentinels before rendering compound expressions (parser leaf fields may preserve non-canonical casing). When a type has a renderer composing sub-components (e.g., `ExprLicense.String()` includes Identifier + OrLater + WITH exception), use the renderer — not a single field — for the full representation. When recording provenance in multi-branch resolution functions, set evidence fields to the input that actually produced the match, not a prior failed branch's input.
5354
- **Minimize Allocations in Hot Paths**: In batch-processing or frequently-called functions, avoid unnecessary O(n) allocations when only a subset of data is needed. Cache results of expensive parsing calls when the same value is checked multiple times in a loop iteration, and iterate to a known cutoff point rather than materializing the full collection (e.g., iterate runes up to a count rather than converting the entire string to `[]rune`).
5455
- **Use Structured Parsers for Structured Identifier Properties**: When checking properties of structured identifiers (PURLs, URIs, import paths), use the appropriate parser rather than naive string operations (`strings.Contains`, `strings.Split`). For example, `strings.Contains(purl, "@")` misclassifies npm scoped packages like `pkg:npm/@scope/name` as versioned because `@` appears in the namespace. Use `packageurl.FromString(p).Version != ""` or an equivalent parser-based check.
5556
- **Use `u.Hostname()` for Port-Safe Host Comparison**: When comparing URL hostnames, use `u.Hostname()` instead of `u.Host`. The `Host` field includes the port component (e.g., `github.com:443`), so direct string comparison against a bare hostname fails silently, misclassifying URLs and triggering unnecessary fallback processing. Similarly, when parsing multi-entry `go-import`/`go-source` meta tags, select the entry whose import prefix most specifically matches the requested path per the Go module spec — blindly taking the first match can resolve to the wrong repository on monorepo vanity pages.
@@ -98,32 +99,16 @@ Schema (YAML-in-Markdown):
9899

99100
```yaml
100101
pending_patterns:
101-
- category: "defensive-coding"
102-
summary: "When a multi-branch resolution function (e.g., name-first then URL fallback) records evidence in a Raw/provenance field, set Raw to the input that actually produced the match — not the input from a prior branch that failed. Misattributed Raw values lose traceability for debugging and audit"
103-
pr: 345
104-
file: "internal/infrastructure/maven/license.go"
105-
date: "2026-04-29"
106-
- category: "defensive-coding"
107-
summary: "When a domain type uses a sentinel value that is 'recognized but not usable' (e.g., NOASSERTION), use the dedicated predicate method (e.g., IsUsableSPDX()) in all guard/gate checks rather than ad-hoc field checks (Expression != \"\", IsZero() || IsNonStandard()). Ad-hoc guards miss sentinel states and cause inconsistent behavior (propagating NOASSERTION as a real license, blocking enrichment, or corrupting leaf counts)"
108-
pr: 366
109-
instances: 7
110-
file: "internal/infrastructure/integration/populate.go"
111-
date: "2026-04-30"
112-
- category: "defensive-coding"
113-
summary: "Canonicalize sentinel values (e.g., NOASSERTION) when rendering compound SPDX expressions — the parser's leaf Raw field preserves original casing, so without explicit rewriting before String(), rendering produces non-canonical output like 'MIT OR noassertion' instead of 'MIT OR NOASSERTION'"
114-
pr: 366
115-
file: "internal/domain/licenses/expression_normalize.go"
116-
date: "2026-04-30"
117102
- category: "whitespace-agnostic-matching"
118103
summary: "Use bytes.Fields tokenization instead of fixed-separator prefix checks when matching directives — tabs and multiple spaces are valid separators"
119104
pr: 140
120105
file: "internal/infrastructure/depparser/detect.go"
121106
date: "2026-04-05"
122-
- category: "defensive-coding"
123-
summary: "When a structured type has a renderer that composes all sub-components (e.g., ExprLicense.String() includes Identifier + OrLater '+' + WITH exception), use the renderer instead of a single field (e.g., leaf.Identifier) when the full representation is needed — partial extraction silently drops components the renderer handles"
124-
pr: 366
125-
file: "internal/infrastructure/clearlydefined/client.go"
126-
date: "2026-04-30"
107+
- category: "ci-path-filters"
108+
summary: "Use **/*.ext (not **.ext) in GitHub Actions path filters — ** must be a standalone path segment (followed by /) to reliably match across directory boundaries; **.ext is non-standard and may behave like *.ext in some glob implementations"
109+
pr: 370
110+
file: ".github/workflows/go-lint.yml"
111+
date: "2026-05-02"
127112
- category: "comment-doc-drift"
128113
summary: "Doc comments must match implementation boundary conditions — RetryConfig said retryDecider controls 429 retries (it doesn't); rateLimitBackoff comment said 'negative' for a zero-inclusive guard (should say 'non-positive')"
129114
pr: 359
@@ -139,6 +124,11 @@ pending_patterns:
139124
pr: 346
140125
file: "internal/infrastructure/treesitter/analyzer.go"
141126
date: "2026-04-29"
127+
- category: "dependency-pinning"
128+
summary: "Pin CI tool binary versions explicitly (e.g., golangci-lint version: '2.2.1') — not just the action SHA. version: latest makes CI non-deterministic; new releases can introduce checks or behavior changes between runs on the same commit"
129+
pr: 370
130+
file: ".github/workflows/go-lint.yml"
131+
date: "2026-05-02"
142132
- category: "testing"
143133
summary: "When generating time-based test fixtures with coarse-grained formatters (e.g., http.TimeFormat at 1-second granularity), truncate to the format boundary and add enough offset (e.g., 2s) so the formatted value is deterministically in the expected range — sub-granularity offsets (50ms) can collapse to the current or past second"
144134
pr: 359
@@ -154,6 +144,11 @@ pending_patterns:
154144
pr: 359
155145
file: "internal/infrastructure/httpclient/client.go"
156146
date: "2026-04-29"
147+
- category: "ci-permissions"
148+
summary: "Verify that GitHub Actions reporter actions (e.g., reviewdog with github-pr-check) have the permissions they need — github-pr-check reporter requires checks: write to publish annotations; missing permissions cause silent failures or degraded reporting"
149+
pr: 370
150+
file: ".github/workflows/lint.yml"
151+
date: "2026-05-02"
157152
- category: "defensive-coding"
158153
summary: "Use time.NewTimer + Stop/drain instead of time.After in select with ctx.Done() to prevent timer accumulation during long cancellable waits"
159154
pr: 359
@@ -167,6 +162,7 @@ pending_patterns:
167162
```
168163
169164
<!-- Promotion history (kept for audit trail):
165+
# defensive-coding: promoted to copilot-learned-coding.instructions.md (PRs #345, #366 — use dedicated predicates and full renderers for sentinel/composed values: IsUsableSPDX() not ad-hoc checks; canonicalize sentinel casing; use type renderer not single field; attribute provenance to correct resolution branch)
170166
# defensive-coding: promoted to copilot-learned-coding.instructions.md (PRs #345, #366 — match write guard quantifiers to write semantics: an "any" flag guarding an "all/only" write misrepresents mixed inputs; verify replacement is a net improvement)
171167
# testing (PR #366): already covered by "Scope Test Assertions to Specific Output Regions" in testing-performance + "Use Spec-Compliant Parsers for Standardized Formats" — use encoding/csv to parse CSV output and assert exact cells by header name instead of fragile strings.Contains on boolean patterns that match the wrong column
172168
# defensive-coding (PR #338): stale pending entry removed — already promoted as "Sanitize Dynamic Content in GitHub Actions Workflow Commands" rule

.github/instructions/copilot-learned-coding.instructions.md

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Rules extracted from recurring Copilot review patterns on coding-standards topic
4848
- **Respect Context Lifecycle in Concurrent and Delegated I/O**: (1) When a function issues I/O (HTTP, database, external API) on behalf of a caller that provides a `context.Context`, thread that context through every intermediate function in the call chain — never substitute `context.Background()` at an internal callsite. `context.Background()` ignores the caller's cancellation and deadline signals, causing orphaned requests that persist after cancellation or exceed timeout budgets. (2) When dispatching goroutines under bounded concurrency, acquire the semaphore before launching the goroutine (not inside it) and `select` on `ctx.Done()` alongside the semaphore send to stop dispatch on cancellation — this avoids spawning parked goroutines that outlive the context.
4949
- **Guard Nil Structs Consistently Across Output Formats**: When a struct field may be nil (e.g., `ReleaseInfo`), apply the nil guard in every output renderer that accesses it (text, CSV, JSON). If one renderer has the guard and another does not, the unguarded path will panic on nil input.
5050
- **Gate Fallback Logic on Error, Not Result Nilness**: When deciding whether to trigger fallback or retry logic, check the error value — not whether the result is nil. A nil result with nil error is a valid success case (e.g., zero matches found), and treating it as a failure triggers unnecessary retries or incorrect fallback paths.
51+
- **Use Dedicated Predicates and Full Renderers for Sentinel/Composed Values**: When a domain type uses sentinel values (e.g., NOASSERTION), use dedicated predicate methods (e.g., `IsUsableSPDX()`) in all guard checks — not ad-hoc field comparisons that miss sentinel states. Canonicalize sentinels before rendering compound expressions (parser leaf fields may preserve non-canonical casing). When a type has a renderer composing sub-components (e.g., `ExprLicense.String()` includes Identifier + OrLater + WITH exception), use the renderer — not a single field — for the full representation. When recording provenance in multi-branch resolution functions, set evidence fields to the input that actually produced the match, not a prior failed branch's input.
5152
- **Minimize Allocations in Hot Paths**: In batch-processing or frequently-called functions, avoid unnecessary O(n) allocations when only a subset of data is needed. Cache results of expensive parsing calls when the same value is checked multiple times in a loop iteration, and iterate to a known cutoff point rather than materializing the full collection (e.g., iterate runes up to a count rather than converting the entire string to `[]rune`).
5253
- **Use Structured Parsers for Structured Identifier Properties**: When checking properties of structured identifiers (PURLs, URIs, import paths), use the appropriate parser rather than naive string operations (`strings.Contains`, `strings.Split`). For example, `strings.Contains(purl, "@")` misclassifies npm scoped packages like `pkg:npm/@scope/name` as versioned because `@` appears in the namespace. Use `packageurl.FromString(p).Version != ""` or an equivalent parser-based check.
5354
- **Use `u.Hostname()` for Port-Safe Host Comparison**: When comparing URL hostnames, use `u.Hostname()` instead of `u.Host`. The `Host` field includes the port component (e.g., `github.com:443`), so direct string comparison against a bare hostname fails silently, misclassifying URLs and triggering unnecessary fallback processing. Similarly, when parsing multi-entry `go-import`/`go-source` meta tags, select the entry whose import prefix most specifically matches the requested path per the Go module spec — blindly taking the first match can resolve to the wrong repository on monorepo vanity pages.
@@ -96,32 +97,16 @@ Schema (YAML-in-Markdown):
9697

9798
```yaml
9899
pending_patterns:
99-
- category: "defensive-coding"
100-
summary: "When a multi-branch resolution function (e.g., name-first then URL fallback) records evidence in a Raw/provenance field, set Raw to the input that actually produced the match — not the input from a prior branch that failed. Misattributed Raw values lose traceability for debugging and audit"
101-
pr: 345
102-
file: "internal/infrastructure/maven/license.go"
103-
date: "2026-04-29"
104-
- category: "defensive-coding"
105-
summary: "When a domain type uses a sentinel value that is 'recognized but not usable' (e.g., NOASSERTION), use the dedicated predicate method (e.g., IsUsableSPDX()) in all guard/gate checks rather than ad-hoc field checks (Expression != \"\", IsZero() || IsNonStandard()). Ad-hoc guards miss sentinel states and cause inconsistent behavior (propagating NOASSERTION as a real license, blocking enrichment, or corrupting leaf counts)"
106-
pr: 366
107-
instances: 7
108-
file: "internal/infrastructure/integration/populate.go"
109-
date: "2026-04-30"
110-
- category: "defensive-coding"
111-
summary: "Canonicalize sentinel values (e.g., NOASSERTION) when rendering compound SPDX expressions — the parser's leaf Raw field preserves original casing, so without explicit rewriting before String(), rendering produces non-canonical output like 'MIT OR noassertion' instead of 'MIT OR NOASSERTION'"
112-
pr: 366
113-
file: "internal/domain/licenses/expression_normalize.go"
114-
date: "2026-04-30"
115100
- category: "whitespace-agnostic-matching"
116101
summary: "Use bytes.Fields tokenization instead of fixed-separator prefix checks when matching directives — tabs and multiple spaces are valid separators"
117102
pr: 140
118103
file: "internal/infrastructure/depparser/detect.go"
119104
date: "2026-04-05"
120-
- category: "defensive-coding"
121-
summary: "When a structured type has a renderer that composes all sub-components (e.g., ExprLicense.String() includes Identifier + OrLater '+' + WITH exception), use the renderer instead of a single field (e.g., leaf.Identifier) when the full representation is needed — partial extraction silently drops components the renderer handles"
122-
pr: 366
123-
file: "internal/infrastructure/clearlydefined/client.go"
124-
date: "2026-04-30"
105+
- category: "ci-path-filters"
106+
summary: "Use **/*.ext (not **.ext) in GitHub Actions path filters — ** must be a standalone path segment (followed by /) to reliably match across directory boundaries; **.ext is non-standard and may behave like *.ext in some glob implementations"
107+
pr: 370
108+
file: ".github/workflows/go-lint.yml"
109+
date: "2026-05-02"
125110
- category: "comment-doc-drift"
126111
summary: "Doc comments must match implementation boundary conditions — RetryConfig said retryDecider controls 429 retries (it doesn't); rateLimitBackoff comment said 'negative' for a zero-inclusive guard (should say 'non-positive')"
127112
pr: 359
@@ -137,6 +122,11 @@ pending_patterns:
137122
pr: 346
138123
file: "internal/infrastructure/treesitter/analyzer.go"
139124
date: "2026-04-29"
125+
- category: "dependency-pinning"
126+
summary: "Pin CI tool binary versions explicitly (e.g., golangci-lint version: '2.2.1') — not just the action SHA. version: latest makes CI non-deterministic; new releases can introduce checks or behavior changes between runs on the same commit"
127+
pr: 370
128+
file: ".github/workflows/go-lint.yml"
129+
date: "2026-05-02"
140130
- category: "testing"
141131
summary: "When generating time-based test fixtures with coarse-grained formatters (e.g., http.TimeFormat at 1-second granularity), truncate to the format boundary and add enough offset (e.g., 2s) so the formatted value is deterministically in the expected range — sub-granularity offsets (50ms) can collapse to the current or past second"
142132
pr: 359
@@ -152,6 +142,11 @@ pending_patterns:
152142
pr: 359
153143
file: "internal/infrastructure/httpclient/client.go"
154144
date: "2026-04-29"
145+
- category: "ci-permissions"
146+
summary: "Verify that GitHub Actions reporter actions (e.g., reviewdog with github-pr-check) have the permissions they need — github-pr-check reporter requires checks: write to publish annotations; missing permissions cause silent failures or degraded reporting"
147+
pr: 370
148+
file: ".github/workflows/lint.yml"
149+
date: "2026-05-02"
155150
- category: "defensive-coding"
156151
summary: "Use time.NewTimer + Stop/drain instead of time.After in select with ctx.Done() to prevent timer accumulation during long cancellable waits"
157152
pr: 359
@@ -165,6 +160,7 @@ pending_patterns:
165160
```
166161
167162
<!-- Promotion history (kept for audit trail):
163+
# defensive-coding: promoted to copilot-learned-coding.instructions.md (PRs #345, #366 — use dedicated predicates and full renderers for sentinel/composed values: IsUsableSPDX() not ad-hoc checks; canonicalize sentinel casing; use type renderer not single field; attribute provenance to correct resolution branch)
168164
# defensive-coding: promoted to copilot-learned-coding.instructions.md (PRs #345, #366 — match write guard quantifiers to write semantics: an "any" flag guarding an "all/only" write misrepresents mixed inputs; verify replacement is a net improvement)
169165
# testing (PR #366): already covered by "Scope Test Assertions to Specific Output Regions" in testing-performance + "Use Spec-Compliant Parsers for Standardized Formats" — use encoding/csv to parse CSV output and assert exact cells by header name instead of fragile strings.Contains on boolean patterns that match the wrong column
170166
# defensive-coding (PR #338): stale pending entry removed — already promoted as "Sanitize Dynamic Content in GitHub Actions Workflow Commands" rule

.github/workflows/go-lint.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: Go Lint
2+
3+
# Run golangci-lint on Go-source PRs. Config is in `.golangci.yml`.
4+
# Workflow-only / docs-only PRs use the actionlint workflow instead.
5+
6+
on:
7+
pull_request:
8+
paths:
9+
- '**.go'
10+
- 'go.mod'
11+
- 'go.sum'
12+
- '.golangci.yml'
13+
push:
14+
branches: [main]
15+
paths:
16+
- '**.go'
17+
- 'go.mod'
18+
- 'go.sum'
19+
- '.golangci.yml'
20+
21+
permissions: {}
22+
23+
jobs:
24+
golangci-lint:
25+
runs-on: ubuntu-latest
26+
timeout-minutes: 5
27+
permissions:
28+
contents: read
29+
pull-requests: read
30+
steps:
31+
- name: Checkout
32+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
33+
34+
- name: Setup Go
35+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
36+
with:
37+
go-version-file: go.mod
38+
cache: true
39+
40+
- name: golangci-lint
41+
# Pin to v9.2.0 SHA. Bumps via dependabot.
42+
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
43+
with:
44+
version: latest
45+
working-directory: .
46+
args: --color always

0 commit comments

Comments
 (0)