diff --git a/.claude/rules/services.md b/.claude/rules/services.md new file mode 100644 index 0000000..650d2ee --- /dev/null +++ b/.claude/rules/services.md @@ -0,0 +1,13 @@ +--- +paths: + - "lib/services/**/*.rb" +--- + +# Service Conventions + +- All HTTP calls use Ruby's native `Net::HTTP` — do not add external HTTP client gems +- Services are plain Ruby classes initialized with tokens/config, no framework coupling +- Constructor pattern: `initialize(token, debug: false, logger: nil)` +- API clients define a private `api_request` method that handles GET/POST, auth headers, and JSON parsing +- Error handling: check response status/`ok` field, log via `debug_log`, return empty/false on failure rather than raising +- SlackService auto-joins channels on `not_in_channel` errors — preserve this retry pattern diff --git a/.claude/rules/slack-modals.md b/.claude/rules/slack-modals.md new file mode 100644 index 0000000..82c8bb8 --- /dev/null +++ b/.claude/rules/slack-modals.md @@ -0,0 +1,12 @@ +--- +paths: + - "lib/helpers/**/*.rb" +--- + +# Slack Modal Conventions + +- Modals use Slack Block Kit format — return Ruby hashes that serialize to JSON +- Two modal types: `global_shortcut_modal` (needs both thread URL and issue URL inputs) and `message_shortcut_modal` (only needs issue URL, thread context passed via `private_metadata`) +- `private_metadata` carries `channel_id` and `thread_ts` as JSON between shortcut trigger and submission +- Callback IDs follow the pattern `gh_comment_modal_` — these are matched in `app.rb` to route submissions +- Use `plain_text_input` elements with `block_id`/`action_id` pairs for form fields diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..b7f5915 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,15 @@ +--- +paths: + - "test/**/*.rb" +--- + +# Testing Rules + +- Use `Minitest::Spec` style (`describe`/`it` blocks) +- All external HTTP calls must be stubbed with WebMock — `WebMock.disable_net_connect!` is enforced globally +- Use the shared fixtures and stub helpers from `test/test_helper.rb`: + - Fixtures: `slack_message`, `slack_thread_response`, `slack_user_response`, `github_comment_response`, `slack_modal_payload` + - Stubs: `stub_slack_conversations_replies`, `stub_slack_users_info`, `stub_slack_chat_post_message`, `stub_github_create_comment` +- Integration tests use `Rack::Test` — call endpoints via `get`, `post` etc. and the `app` method returns `Sinatra::Application` +- `setup` calls `WebMock.reset!` automatically — no need to repeat it +- Test env vars (`SLACK_BOT_TOKEN`, `GITHUB_TOKEN`) are set in `test_helper.rb` diff --git a/.claude/skills/ci/SKILL.md b/.claude/skills/ci/SKILL.md new file mode 100644 index 0000000..49825fd --- /dev/null +++ b/.claude/skills/ci/SKILL.md @@ -0,0 +1,13 @@ +--- +name: ci +description: Run the full CI suite (syntax + rubocop + tests) and report results +allowed-tools: Bash(bundle exec *) +--- + +Run the full CI check suite: + +```bash +bundle exec rake ci +``` + +Report the results clearly. If any step fails, identify which step failed and show the relevant error output. diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 0000000..a04dc43 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,19 @@ +--- +name: release +description: Walk through the release process - preview changelog, bump version, create PR +disable-model-invocation: true +argument-hint: "[major|minor|patch]" +--- + +Create a release for this project. The bump type is: $ARGUMENTS (default to what `rake release:preview` suggests if not specified). + +Steps: + +1. Run `bundle exec rake release:preview` to show the current version, suggested bump type, and changelog preview +2. Confirm the bump type with the user before proceeding +3. Ensure the working directory is clean (`git status`) +4. Run `bundle exec rake ci` to verify all checks pass +5. Run `bundle exec rake release:$ARGUMENTS` to create the release (this bumps version, updates CHANGELOG.md, and creates a git tag) +6. Show the user the final commands to push: `git push origin main && git push origin ` + +Do NOT push automatically — let the user decide when to push. diff --git a/.claude/skills/review-pr/SKILL.md b/.claude/skills/review-pr/SKILL.md new file mode 100644 index 0000000..8b6e5d2 --- /dev/null +++ b/.claude/skills/review-pr/SKILL.md @@ -0,0 +1,33 @@ +--- +name: review-pr +description: Review a pull request for code style, architecture, and correctness +disable-model-invocation: true +context: fork +agent: Explore +argument-hint: "[PR number or URL]" +allowed-tools: Bash(gh *) +--- + +Review pull request $ARGUMENTS against this project's conventions. + +## Gather context + +- PR diff: !`gh pr view $ARGUMENTS --json additions,deletions,changedFiles` +- PR details: !`gh pr view $ARGUMENTS` +- Full diff: !`gh pr diff $ARGUMENTS` + +## Review checklist + +1. **Code style**: RuboCop compliance (120-char lines, 20-line methods, single quotes) +2. **Architecture**: Services stay in `lib/services/`, helpers in `lib/helpers/`. Services use Net::HTTP, not external HTTP gems +3. **Testing**: New functionality has corresponding tests. All HTTP calls are stubbed with WebMock. Tests use shared fixtures from `test_helper.rb` +4. **Commit messages**: Follow conventional commits format (`feat:`, `fix:`, `docs:`, etc.) +5. **Error handling**: API errors handled gracefully, debug logging via `debug_log` + +## Output + +Provide a structured review with: +- Summary of changes +- Issues found (if any), with file and line references +- Suggestions for improvement +- Overall assessment (approve / request changes) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 120000 index 0000000..949a29f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +../CLAUDE.md \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c6642c..e44e864 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - ruby-version: ['3.1', '3.2', '3.3'] + ruby-version: ['3.2', '3.3', '3.4', '4.0'] steps: - uses: actions/checkout@v4 diff --git a/.rubocop.yml b/.rubocop.yml index edf398c..7d62055 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,7 +8,7 @@ plugins: - rubocop-rake AllCops: - TargetRubyVersion: 3.1 + TargetRubyVersion: 3.2 NewCops: enable Exclude: - "vendor/**/*" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f78e4ce --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Slack-github-threads is a Ruby Sinatra app that exports Slack thread conversations as comments on GitHub issues. Users trigger it via a `/ghcomment` slash command or Slack shortcuts (global and message). + +## Common Commands + +```bash +bundle install # Install dependencies +bundle exec rake test # Run all tests +bundle exec rake test_services # Run service unit tests only +bundle exec rake test_app # Run integration tests only +bundle exec rake ci # Full CI: syntax + rubocop + tests +bundle exec rake rubocop # Lint only +bundle exec rake lint # Syntax check + rubocop +bundle exec rake server # Start dev server +DEBUG=true bundle exec ruby app.rb # Run with debug logging +``` + +To run a single test file: `bundle exec ruby test/services/test_text_processor.rb` + +## Architecture + +``` +Slack (slash command / shortcut) + → app.rb (Sinatra routing, 3 endpoints: GET /up, POST /ghcomment, POST /shortcut) + → CommentService (orchestration) + ├→ SlackService (fetch thread via conversations.replies, resolve user mentions) + ├→ TextProcessor (format messages: HTML entity decoding, @mention replacement) + └→ GitHubService (POST comment to issue via REST API) +``` + +- **app.rb** — Entry point. Routes requests, validates params, delegates to CommentService. +- **lib/services/comment_service.rb** — Orchestrates the flow: fetch thread → format → post to GitHub → reply in Slack. +- **lib/services/slack_service.rb** — Slack API client (Net::HTTP). Auto-joins channels if bot isn't a member. +- **lib/services/github_service.rb** — GitHub API client (Net::HTTP). Parses issue URLs to extract org/repo/number. +- **lib/services/text_processor.rb** — Converts Slack message formatting to GitHub-compatible markdown. +- **lib/helpers/modal_builder.rb** — Constructs Slack Block Kit modals for shortcut flows. +- **lib/version_helper.rb** — Semantic versioning and changelog generation from conventional commits. + +All API calls use Ruby's native `Net::HTTP` — no external HTTP client gems. + +## Testing + +- Framework: Minitest with `Minitest::Spec` style +- HTTP mocking: WebMock (all external calls must be stubbed) +- `test/test_helper.rb` provides shared fixtures (`slack_message`, `slack_thread_response`) and stub helpers (`stub_slack_conversations_replies`, `stub_github_create_comment`) +- Integration tests use `Rack::Test` against the Sinatra app + +## Environment Variables + +Required: `SLACK_BOT_TOKEN` (xoxb_*), `GITHUB_TOKEN` (ghp_*) +Optional: `DEBUG` (true/false), `RACK_ENV`, `PORT` (default 3000) + +## Code Style + +RuboCop enforced. Key rules: 120-char line limit, 20-line method limit, single quotes preferred. Uses rubocop-minitest and rubocop-rake plugins. + +## Commit Conventions + +Uses conventional commits: `feat:`, `fix:`, `docs:`, `chore:`, `refactor:`, `test:`, `perf:`, `style:`. Breaking changes use `feat!:` or `BREAKING CHANGE:` footer. These drive automatic version bumping (major/minor/patch). diff --git a/Gemfile.lock b/Gemfile.lock index 6500f63..eb52870 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,27 +11,30 @@ GEM rexml daemons (1.4.1) dotenv (3.1.8) + drb (2.2.3) eventmachine (1.2.7) hashdiff (1.2.0) json (2.13.2) language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) - minitest (5.25.5) - mustermann (3.0.3) + minitest (6.0.2) + drb (~> 2.0) + prism (~> 1.5) + mustermann (3.0.4) ruby2_keywords (~> 0.0.1) nio4r (2.7.4) parallel (1.27.0) parser (3.3.9.0) ast (~> 2.4.1) racc - prism (1.4.0) + prism (1.9.0) public_suffix (6.0.2) puma (7.0.1) nio4r (~> 2.0) racc (1.8.1) - rack (3.1.16) - rack-protection (4.1.1) + rack (3.2.5) + rack-protection (4.2.1) base64 (>= 0.1.0) logger (>= 1.6.0) rack (>= 3.0.0, < 4) @@ -43,7 +46,7 @@ GEM rainbow (3.1.1) rake (13.3.0) regexp_parser (2.10.0) - rexml (3.4.1) + rexml (3.4.4) rubocop (1.79.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -67,11 +70,11 @@ GEM rubocop (>= 1.72.1) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) - sinatra (4.1.1) + sinatra (4.2.1) logger (>= 1.6.0) mustermann (~> 3.0) rack (>= 3.0.0, < 4) - rack-protection (= 4.1.1) + rack-protection (= 4.2.1) rack-session (>= 2.0.0, < 3) tilt (~> 2.0) thin (2.0.1) @@ -79,10 +82,10 @@ GEM eventmachine (~> 1.0, >= 1.0.4) logger rack (>= 1, < 4) - tilt (2.6.1) + tilt (2.7.0) unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) - unicode-emoji (4.0.4) + unicode-emoji (4.2.0) webmock (3.25.1) addressable (>= 2.8.0) crack (>= 0.3.2) @@ -106,4 +109,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.6.8 + 4.0.3