feat(github): add the GitHub adapter's client and core methods #88

Merged
deadstyle merged 2 commits from feature/84-github-adapter into develop 2026-10-09 09:54:06 -05:00
Owner

Summary

lazyforge can now talk to github.com and GitHub Enterprise Server. The new adapter covers repos, PRs with one CI state built from both check runs and commit statuses, merge pinned to the confirmed head, approve, issues, comments, close and releases. Refreshes use conditional requests, so an unchanged list doesn't spend GitHub's 5,000/hour budget. Nothing is user-visible yet: onboarding and connect wiring is #86.

Refs #84 (part of #83)

Decisions

  • ADR 0017: a thin net/http client and no go-github. Also recorded there: the conditional-request cache (1,000 entries, 32 MiB, no body over 1 MiB), the CI fold, the merge-method rule, and a 403 rate limit mapped to ErrRateLimited.
  • Merge strategy: the first method GitHub's merge button offers among those the repo allows (merge commit, squash, rebase). /user/repos doesn't report those flags, so Repo.MergeStyle stays empty and the confirm dialog says "repo default". The reasoning is in the ticket comment.
  • .golangci.yml: the depguard rule now covers internal/forge/github too, the same way it covers gitea, so only cmd/ can import it.

Verification

make check (golangci-lint v2.14.0, the version CI pins):

golangci-lint run
0 issues.
ok  	git.bobparsons.dev/deadstyle/lazyforge/internal/forge/github
No vulnerabilities found.
go build -ldflags ... -o bin/lazyforge ./cmd/lazyforge
shellcheck install.sh scripts/*.sh
/bin/sh: line 1: shellcheck: command not found

shellcheck isn't installed on the dev machine. This MR touches no shell files, and CI runs that step.

  • forgetest.RunContract passes 9 cases. The 3 asset cases skip because AssetReader comes in #87.
  • Mutation checks: disabling the cache write or the total_count > 0 guard fails 4 tests. Reverting the vanished-SHA fix fails its 404 and 422 subtests.
  • Fixtures were recorded with gh api on 2026-10-09 from public repos (cli/cli, kubernetes/kubernetes, Bparsons0904/*). The synthesized ones are marked as such in testdata/README.md. I checked the fixtures for tokens and private data and found none.

Review

Three fresh reviewers ran: standards, comments, and spec. Spec passed.

  • Fixed: a PR whose head SHA is gone failed the whole PR list (now no CI); the 304 replay header; the cache's byte bound; 10 comments that restated the code.
  • Dismissed, with reasons:
    • Paging through all closed PRs: nothing calls the closed or merged filters, and gitea behaves the same.
    • The repo GET on every merge: after the first merge it is an ETag 304, so a bulk merge costs no budget.
    • Not telling null allow_* flags apart from false ones: merging needs write access, and a write token gets the flags.
    • A stale ETag right after a write: GitHub ETags are content hashes.
    • Plain-http GHES: gitea allows it too.
    • www.github.com, a comma inside a Link URL, and the /merge label match: low risk.
  • I re-checked the fixes myself rather than re-dispatching the reviewers. They are small: one helper, a header, and the cache bound.

Docs

docs/architecture.md has the GitHub column verified for these rows (runs, jobs and logs are left for #85) and links ADR 0017. CLAUDE.md's file map now lists internal/forge/github/. design.md is unchanged: #86 records the release scope.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QhCwUhPRAFrAziCumaEa67

## Summary lazyforge can now talk to github.com and GitHub Enterprise Server. The new adapter covers repos, PRs with one CI state built from both check runs and commit statuses, merge pinned to the confirmed head, approve, issues, comments, close and releases. Refreshes use conditional requests, so an unchanged list doesn't spend GitHub's 5,000/hour budget. Nothing is user-visible yet: onboarding and connect wiring is #86. Refs #84 (part of #83) ## Decisions - ADR 0017: a thin `net/http` client and no `go-github`. Also recorded there: the conditional-request cache (1,000 entries, 32 MiB, no body over 1 MiB), the CI fold, the merge-method rule, and a 403 rate limit mapped to `ErrRateLimited`. - Merge strategy: the first method GitHub's merge button offers among those the repo allows (merge commit, squash, rebase). `/user/repos` doesn't report those flags, so `Repo.MergeStyle` stays empty and the confirm dialog says "repo default". The reasoning is in the ticket comment. - `.golangci.yml`: the depguard rule now covers `internal/forge/github` too, the same way it covers gitea, so only `cmd/` can import it. ## Verification `make check` (golangci-lint v2.14.0, the version CI pins): ``` golangci-lint run 0 issues. ok git.bobparsons.dev/deadstyle/lazyforge/internal/forge/github No vulnerabilities found. go build -ldflags ... -o bin/lazyforge ./cmd/lazyforge shellcheck install.sh scripts/*.sh /bin/sh: line 1: shellcheck: command not found ``` `shellcheck` isn't installed on the dev machine. This MR touches no shell files, and CI runs that step. - `forgetest.RunContract` passes 9 cases. The 3 asset cases skip because `AssetReader` comes in #87. - Mutation checks: disabling the cache write or the `total_count > 0` guard fails 4 tests. Reverting the vanished-SHA fix fails its 404 and 422 subtests. - Fixtures were recorded with `gh api` on 2026-10-09 from public repos (`cli/cli`, `kubernetes/kubernetes`, `Bparsons0904/*`). The synthesized ones are marked as such in `testdata/README.md`. I checked the fixtures for tokens and private data and found none. ## Review Three fresh reviewers ran: standards, comments, and spec. Spec passed. - Fixed: a PR whose head SHA is gone failed the whole PR list (now no CI); the 304 replay header; the cache's byte bound; 10 comments that restated the code. - Dismissed, with reasons: - Paging through all closed PRs: nothing calls the closed or merged filters, and gitea behaves the same. - The repo GET on every merge: after the first merge it is an ETag 304, so a bulk merge costs no budget. - Not telling null `allow_*` flags apart from false ones: merging needs write access, and a write token gets the flags. - A stale ETag right after a write: GitHub ETags are content hashes. - Plain-`http` GHES: gitea allows it too. - `www.github.com`, a comma inside a Link URL, and the `/merge` label match: low risk. - I re-checked the fixes myself rather than re-dispatching the reviewers. They are small: one helper, a header, and the cache bound. ## Docs `docs/architecture.md` has the GitHub column verified for these rows (runs, jobs and logs are left for #85) and links ADR 0017. `CLAUDE.md`'s file map now lists `internal/forge/github/`. `design.md` is unchanged: #86 records the release scope. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QhCwUhPRAFrAziCumaEa67
New internal/forge/github talks to github.com and GitHub Enterprise
Server: repos, PRs with CI folded from check runs and commit statuses,
merge pinned to the head SHA, approve, issues, comments, close and
releases. Conditional requests keep refreshes off the rate limit.
ADR 0017 records the decisions.

Refs #84

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhCwUhPRAFrAziCumaEa67
fix(github): tolerate a vanished head SHA and bound the ETag cache by bytes
All checks were successful
ci / check (pull_request) Successful in 1m6s
73b31151b7
Review follow-up: a 404/422 on a PR's CI lookup now reads as no CI
instead of failing the list, the 304 replay carries the cached body's
length, and the cache caps total bytes as well as entries.

Refs #84

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QhCwUhPRAFrAziCumaEa67
deadstyle merged commit d3f3c090ec into develop 2026-10-09 09:54:06 -05:00
deadstyle deleted branch feature/84-github-adapter 2026-10-09 09:54:06 -05:00
Sign in to join this conversation.
No description provided.