From d0eaa9b6418e1f753c9b9c315628e769bda51e58 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Sat, 28 Mar 2026 12:14:28 +0000 Subject: [PATCH 1/5] docs: add CLAUDE.md and symlink as copilot-instructions Add project guidance file for Claude Code with commands, architecture, testing, and code style info. Symlink .github/copilot-instructions.md to share the same content with GitHub Copilot. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/copilot-instructions.md | 1 + CLAUDE.md | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 120000 .github/copilot-instructions.md create mode 100644 CLAUDE.md 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/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). From ed1fc9e11d83f7c4d65a9a149a838d53c77a6899 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Sat, 28 Mar 2026 12:29:15 +0000 Subject: [PATCH 2/5] docs: add Claude Code rules and skills Add path-scoped rules for testing, services, and Slack modals that auto-load when working on matching files. Add skills for CI, release workflow, and PR review. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/rules/services.md | 13 ++++++++++++ .claude/rules/slack-modals.md | 12 +++++++++++ .claude/rules/testing.md | 15 ++++++++++++++ .claude/skills/ci/SKILL.md | 13 ++++++++++++ .claude/skills/release/SKILL.md | 19 ++++++++++++++++++ .claude/skills/review-pr/SKILL.md | 33 +++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+) create mode 100644 .claude/rules/services.md create mode 100644 .claude/rules/slack-modals.md create mode 100644 .claude/rules/testing.md create mode 100644 .claude/skills/ci/SKILL.md create mode 100644 .claude/skills/release/SKILL.md create mode 100644 .claude/skills/review-pr/SKILL.md 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) From 0bff86aea4e96bbf78422441b5bf956e9ef16d25 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Sat, 28 Mar 2026 12:34:35 +0000 Subject: [PATCH 3/5] fix(deps): update rack, rexml, and sinatra for security fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rack 3.1.16 → 3.2.5 (7 CVEs: DoS, directory traversal, XSS) - rexml 3.4.1 → 3.4.4 (DoS via malformed XML) - sinatra 4.1.1 → 4.2.1 (ReDoS in ETag headers) Co-Authored-By: Claude Opus 4.6 (1M context) --- Gemfile.lock | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) 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 From b5c1d98fb40ef8966616c132a0872eaeefe08cf4 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Sat, 28 Mar 2026 12:37:27 +0000 Subject: [PATCH 4/5] chore: drop Ruby 3.1, add 3.4 to CI matrix Ruby 3.1 reached EOL March 2025. Update CI matrix to 3.2, 3.3, 3.4 and bump RuboCop TargetRubyVersion to 3.2. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 2 +- .rubocop.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c6642c..f570247 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'] 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/**/*" From fbe842e57df6a1aa069c24448ab4ece13368ec31 Mon Sep 17 00:00:00 2001 From: Mark Allen Date: Sat, 28 Mar 2026 12:37:48 +0000 Subject: [PATCH 5/5] chore: add Ruby 4.0 to CI matrix Test against all supported Ruby versions: 3.2, 3.3, 3.4, 4.0. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f570247..e44e864 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - ruby-version: ['3.2', '3.3', '3.4'] + ruby-version: ['3.2', '3.3', '3.4', '4.0'] steps: - uses: actions/checkout@v4