gem-contribute 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/workshop-issue.md +29 -0
  3. data/.github/workflows/auto-merge-kicked-tires.yml +88 -0
  4. data/CHANGELOG.md +24 -0
  5. data/CLAUDE.md +47 -0
  6. data/CONTRIBUTING.md +46 -0
  7. data/KICKED_THE_TIRES.yml +22 -0
  8. data/LICENSE +21 -0
  9. data/MAINTAINER.md +92 -0
  10. data/README.md +89 -0
  11. data/Rakefile +10 -0
  12. data/docs/_config.yml +30 -0
  13. data/docs/adr/0001-just-in-time-auth.md +44 -0
  14. data/docs/adr/0002-bundler-lockfile-parser.md +35 -0
  15. data/docs/adr/0003-issue-tracker-preference.md +33 -0
  16. data/docs/adr/0004-device-flow-auth.md +36 -0
  17. data/docs/adr/0005-render-labels-verbatim.md +46 -0
  18. data/docs/adr/0006-standalone-gem-not-plugin.md +31 -0
  19. data/docs/adr/0007-display-contributing-verbatim.md +39 -0
  20. data/docs/adr/0008-rooibos-tui-framework.md +62 -0
  21. data/docs/adr/0009-top-level-namespace.md +37 -0
  22. data/docs/adr/README.md +21 -0
  23. data/docs/claude-code-prompt.md +40 -0
  24. data/docs/design.md +234 -0
  25. data/docs/index.md +102 -0
  26. data/docs/prep-plan.md +165 -0
  27. data/docs/workshop.md +60 -0
  28. data/exe/gem-contribute +7 -0
  29. data/lib/gem_contribute/auth.rb +161 -0
  30. data/lib/gem_contribute/cache.rb +98 -0
  31. data/lib/gem_contribute/cli/auth.rb +164 -0
  32. data/lib/gem_contribute/cli/config.rb +87 -0
  33. data/lib/gem_contribute/cli/fork_clone_branch.rb +197 -0
  34. data/lib/gem_contribute/cli/issues.rb +123 -0
  35. data/lib/gem_contribute/cli/scan.rb +117 -0
  36. data/lib/gem_contribute/cli/submit.rb +155 -0
  37. data/lib/gem_contribute/cli.rb +104 -0
  38. data/lib/gem_contribute/config.rb +60 -0
  39. data/lib/gem_contribute/errors.rb +32 -0
  40. data/lib/gem_contribute/host_adapter.rb +40 -0
  41. data/lib/gem_contribute/host_adapters/github_adapter.rb +215 -0
  42. data/lib/gem_contribute/locked_gem.rb +26 -0
  43. data/lib/gem_contribute/lockfile_parser.rb +61 -0
  44. data/lib/gem_contribute/project.rb +21 -0
  45. data/lib/gem_contribute/resolver.rb +131 -0
  46. data/lib/gem_contribute/token_store.rb +86 -0
  47. data/lib/gem_contribute/version.rb +5 -0
  48. data/lib/gem_contribute.rb +32 -0
  49. data/script/lint-kicked-tires.rb +76 -0
  50. data/sig/gem_contribute.rbs +3 -0
  51. metadata +114 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6f6941e81cacd8ab2bb7abb6824bb54d5b04ec05789d24c792b48eb4c5125e02
