diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8ef57ab --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,39 @@ +# Dependabot: weekly dependency PRs for both Go modules, the Actions pins in +# .github/workflows, and the Docusaurus site. Minor + patch bumps are grouped +# into one PR per ecosystem so the weekly noise is a handful of PRs, not +# dozens; majors stay individual because they usually need real review. +version: 2 +updates: + - package-ecosystem: gomod + directory: "/" + schedule: + interval: weekly + groups: + go-minor-and-patch: + update-types: ["minor", "patch"] + + # The desktop shell is a separate Go module with its own go.mod (see + # Makefile: it is the repo's only cgo code, quarantined from the core). + - package-ecosystem: gomod + directory: "/cmd/msgbrowse-desktop" + schedule: + interval: weekly + groups: + desktop-minor-and-patch: + update-types: ["minor", "patch"] + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + groups: + actions: + patterns: ["*"] + + - package-ecosystem: npm + directory: "/docs-site" + schedule: + interval: weekly + groups: + docs-site-minor-and-patch: + update-types: ["minor", "patch"] diff --git a/.github/rulesets/README.md b/.github/rulesets/README.md new file mode 100644 index 0000000..51e309b --- /dev/null +++ b/.github/rulesets/README.md @@ -0,0 +1,56 @@ +# Branch protection rulesets + +Ruleset definitions for this repository, kept in-repo so the protection policy +is reviewable and versioned like everything else. GitHub does not read this +directory automatically — a repo admin applies it once (and re-applies it after +any change here). + +## What `main.json` enforces on `main` + +- **No direct pushes** — all changes land through a pull request. +- **Squash merges only** — matches the SDD merge strategy in CLAUDE.md and + keeps history linear (the `required_linear_history` rule backstops this). +- **Required status checks** — the two CI jobs that run on *every* PR must + pass before merging: + - `gofmt + vet + tests` (ci.yml) + - `docker image builds` (ci.yml) + + The CSS, Desktop, and Security workflows are deliberately **not** required: + they are path-filtered and only run when their files change, so requiring + them would deadlock every PR that doesn't touch those paths (a required + check that never reports never turns green). +- **Review-thread resolution required** — every PR conversation must be + resolved before merge. The approving-review count is 0 because this repo is + developed by a single owner + agent pair; bump + `required_approving_review_count` to 1 if a second reviewer account should + gate merges. +- **No force pushes, no branch deletion** on `main`. +- **Branches need not be up to date with `main` to merge** + (`strict_required_status_checks_policy: false`) — squash merges keep history + clean without forcing a rebase-and-rerun on every landed PR. Flip it to + `true` for stricter pre-merge testing at the cost of serializing merges. + +## How to apply + +### Option A — GitHub UI (import) + +Settings → Rules → Rulesets → New ruleset → **Import a ruleset** → upload +`main.json`. + +### Option B — `gh` CLI + +```sh +gh api repos/joestump-agent/msgbrowse/rulesets --input .github/rulesets/main.json +``` + +To update an existing ruleset instead of creating a duplicate, find its id and +`PUT` it: + +```sh +gh api repos/joestump-agent/msgbrowse/rulesets --jq '.[] | {id, name}' +gh api -X PUT repos/joestump-agent/msgbrowse/rulesets/ --input .github/rulesets/main.json +``` + +> **Plan note:** rulesets (like classic branch protection) are free on public +> repositories; private repositories need GitHub Pro/Team for them to be +> enforced. diff --git a/.github/rulesets/main.json b/.github/rulesets/main.json new file mode 100644 index 0000000..a54080c --- /dev/null +++ b/.github/rulesets/main.json @@ -0,0 +1,39 @@ +{ + "name": "protect-main", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["~DEFAULT_BRANCH"], + "exclude": [] + } + }, + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { "type": "required_linear_history" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": true, + "allowed_merge_methods": ["squash"] + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "do_not_enforce_on_create": false, + "required_status_checks": [ + { "context": "gofmt + vet + tests" }, + { "context": "docker image builds" } + ] + } + } + ], + "bypass_actors": [] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e18be5..93686e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,12 @@ jobs: steps: - uses: actions/checkout@v4 + # check-latest so builds pick up Go patch releases (stdlib security + # fixes land there — see security.yml) instead of the runner's cache. - uses: actions/setup-go@v5 with: go-version: "1.25" + check-latest: true cache: true # make check runs gofmt -l, go vet, and go test — no build tag needed. diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index e1a3c56..59d5de9 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -138,9 +138,12 @@ jobs: steps: - uses: actions/checkout@v4 + # check-latest so release artifacts pick up Go patch releases (stdlib + # security fixes land there — see security.yml). - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} + check-latest: true cache: true cache-dependency-path: | go.sum @@ -480,9 +483,12 @@ jobs: sudo apt-get update sudo apt-get install -y --no-install-recommends libgtk-3-dev libwebkit2gtk-4.1-dev pkg-config + # check-latest so release artifacts pick up Go patch releases (stdlib + # security fixes land there — see security.yml). - uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} + check-latest: true cache: true cache-dependency-path: | go.sum diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..0a54bfa --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,74 @@ +name: Security + +# govulncheck over both Go modules (the core and the desktop shell). Unlike a +# plain dependency-version audit, govulncheck reports only vulnerabilities in +# code paths the modules actually reach, so findings are actionable. +# +# Scheduling rationale: new vulnerabilities are published against dependencies +# we already have, not just when we bump them — so this runs weekly against +# main (and on demand) in addition to gating PRs that touch the module graphs. +# It is deliberately NOT a required branch-protection check: it is +# path-filtered on PRs, and a newly published vuln in an unrelated dependency +# must not turn every open PR red. +on: + pull_request: + paths: + - "go.mod" + - "go.sum" + - "cmd/msgbrowse-desktop/go.mod" + - "cmd/msgbrowse-desktop/go.sum" + - ".github/workflows/security.yml" + push: + branches: [main] + paths: + - "go.mod" + - "go.sum" + - "cmd/msgbrowse-desktop/go.mod" + - "cmd/msgbrowse-desktop/go.sum" + - ".github/workflows/security.yml" + schedule: + # Weekly, Monday 06:23 UTC (off the :00 spike so scheduled runs aren't + # queued behind everyone else's). + - cron: "23 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + govulncheck: + name: govulncheck + runs-on: ubuntu-latest + # The core is CGO_ENABLED=0 everywhere else in CI; keep the scan honest to + # the shipped build. + env: + CGO_ENABLED: "0" + steps: + - uses: actions/checkout@v4 + + # check-latest matters here: stdlib vulnerabilities are fixed in patch + # releases (e.g. GO-2026-5856 fixed in 1.25.12), so scanning with the + # runner's cached older patch would keep reporting fixed vulns — or worse, + # miss that the build toolchain needs bumping. + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + check-latest: true + cache: true + cache-dependency-path: | + go.sum + cmd/msgbrowse-desktop/go.sum + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Scan core module + run: govulncheck ./... + + # The desktop shell is its own Go module (cmd/msgbrowse-desktop/go.mod). + # Without the `desktop` build tag this scans the same pure-Go surface the + # headless tests cover — the cgo/webview code needs GTK headers and is + # exercised by desktop.yml instead. + - name: Scan desktop module + working-directory: cmd/msgbrowse-desktop + run: govulncheck ./... diff --git a/Dockerfile b/Dockerfile index 681b2fe..9cdcc9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,10 @@ # --- build stage --- # msgbrowse uses the pure-Go modernc.org/sqlite driver (FTS5 built in), so the # build needs no C toolchain — just the Go image. -FROM golang:1.25-bookworm AS build +# The official golang images set GOTOOLCHAIN=local, which would silently ignore +# the go.mod toolchain directive — pin the exact patch so the shipped image gets +# stdlib security fixes (see GO-2026-5856). +FROM golang:1.25.12-bookworm AS build WORKDIR /src diff --git a/cmd/msgbrowse-desktop/go.mod b/cmd/msgbrowse-desktop/go.mod index d98cfa6..5d1d2a6 100644 --- a/cmd/msgbrowse-desktop/go.mod +++ b/cmd/msgbrowse-desktop/go.mod @@ -2,6 +2,8 @@ module github.com/joestump/msgbrowse/cmd/msgbrowse-desktop go 1.25.0 +toolchain go1.25.12 + replace github.com/joestump/msgbrowse => ../.. require ( diff --git a/go.mod b/go.mod index 39ad0df..bdc69b8 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/joestump/msgbrowse go 1.25.0 +toolchain go1.25.12 + require ( github.com/charmbracelet/log v1.0.0 github.com/modelcontextprotocol/go-sdk v1.6.1