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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/workshop-issue.md +29 -0
- data/.github/workflows/auto-merge-kicked-tires.yml +88 -0
- data/CHANGELOG.md +24 -0
- data/CLAUDE.md +47 -0
- data/CONTRIBUTING.md +46 -0
- data/KICKED_THE_TIRES.yml +22 -0
- data/LICENSE +21 -0
- data/MAINTAINER.md +92 -0
- data/README.md +89 -0
- data/Rakefile +10 -0
- data/docs/_config.yml +30 -0
- data/docs/adr/0001-just-in-time-auth.md +44 -0
- data/docs/adr/0002-bundler-lockfile-parser.md +35 -0
- data/docs/adr/0003-issue-tracker-preference.md +33 -0
- data/docs/adr/0004-device-flow-auth.md +36 -0
- data/docs/adr/0005-render-labels-verbatim.md +46 -0
- data/docs/adr/0006-standalone-gem-not-plugin.md +31 -0
- data/docs/adr/0007-display-contributing-verbatim.md +39 -0
- data/docs/adr/0008-rooibos-tui-framework.md +62 -0
- data/docs/adr/0009-top-level-namespace.md +37 -0
- data/docs/adr/README.md +21 -0
- data/docs/claude-code-prompt.md +40 -0
- data/docs/design.md +234 -0
- data/docs/index.md +102 -0
- data/docs/prep-plan.md +165 -0
- data/docs/workshop.md +60 -0
- data/exe/gem-contribute +7 -0
- data/lib/gem_contribute/auth.rb +161 -0
- data/lib/gem_contribute/cache.rb +98 -0
- data/lib/gem_contribute/cli/auth.rb +164 -0
- data/lib/gem_contribute/cli/config.rb +87 -0
- data/lib/gem_contribute/cli/fork_clone_branch.rb +197 -0
- data/lib/gem_contribute/cli/issues.rb +123 -0
- data/lib/gem_contribute/cli/scan.rb +117 -0
- data/lib/gem_contribute/cli/submit.rb +155 -0
- data/lib/gem_contribute/cli.rb +104 -0
- data/lib/gem_contribute/config.rb +60 -0
- data/lib/gem_contribute/errors.rb +32 -0
- data/lib/gem_contribute/host_adapter.rb +40 -0
- data/lib/gem_contribute/host_adapters/github_adapter.rb +215 -0
- data/lib/gem_contribute/locked_gem.rb +26 -0
- data/lib/gem_contribute/lockfile_parser.rb +61 -0
- data/lib/gem_contribute/project.rb +21 -0
- data/lib/gem_contribute/resolver.rb +131 -0
- data/lib/gem_contribute/token_store.rb +86 -0
- data/lib/gem_contribute/version.rb +5 -0
- data/lib/gem_contribute.rb +32 -0
- data/script/lint-kicked-tires.rb +76 -0
- data/sig/gem_contribute.rbs +3 -0
- 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
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.
|