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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Please choose versions by [Semantic Versioning](http://semver.org/).
* MINOR version when you add functionality in a backwards-compatible manner, and
* PATCH version when you make backwards-compatible bug fixes.

## Unreleased

- fix: rule-checks: gate the two `go-licensing` MUST rules on real repo visibility instead of assuming every repo is public. `check_license_file_required` was a bare `[ ! -f LICENSE ]` — its own message said "Public Go projects must…" but nothing checked whether the repo was public, so the guide's documented private-repo exemption was never implemented. `check_readme_license_section` had the same gap. Visibility now comes from `gh repo view --json isPrivate` (memoised, and only called when a finding would otherwise fire), and both rules **fail open** when it cannot be determined — a MUST-tier false positive blocks every PR in an org, while a missed finding on a public repo surfaces at the next review. `license-file-required` is also now actually Go-scoped (requires `go.mod`), matching its own docs. Measured impact: 69 of 73 non-archived `Seibert-Data` repos deliberately carry no LICENSE, so the rule was blocking essentially every PR org-wide; one such PR had to be admin-merged to land.
- fix: go-licensing-guide: replace the host-based visibility heuristic ("hosted on `github.com` → public") with the repo's `isPrivate` flag. The Octopus migration moved 73 private repos onto `github.com`, so host stopped implying visibility — this was the root cause of the false positive above. Documents the fail-open behaviour.

## v0.42.2

- fix: ast-grep-runner: pass finding payloads to `jq` via `--slurpfile` instead of `--argjson`, fixing `Argument list too long` on large result sets. Both sites moved: the per-owner merge and the final assembly. Payloads were being read out of files into shell variables and pushed back through `argv`; `jq` now reads the files directly, so payload size is irrelevant. Scales with finding count and matched-text size, not file count — reproduced with 1,956 findings from 429 files, which previously produced an **empty output file and exit 0**.
Expand Down
8 changes: 6 additions & 2 deletions docs/go-licensing-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ This guide covers how to properly handle licensing in Go projects.

Licensing requirements depend on whether the repository is **public** or **private/internal**:

| | Public (GitHub) | Private/Internal (Bitbucket) |
| | Public | Private/Internal |
|---|---|---|
| LICENSE file | Required | Not needed |
| README license section | Required | Not needed |
| Source file headers | Required | Not needed |

**How to detect**: If the repo is hosted on `github.com` → public. If hosted on `bitbucket.seibert.tools` or similar internal hosting → private/internal.
**How to detect**: read the repo's own visibility flag — `gh repo view --json isPrivate -q .isPrivate`. `false` → public, `true` → private/internal.

**Do NOT infer visibility from the host.** `github.com` used to mean public and `bitbucket.seibert.tools` private, but the Octopus migration moved 73 private `Seibert-Data` repos onto `github.com`. Host-based detection now misclassifies nearly every private repo as public — it was the cause of a MUST-tier false positive that blocked merges org-wide (fixed 2026-08).

**When visibility cannot be determined** (no `gh`, no remote, offline, not a repo), these rules **do not fire**. They are MUST-tier, so a false positive blocks every PR in the org, while a missed finding on a public repo is caught at the next review.

The rest of this guide applies to **public repositories only**.

Expand Down
65 changes: 53 additions & 12 deletions scripts/rule-checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,27 +100,67 @@ changed_file_matches() {
return 1
}

# ---------------------------------------------------------------------------
# repo_visibility — prints "public", "private" or "unknown". Memoised: one
# `gh` call per run at most, and only when a licensing rule is about to fire.
#
# Host is NOT a proxy for visibility. Private repos live on github.com too, so
# the only reliable signal is the repo's own isPrivate flag.
# ---------------------------------------------------------------------------
REPO_VISIBILITY_CACHE=""
repo_visibility() {
if [ -n "$REPO_VISIBILITY_CACHE" ]; then
printf '%s' "$REPO_VISIBILITY_CACHE"
return 0
fi
local vis="unknown" out
if command -v gh >/dev/null 2>&1; then
if out=$( (cd "$TARGET_DIR" && gh repo view --json isPrivate -q .isPrivate) 2>/dev/null ); then
case "$out" in
true) vis="private" ;;
false) vis="public" ;;
esac
fi
fi
REPO_VISIBILITY_CACHE="$vis"
printf '%s' "$vis"
}

# ---------------------------------------------------------------------------
# repo_is_public — true only when the repo is KNOWN public.
#
# Fails open on "unknown" (no gh, no remote, offline, not a repo) by design:
# these are MUST-tier rules, so a false positive blocks merges on every PR in
# the org, while a missed finding on a public repo surfaces at the next review.
# ---------------------------------------------------------------------------
repo_is_public() {
[ "$(repo_visibility)" = "public" ]
}

# ---------------------------------------------------------------------------
# RULE: go-licensing/license-file-required (MUST)
# Always run (cheap, applies to any Go project).
# Public Go repos only — see docs/go-licensing-guide.md § Public vs Private.
# Ordered cheapest-first so the `gh` call only happens when a finding would
# otherwise be emitted.
# ---------------------------------------------------------------------------
check_license_file_required() {
local license_file="$TARGET_DIR/LICENSE"
if [ ! -f "$license_file" ]; then
emit_finding \
"license-assistant" \
"go-licensing/license-file-required" \
"MUST" \
"$TARGET_DIR/LICENSE" \
0 0 \
"(file absent)" \
"No LICENSE file found at repo root. Public Go projects must have a root LICENSE file. See docs/go-licensing-guide.md."
fi
[ -f "$TARGET_DIR/LICENSE" ] && return 0
[ -f "$TARGET_DIR/go.mod" ] || return 0 # rule is Go-scoped per its own docs
repo_is_public || return 0
emit_finding \
"license-assistant" \
"go-licensing/license-file-required" \
"MUST" \
"$TARGET_DIR/LICENSE" \
0 0 \
"(file absent)" \
"No LICENSE file found at repo root. Public Go projects must have a root LICENSE file. See docs/go-licensing-guide.md."
}

# ---------------------------------------------------------------------------
# RULE: go-licensing/readme-license-section-required (MUST)
# Run when README.md changed, or always (cheap grep).
# Public repos only — same visibility gate as license-file-required.
# ---------------------------------------------------------------------------
check_readme_license_section() {
changed_file_matches "*README.md" || return 0
Expand All @@ -129,6 +169,7 @@ check_readme_license_section() {
return 0 # no README — different issue, not this rule
fi
if ! grep -qE '^## License' "$readme"; then
repo_is_public || return 0
local line
line=$(wc -l < "$readme")
emit_finding \
Expand Down
Loading