4
+ data.tar.gz: 9a548392f2d027dbc99719d74de627940086e3858fd5c5152fa8d4ecffddb7b8
5
+ SHA512:
6
+ metadata.gz: 1a4c00ea48ba2ef0245f3564d97b2bb0e6d3eb7098a79ef598a58dda95c6130630648e04351e8a5ef7f767456d938a836c647aa02c18d7b22663816a6b9d7d2e
7
+ data.tar.gz: 1d60a359d87cc17ba4e2c8bad1acab5f039334df63afd34055165d1a61b569e3c9c7fd57f5159d33ed0031e7634cabb93fd1b82e52f36cd4a5d0e30bfd2ef2ca
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: Workshop issue
3
+ about: A small, scoped task suitable for a workshop attendee
4
+ title: ''
5
+ labels: ['good first issue', 'workshop']
6
+ assignees: ''
7
+ ---
8
+
9
+ ## What
10
+
11
+ One sentence describing the change.
12
+
13
+ ## Why
14
+
15
+ One sentence on the user-facing reason this matters.
16
+
17
+ ## Where
18
+
19
+ File or module where the work happens. Be specific — workshop time is short.
20
+
21
+ ## Acceptance
22
+
23
+ - [ ] Specific, testable criterion 1
24
+ - [ ] Specific, testable criterion 2
25
+ - [ ] Test added or updated where appropriate
26
+
27
+ ## Hints
28
+
29
+ Anything a stranger to the codebase needs to know. Link to relevant ADRs if a decision constrains the implementation.
@@ -0,0 +1,88 @@
1
+ name: Auto-merge KICKED_THE_TIRES.yml PRs
2
+
3
+ # Triggers on PRs that touch KICKED_THE_TIRES.yml. If the PR ONLY touches
4
+ # that file and the YAML passes the schema check, the PR is squash-merged
5
+ # automatically. Anything else (multi-file PRs, malformed YAML, schema
6
+ # violations) leaves the PR open for manual review.
7
+ #
8
+ # Security note: we use pull_request_target so the workflow itself runs
9
+ # from the base branch (trusted) and has write permissions, but we never
10
+ # check out the PR HEAD. The proposed YAML is fetched via the GitHub API
11
+ # as text — it's data, not code, and is never executed.
12
+
13
+ on:
14
+ pull_request_target:
15
+ types: [opened, synchronize, reopened]
16
+ paths:
17
+ - 'KICKED_THE_TIRES.yml'
18
+
19
+ permissions:
20
+ contents: write
21
+ pull-requests: write
22
+
23
+ jobs:
24
+ validate-and-merge:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ # Default checkout (no `ref:`) gets the BASE branch, not the PR HEAD.
28
+ # Our lint script lives there and is trusted.
29
+ - uses: actions/checkout@v4
30
+
31
+ - name: Confirm only KICKED_THE_TIRES.yml changed
32
+ id: check
33
+ env:
34
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35
+ PR_NUMBER: ${{ github.event.pull_request.number }}
36
+ REPO: ${{ github.repository }}
37
+ run: |
38
+ set -euo pipefail
39
+ changed=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only)
40
+ echo "Files in this PR:"
41
+ echo "$changed"
42
+
43
+ if [ "$changed" = "KICKED_THE_TIRES.yml" ]; then
44
+ echo "auto_merge=true" >> "$GITHUB_OUTPUT"
45
+ else
46
+ echo "auto_merge=false" >> "$GITHUB_OUTPUT"
47
+ echo "::notice::PR touches files beyond KICKED_THE_TIRES.yml; skipping auto-merge."
48
+ fi
49
+
50
+ - name: Fetch the proposed YAML content
51
+ if: steps.check.outputs.auto_merge == 'true'
52
+ env:
53
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54
+ PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
55
+ REPO: ${{ github.repository }}
56
+ run: |
57
+ set -euo pipefail
58
+ # Pull the file via the API rather than checking out the PR HEAD.
59
+ gh api "/repos/$REPO/contents/KICKED_THE_TIRES.yml?ref=$PR_HEAD_SHA" \
60
+ -H "Accept: application/vnd.github.raw" > KICKED_THE_TIRES.yml
61
+
62
+ - name: Validate schema
63
+ if: steps.check.outputs.auto_merge == 'true'
64
+ run: ruby script/lint-kicked-tires.rb
65
+
66
+ - name: Auto-merge
67
+ if: steps.check.outputs.auto_merge == 'true'
68
+ env:
69
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70
+ PR_NUMBER: ${{ github.event.pull_request.number }}
71
+ REPO: ${{ github.repository }}
72
+ PR_AUTHOR: ${{ github.event.pull_request.user.login }}
73
+ run: |
74
+ set -euo pipefail
75
+ gh pr comment "$PR_NUMBER" --repo "$REPO" \
76
+ --body "🎉 Thanks @${PR_AUTHOR} for kicking the tires! Auto-merging."
77
+ gh pr merge "$PR_NUMBER" --repo "$REPO" --squash --delete-branch
78
+
79
+ - name: Comment on validation failure
80
+ if: failure() && steps.check.outputs.auto_merge == 'true'
81
+ env:
82
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83
+ PR_NUMBER: ${{ github.event.pull_request.number }}
84
+ REPO: ${{ github.repository }}
85
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
86
+ run: |
87
+ gh pr comment "$PR_NUMBER" --repo "$REPO" \
88
+ --body "❌ Auto-merge skipped — the YAML didn't pass validation. See the [run log]($RUN_URL) for the specific error, then push a fix and the workflow will re-run."
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.1.0] - 2026-04-28
8
+
9
+ ### Added
10
+
11
+ - `gem-contribute scan [path]` — parse a `Gemfile.lock`, resolve each gem to its source repository, and rank GitHub-hosted projects by open `good first issue` count.
12
+ - `gem-contribute issues <gem|all>` — list open good-first-issues for a single gem or every github.com-hosted gem in the lockfile.
13
+ - `gem-contribute auth login|status|logout` — OAuth device-flow authentication with GitHub. Token cached at `~/.config/gem-contribute/auth.json` (mode 0600). The login flow auto-copies the one-time code to the clipboard and opens the verification URL in the browser.
14
+ - `gem-contribute fix <gem>/<issue#>` (alias: `fork-clone-branch`) — fork the gem's repo, clone the fork to `<clone_root>/<owner>/<repo>`, create a `gem-contribute/issue-<N>` branch from the default, and add an `upstream` remote pointing at the canonical project.
15
+ - `gem-contribute submit` — push the current branch to the user's fork and open a pre-filled GitHub compare page in the browser. The PR title and body are pre-populated from the issue (`Closes #<N>.`); the user reviews and submits via the web UI.
16
+ - `gem-contribute config set|get|list` — persistent user configuration at `~/.config/gem-contribute/config.yml`. `clone_root` controls where `fix` puts forks.
17
+ - `gem-contribute --refresh` — clear the disk cache before running (useful when source repositories have changed faster than the cache TTLs).
18
+ - gem-contribute auto-injects itself into its own `scan` and `issues` results, so the tool you're using is always one of the contribution targets you can see.
19
+ - Follows GitHub 301 redirects automatically when a repository has been renamed (e.g. `rainbow` → `ku1ik/rainbow`), so renamed projects keep their place in the rankings.
20
+
21
+ ### Notes
22
+
23
+ - v0.1 is GitHub-only. The `HostAdapter` interface is already in place so GitLab and others can land later without disturbing the data model.
24
+ - A Rooibos TUI on top of these commands is planned (see [issue #2](https://github.com/cdhagmann/gem-contribute/issues/2)).
data/CLAUDE.md ADDED
@@ -0,0 +1,47 @@
1
+ # CLAUDE.md
2
+
3
+ This file is read by Claude Code when working in this repository. Treat it as the contract for how to make changes here.
4
+
5
+ ## Project shape
6
+
7
+ `gem-contribute` is a terminal UI that reads a project's `Gemfile.lock`, surfaces open contributable issues from the gems' source repositories, and offers one-keystroke fork-clone-branch.
8
+
9
+ Read `docs/design.md` and the ADRs in `docs/adr/` before making non-trivial changes. The design doc describes the architecture and the ADRs explain why specific decisions were made. If a change conflicts with an ADR, propose updating the ADR first; don't silently violate it.
10
+
11
+ ## Working agreement
12
+
13
+ - **Decisions before code.** When uncertain about an architectural question, surface the question and the alternatives instead of picking one and writing code. The ADR pattern in `docs/adr/` is how those decisions get recorded.
14
+ - **Small PRs.** Each change should be reviewable in one sitting. Multi-commit PRs are fine; multi-concern PRs are not.
15
+ - **Test what's testable.** Parsers, resolvers, adapters, and `Update` functions all get tests. View tests assert colors and modifiers. System tests inject events and snapshot results. (The earlier "no TUI tests at v1" stance is obsolete; see ADR-0008.)
16
+ - **Match existing style.** Run `bin/rubocop` before opening a PR.
17
+ - **Don't reach across boundaries.** The TUI layer talks to the data layer only through Commands and messages. Adapters don't read config files; they receive what they need as arguments. The boundaries exist for testability and for the offline mode.
18
+ - **Async work is always a Rooibos Command.** Don't spawn threads. Don't use `Async`. Don't shell out synchronously. If it can take longer than ~50ms, it's a Command.
19
+
20
+ ## What's deliberately out of scope
21
+
22
+ The following are not bugs, they are design decisions. Don't "fix" them without first proposing an ADR update:
23
+
24
+ - Label normalization (ADR-0005)
25
+ - CONTRIBUTING.md parsing or summarization (ADR-0007)
26
+ - Bundler plugin packaging (ADR-0006)
27
+ - Direct threading or non-Rooibos async (ADR-0008)
28
+ - PR creation from inside the TUI
29
+ - Private repos or private gems at v1
30
+ - A standalone `Worker` orchestrator class. Fork-clone-branch is a state machine in `Update` driven by Commands. No orchestrator.
31
+
32
+ ## Tooling notes
33
+
34
+ - Ruby 3.2+ (Ractor support is required for Rooibos's thread-safe state)
35
+ - `ratatui_ruby` requires a Rust toolchain to build
36
+ - `rooibos` is the TUI framework on top of `ratatui_ruby`; pinned to `~> 0.7.0`
37
+ - Cache lives at `~/.cache/gem-contribute/`; nuke it with `gem-contribute --refresh`
38
+ - Auth tokens at `~/.config/gem-contribute/auth.json`, mode 0600
39
+ - Config at `~/.config/gem-contribute/config.yml`
40
+
41
+ ## When proposing changes
42
+
43
+ If you're adding a feature: which ADR(s) does it touch? If it doesn't touch any, do you need a new one?
44
+
45
+ If you're fixing a bug: is there a regression test? If not, why not?
46
+
47
+ If you're refactoring: what's the user-facing benefit? "Cleaner code" is not a benefit by itself.
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,46 @@
1
+ # Contributing
2
+
3
+ Thanks for considering a contribution. This project is *about* lowering the friction to open-source contribution, so we try to walk our talk.
4
+
5
+ ## Quick start
6
+
7
+ ```
8
+ git clone https://github.com/cdhagmann/gem-contribute
9
+ cd gem-contribute
10
+ bundle install
11
+ bin/rspec # tests should pass on a clean checkout
12
+ bin/gem-contribute # tool should run against this repo's own Gemfile.lock
13
+ ```
14
+
15
+ ## What we welcome
16
+
17
+ - Bug fixes, with a regression test where reasonable
18
+ - New host adapters (GitLab, Codeberg, sourcehut)
19
+ - Better error messages — there's no such thing as too clear here
20
+ - Documentation improvements, including in `docs/adr/` if you spot reasoning that's stale
21
+ - Performance improvements with before/after numbers
22
+ - Accessibility improvements to the TUI (color contrast, keyboard-only flows, screen-reader compatibility)
23
+
24
+ ## What we'd push back on
25
+
26
+ - Label normalization (see [ADR-0005](docs/adr/0005-render-labels-verbatim.md))
27
+ - Parsing CONTRIBUTING.md for structured data (see [ADR-0007](docs/adr/0007-display-contributing-verbatim.md))
28
+ - AI-anything that summarizes, suggests, or rewrites maintainer-authored content
29
+ - Bundler plugin packaging (see [ADR-0006](docs/adr/0006-standalone-gem-not-plugin.md)) — we'll consider it later, just not now
30
+
31
+ If you have a strong case for any of the above, open an issue first and let's talk before you write code.
32
+
33
+ ## PR expectations
34
+
35
+ - Run `bin/rubocop` and `bin/rspec` before pushing
36
+ - Write a clear commit message; the PR description should explain *why*, not just *what*
37
+ - New behavior gets a test
38
+ - New decisions of any consequence get an ADR. They're short — see existing ones for the format
39
+
40
+ ## Code of Conduct
41
+
42
+ Be kind. Assume good faith. The Ruby community deserves both. Specific incidents go to chris@example.com (placeholder — TODO: update before merging this).
43
+
44
+ ## AI assistance
45
+
46
+ This project was built with significant AI assistance and we're not hiding that. If you use AI to help write a contribution, that's fine; what's not fine is shipping code you don't understand. The bar for review is the same regardless of how the code was authored: you can explain why every line is there, and you can defend the design choices.
@@ -0,0 +1,22 @@
1
+ # People who've used gem-contribute against the sandbox issue (#5) to
2
+ # practice the fork → clone → branch → submit loop end-to-end.
3
+ #
4
+ # To add yourself: append a new entry below. Anything beyond `handle` and
5
+ # `date` is optional. `location` is reserved for a future Rooibos TUI
6
+ # view that plots contributors on Ratatui's world-map widget; providing
7
+ # it is opt-in. The map's geocoding script (added when that view lands)
8
+ # turns these strings into lat/long.
9
+ #
10
+ # Schema:
11
+ # - handle: your-github-handle # required, no leading @
12
+ # date: YYYY-MM-DD # required; the day you ran submit
13
+ # note: "one-line freeform message" # optional
14
+ # location: "City, Region, Country" # optional; e.g. "Asheville, NC, US"
15
+ # # or just "US" for country-level
16
+ #
17
+ # Use whatever precision you're comfortable with — "US" is fine.
18
+
19
+ - handle: cdhagmann
20
+ date: 2026-04-28
21
+ note: "Built gem-contribute. First through the loop."
22
+ location: "Durham, NC, US"
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Christopher Dean Hagmann
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/MAINTAINER.md ADDED
@@ -0,0 +1,92 @@
1
+ # Maintainer notes
2
+
3
+ This document is for the person who maintains the published `gem-contribute`
4
+ gem. Most contributors will never need to read it. It captures the few
5
+ out-of-band steps that can't be automated or committed.
6
+
7
+ ## OAuth App registration (one-time, required for Stage 2)
8
+
9
+ `gem-contribute` authenticates users with GitHub via the OAuth 2.0 Device
10
+ Authorization Grant. See [ADR-0004](docs/adr/0004-device-flow-auth.md) for
11
+ why we picked device flow over Personal Access Tokens.
12
+
13
+ Device flow needs only a **client ID**, no client secret. The client ID is a
14
+ public value that ships in the source tree — there is nothing to protect.
15
+ GitHub explicitly supports this pattern for CLI tools (see the [GitHub
16
+ docs][gh-device-flow]).
17
+
18
+ ### Steps
19
+
20
+ 1. Sign in to the GitHub account that will own the OAuth App. For now this
21
+ is Chris's personal account. If the gem is later donated to a more
22
+ permanent home, the OAuth App migrates with it (you'd register a new App
23
+ under the new owner and update `CLIENT_ID` in `lib/gem_contribute/auth.rb`).
24
+
25
+ 2. Go to <https://github.com/settings/developers>.
26
+
27
+ 3. Click **"OAuth Apps"** in the left sidebar, then **"New OAuth App"**.
28
+
29
+ 4. Fill in the form:
30
+
31
+ | Field | Value |
32
+ |--------------------------------|----------------------------------------------------------|
33
+ | Application name | `gem-contribute` |
34
+ | Homepage URL | `https://github.com/cdhagmann/gem-contribute` |
35
+ | Application description | `Find and contribute to the gems in your Gemfile.lock.` |
36
+ | Authorization callback URL | `https://github.com/cdhagmann/gem-contribute` |
37
+
38
+ The "Authorization callback URL" is a required field on the form but
39
+ isn't used by device flow. Pointing it at the repo URL is the
40
+ conventional dummy value.
41
+
42
+ 5. Click **"Register application"**.
43
+
44
+ 6. **Critical — enable Device Flow.** On the App's settings page after
45
+ registration, scroll down to the **"Device Flow"** section and check
46
+ **"Enable Device Flow"**, then click **"Update application"**. Without
47
+ this checkbox, the device-flow endpoints return
48
+ `device_flow_disabled` and the tool will not work. This step is easy to
49
+ miss because it's a separate save below the basic settings.
50
+
51
+ 7. Copy the **Client ID** from the App's settings page. It looks like
52
+ `Iv1.abcdef0123456789` for newer GitHub OAuth Apps or a 20-character
53
+ hex string for older ones. **Do not generate or copy a client secret.**
54
+ Device flow doesn't use one.
55
+
56
+ 8. Paste the Client ID back into the conversation with Claude Code, or
57
+ commit it directly to `lib/gem_contribute/auth.rb` as the value of the
58
+ `CLIENT_ID` constant. The current placeholder is a deliberate
59
+ sentinel that will raise at runtime.
60
+
61
+ ### Rate limits to know about
62
+
63
+ - **50 device-code requests per hour, per OAuth App.** This is the cap on
64
+ *starting* a device flow, not on completing one. Workshop scale (~12
65
+ attendees) is comfortably under. If the tool ever sees enough adoption
66
+ to brush this limit, register additional OAuth Apps (and round-robin
67
+ client IDs) or migrate to a GitHub App with refresh-token logic.
68
+
69
+ - **The user's own API rate limit** is the standard 5,000/hr authenticated.
70
+ No App-level cap.
71
+
72
+ ### Migrating the App later
73
+
74
+ If `gem-contribute` moves to an org or a different maintainer:
75
+
76
+ 1. The new owner registers a fresh OAuth App following the steps above.
77
+ 2. Update `CLIENT_ID` in `lib/gem_contribute/auth.rb` and ship a new gem
78
+ release.
79
+ 3. Existing users will see one auth re-prompt the next time they invoke an
80
+ auth-required command, because the new client ID won't match the cached
81
+ token's issuer. That's acceptable — it's effectively a one-time
82
+ re-login event.
83
+
84
+ The old OAuth App can stay registered for a transition period so users on
85
+ older gem versions continue to work.
86
+
87
+ ## Cutting a release
88
+
89
+ (Stub — fill in when we cut v0.1.0 to RubyGems. Notes will live here:
90
+ gemspec metadata checks, `bundle exec rake release` flow, signing, etc.)
91
+
92
+ [gh-device-flow]: https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # gem-contribute
2
+
3
+ Find contributable issues in the gems your project already depends on.
4
+
5
+ ```
6
+ $ gem-contribute scan
7
+ Scanning Gemfile.lock (44 gems)...
8
+ 44 gems · 42 on github.com · 2 unknown source
9
+
10
+ Top contributable projects (by open `good first issue` count):
11
+ rubocop 4 github.com/rubocop/rubocop
12
+ rspec 1 github.com/rspec/rspec
13
+ rspec-core 1 github.com/rspec/rspec
14
+ reline 1 github.com/ruby/reline
15
+ ...
16
+ gem-contribute 1 github.com/cdhagmann/gem-contribute
17
+ ```
18
+
19
+ The premise: the gems in your `Gemfile.lock` are the projects you have the most context on. If you depend on `sidekiq`, you have opinions about Sidekiq. That's a better starting point for open-source contribution than scanning all of GitHub for `good-first-issue` tags and hoping one looks interesting.
20
+
21
+ ## Status
22
+
23
+ Early. v0.1 is a CLI. A Rooibos TUI is planned (see [issue #2](https://github.com/cdhagmann/gem-contribute/issues/2)). This is being built as a workshop project for **[Blue Ridge Ruby 2026](https://blueridgeruby.com)**. Expect rough edges through the conference.
24
+
25
+ ## Install
26
+
27
+ ```
28
+ gem install gem-contribute
29
+ ```
30
+
31
+ Requires Ruby 3.2 or later.
32
+
33
+ ## Usage
34
+
35
+ The CLI is a small set of subcommands:
36
+
37
+ ```
38
+ gem-contribute scan [path] Summarize the contributable surface of a Gemfile.lock.
39
+ gem-contribute issues <gem|all> List "good first issue" issues for one gem (or all).
40
+ gem-contribute auth login Authenticate with GitHub via OAuth device flow.
41
+ gem-contribute fix <gem>/<issue#> Fork the gem's repo, clone the fork, branch from main.
42
+ gem-contribute submit Push the branch and open a pre-filled PR in the browser.
43
+ gem-contribute config set <key> <val> Persist user preferences (e.g. clone_root).
44
+ ```
45
+
46
+ A typical session:
47
+
48
+ ```sh
49
+ $ gem-contribute auth login # one-time; uses GitHub device flow
50
+ $ gem-contribute scan # see what's worth contributing to
51
+ $ gem-contribute issues rubocop # drill into one project's issues
52
+ $ gem-contribute fix rubocop/12345 # fork, clone, branch
53
+ $ cd ~/code/oss/rubocop/rubocop # (or wherever clone_root points)
54
+ # ... make your change, commit ...
55
+ $ gem-contribute submit # push + open the PR compare page in your browser
56
+ ```
57
+
58
+ The `auth login` step opens GitHub's device-flow page in your browser and copies the one-time code to your clipboard — same UX as `gh auth login`, no token paste, no client secret. Tokens cache at `~/.config/gem-contribute/auth.json` (mode 0600).
59
+
60
+ ## Configuration
61
+
62
+ User config lives at `~/.config/gem-contribute/config.yml`. Manage it with `gem-contribute config`:
63
+
64
+ ```sh
65
+ gem-contribute config set clone_root ~/Projects/oss
66
+ gem-contribute config list
67
+ ```
68
+
69
+ | Key | Default | Notes |
70
+ |--------------|--------------|--------------------------------------------------|
71
+ | `clone_root` | `~/code/oss` | Where `fix` clones forks (`<root>/<owner>/<repo>`). |
72
+
73
+ ## Design
74
+
75
+ See [`docs/design.md`](docs/design.md) for the architecture overview and [`docs/adr/`](docs/adr/) for individual decisions with their reasoning. The short version: scan first, auth lazily, abstract the source host so GitHub isn't the only option forever, render the data as the maintainer wrote it (don't normalize labels, don't summarize CONTRIBUTING).
76
+
77
+ ## Contributing
78
+
79
+ The tool is *for* finding contributable projects, so it had better be one. See [`CONTRIBUTING.md`](CONTRIBUTING.md). Issues tagged `good first issue` are real and reviewed.
80
+
81
+ If you're attending Blue Ridge Ruby 2026 and arrived here from the workshop, see [`docs/workshop.md`](docs/workshop.md) for the exercises.
82
+
83
+ ## Disclosure
84
+
85
+ Built with substantial assistance from Claude (Anthropic). Architecture, design decisions, and code review are mine; a fair amount of the typing isn't. Decisions are documented in `docs/adr/` partly so the reasoning is auditable independent of who or what produced the diff.
86
+
87
+ ## License
88
+
89
+ MIT.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
data/docs/_config.yml ADDED
@@ -0,0 +1,30 @@
1
+ title: gem-contribute
2
+ description: Find contributable issues in the gems your project already depends on.
3
+ url: https://cdhagmann.com
4
+ baseurl: /gem-contribute
5
+ theme: jekyll-theme-cayman
6
+ markdown: kramdown
7
+ plugins:
8
+ - jekyll-relative-links
9
+
10
+ # Process the existing markdown files in docs/ as pages, but don't try to
11
+ # render them as posts.
12
+ defaults:
13
+ - scope:
14
+ path: ""
15
+ values:
16
+ layout: default
17
+
18
+ # Don't try to publish working notes that aren't intended as docs.
19
+ exclude:
20
+ - claude-code-prompt.md
21
+ - prep-plan.md
22
+ - workshop-issues/
23
+
24
+ # Make markdown links to other .md files Just Work in the rendered HTML.
25
+ relative_links:
26
+ enabled: true
27
+ collections: true
28
+ include:
29
+ - CONTRIBUTING.md
30
+ - CHANGELOG.md
@@ -0,0 +1,44 @@
1
+ # ADR 0001: Just-in-time authentication
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-04-27
5
+
6
+ ## Context
7
+
8
+ `gem-contribute` reads a Gemfile.lock, resolves source URLs, and queries hosts (GitHub primarily) for issues. Issue browsing is read-only and works against public repos with no auth. Forking, cloning, and branching require auth.
9
+
10
+ Two reasonable architectures:
11
+
12
+ 1. **Auth at startup.** Prompt for OAuth on first run; everything is authenticated thereafter.
13
+ 2. **Auth just-in-time.** Run anonymously until the user takes an action that requires auth, then prompt.
14
+
15
+ ## Decision
16
+
17
+ Auth lazily, per host, on first action that requires it.
18
+
19
+ ## Reasoning
20
+
21
+ The lockfile-scanning and issue-browsing parts of the tool are useful without auth. A user who runs `gem-contribute` for the first time sees:
22
+
23
+ > 47 gems · 44 on github.com · 2 on gitlab.com · 1 unknown source
24
+ > Hit Enter on a gem to browse its issues.
25
+
26
+ That's value before the auth prompt. The prompt becomes "you want to fork this — let's connect your GitHub" instead of "before doing anything, please authorize this app."
27
+
28
+ Per-host matters because the tool is designed to grow GitLab and Codeberg adapters. Asking for GitHub auth on launch when the user only ever interacts with GitLab gems would be backwards.
29
+
30
+ ## Alternatives considered
31
+
32
+ - **Auth at startup.** Simpler to implement; worse UX. Rejected.
33
+ - **PAT-only (no OAuth).** Lower setup burden for the maintainer; higher for the user. See ADR-0004.
34
+
35
+ ## Consequences
36
+
37
+ - The host adapter must distinguish public-API methods from auth-required ones at the type level (`AuthRequired` exception).
38
+ - The TUI needs an auth-prompt overlay that can fire mid-session.
39
+ - The token cache is keyed by host, not global.
40
+ - Tests must cover both authenticated and anonymous paths for every adapter method.
41
+
42
+ ## Implementation note (post-ADR-0008)
43
+
44
+ With Rooibos as the TUI framework, the JIT auth flow is naturally expressed as a state machine in `Update`: an action that requires auth dispatches a `Command.http` against an adapter method that returns `AuthRequired`; the resulting message transitions the model into an auth-pending state; device-flow polling runs as a sequence of `Command.http` + `Command.wait` cycles; on success the model retries the original action. This is cleaner and more testable than the imperative interrupt-and-resume pattern that would have been required with bare `ratatui_ruby`. The decision in this ADR is unchanged; only the implementation gets nicer.
@@ -0,0 +1,35 @@
1
+ # ADR 0002: Use Bundler's lockfile parser
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-04-27
5
+
6
+ ## Context
7
+
8
+ We need to read `Gemfile.lock` and produce a list of gems with names, versions, and source types (rubygems / git / path).
9
+
10
+ ## Decision
11
+
12
+ Use `Bundler::LockfileParser` from the `bundler` gem.
13
+
14
+ ## Reasoning
15
+
16
+ Bundler is already a dependency of any project that has a Gemfile.lock, and it ships a parser that handles every edge case the lockfile format has accumulated over a decade. Writing our own parser is a guaranteed source of bugs that would mostly manifest on other people's machines, with their unusual lockfiles.
17
+
18
+ The parser API is stable and documented:
19
+
20
+ ```ruby
21
+ parser = Bundler::LockfileParser.new(File.read("Gemfile.lock"))
22
+ parser.specs # => Array of Bundler::LazySpecification
23
+ ```
24
+
25
+ Each spec has `.name`, `.version`, `.source` — exactly what we need.
26
+
27
+ ## Alternatives considered
28
+
29
+ - **Write our own line-by-line parser.** Tempting because the format looks simple, but it isn't. Plugins, git sources, path sources, platform-specific gems, and lockfile version differences all complicate it. Rejected.
30
+ - **Regex over the file.** No.
31
+
32
+ ## Consequences
33
+
34
+ - `bundler` is a runtime dependency. It's already on every Ruby developer's machine, but worth declaring explicitly.
35
+ - We're coupled to Bundler's internal API. If they change `LazySpecification`, we adapt. The risk is low and the upside is large.
@@ -0,0 +1,33 @@
1
+ # ADR 0003: Prefer `bug_tracker_uri` over `source_code_uri`
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-04-27
5
+
6
+ ## Context
7
+
8
+ The RubyGems v1 API returns metadata for a gem including several URIs from the gemspec: `homepage_uri`, `source_code_uri`, `bug_tracker_uri`, `documentation_uri`, `changelog_uri`, etc. Most of the time `source_code_uri` and `bug_tracker_uri` point to the same GitHub repo. Sometimes they don't.
9
+
10
+ A small but real fraction of gems host code on one platform and issues on another (commonly: code on GitHub Enterprise, issues on a public GitHub repo). For those gems, we want issues, not code.
11
+
12
+ ## Decision
13
+
14
+ Prefer `bug_tracker_uri`. Fall back to `source_code_uri`. Fall back to `homepage_uri` if it points at a recognized host. Otherwise mark the gem as `:unknown` source.
15
+
16
+ ## Reasoning
17
+
18
+ The tool is named `gem-contribute` and the primary action is "browse and respond to issues." The bug tracker is the canonical location of issues. If a maintainer set both URIs, they did so deliberately, and we should respect their choice.
19
+
20
+ For gems that only set `source_code_uri`, the fallback is correct because most of the time the source repo *is* the issue tracker.
21
+
22
+ The `homepage_uri` fallback is a hail-mary for gems whose maintainer never set the more specific URIs but happened to put a GitHub URL in the homepage field. In practice this catches a few percent of older gems.
23
+
24
+ ## Alternatives considered
25
+
26
+ - **Always use `source_code_uri`.** Loses the rare-but-real case where issues live elsewhere. Rejected.
27
+ - **Only use `bug_tracker_uri`, with no fallback.** Excludes too many gems. Rejected.
28
+ - **Try them all and let the user pick.** Workable, but the right answer is almost always the first non-nil one in our preference order. Don't make the user choose.
29
+
30
+ ## Consequences
31
+
32
+ - A small number of gems will have `bug_tracker_uri` pointing at something that isn't a host we have an adapter for (private Bugzilla, mailing list, etc.). Those gems become `:unknown` and aren't actionable. We surface them anyway because seeing "this gem has no contributable issue tracker" is itself useful information.
33
+ - We may want a `--prefer-source` flag eventually for users who specifically want code-level contributions over issue triage. Not in v1.