plan_my_stuff
A Rails engine gem that provides a GitHub-backed ticketing and project tracking layer for internal Rails apps. Wraps GitHub Issues and Projects V2 via Octokit, handling issue CRUD, comments with visibility controls, project board management, and a configurable request gateway.
Ruby: >= 3.3 | Rails: >= 6.1, < 8 | API client: Octokit
[!IMPORTANT] commonmarker version compatibility: This gem requires commonmarker v1.0+ (pinned
~> 2.7). The v0.23 to v1.0 upgrade was a full rewrite with breaking changes:
- Module renamed:
CommonMarkertoCommonmarker- Methods renamed:
render_htmltoto_html,render_doctoparse- Options changed from array of symbols (
[:HARDBREAKS]) to nested hash (options: { render: { hardbreaks: true } })- Underlying engine changed from C (libcmark-gfm) to Rust (comrak)
If your app already uses commonmarker < 1.0, you will need to upgrade. Alternatively, configure
markdown_renderer = :redcarpetornilto avoid the dependency entirely.
Installation
Add to your Gemfile:
gem 'plan_my_stuff'
Then run:
bundle install
rails generate plan_my_stuff:install
This creates config/initializers/plan_my_stuff.rb with documented configuration options.
Mount the engine in config/routes.rb:
mount PlanMyStuff::Engine, at: '/tickets'
Markdown renderer
The gem supports three markdown rendering options. The chosen gem must be in your Gemfile - plan_my_stuff does not declare either as a runtime dependency.
| Option | Gemfile requirement | Config |
|---|---|---|
| commonmarker (default) | gem 'commonmarker', '~> 2.7' |
config.markdown_renderer = :commonmarker |
| redcarpet | gem 'redcarpet' |
config.markdown_renderer = :redcarpet |
| None (raw) | Nothing | config.markdown_renderer = nil |
Overriding views
Copy the default views into your app for customization:
rails generate plan_my_stuff:views
Configuration
# config/initializers/plan_my_stuff.rb
PlanMyStuff.configure do |config|
# Auth (PAT from a bot account with repo + project scopes)
config.access_token = ENV['PMS_GITHUB_TOKEN']
# Organization
config.organization = 'YourOrganization'
# Named repo configs
config.repos[:marketing_website] = 'YourOrganization/MarketingWebsite'
config.repos[:cms_website] = 'YourOrganization/CMSWebsite'
config.default_repo = :cms_website
# Default project board
config.default_project_number = 123
# User class (your app's model)
config.user_class = 'User'
config.display_name_method = :full_name
config.user_id_method = :id
# Support role check (symbol or proc)
config.support_method = :support?
# Markdown rendering (:commonmarker, :redcarpet, or nil)
config.markdown_renderer = :commonmarker
# Request gateway (proc or nil; nil = always send)
config.should_send_request = nil
# Background jobs (when gateway defers a request)
config.job_classes = {
create_ticket: 'PmsCreateTicketJob',
post_comment: 'PmsPostCommentJob',
update_status: 'PmsUpdateStatusJob'
}
# Deferred request notifications
config.deferred_notifier = nil
config.deferred_email_from = 'noreply@example.com'
config.deferred_email_to = 'support@example.com'
# Custom fields (stored in issue/comment metadata)
config.custom_fields = {
ticket_type: { type: :string },
notification_recipients: { type: :array }
}
# App name (appears in metadata)
config.app_name = 'MyApp'
end
The PMS alias is available for brevity: PMS.configure, PMS::Issue.find, etc.
Usage
Issues
# Create
issue = PMS::Issue.create!(
title: 'Login page broken on Safari',
body: 'Users report blank screen after clicking login...',
repo: :cms_website,
labels: ['bug', 'app:cms_website'],
user: current_user,
add_to_project: true
)
# Find
issue = PMS::Issue.find(repo: :cms_website, number: 123)
issue.title
issue.body # body without metadata
issue. # PlanMyStuff::IssueMetadata
issue.visible_to?(user) # visibility check
issue.comments # all comments
issue.pms_comments # only PMS-created comments
# List
issues = PMS::Issue.list(repo: :cms_website, state: :open, labels: ['bug'])
# Update
issue.update!(title: 'Updated title', labels: ['bug', 'P1'])
# Close / Reopen
issue.update!(state: :closed)
issue.update!(state: :open)
# Viewer management (visibility allowlist)
PMS::Issue.add_viewers(repo: :cms_website, number: 123, user_ids: [5, 12])
PMS::Issue.remove_viewers(repo: :cms_website, number: 123, user_ids: [5])
Comments
# Create
comment = PMS::Comment.create!(
repo: :cms_website,
issue_number: 123,
body: 'Deployed fix to staging, please retest.',
user: current_user,
visibility: :public # or :internal (support-only)
)
# List
comments = PMS::Comment.list(repo: :cms_website, issue_number: 123)
comments = PMS::Comment.list(repo: :cms_website, issue_number: 123, pms_only: true)
comment.body # visible text
comment. # PlanMyStuff::CommentMetadata (nil for non-PMS comments)
comment.visibility # :public, :internal, or nil
comment.pms_comment? # true/false
comment.visible_to?(user)
Labels
PMS::Label.add(repo: :cms_website, issue_number: 123, labels: ['in-progress'])
PMS::Label.remove(repo: :cms_website, issue_number: 123, labels: ['triage'])
Projects
# List all projects
projects = PMS::Project.list
# View a project board
project = PMS::Project.find(14)
project.title
project.statuses # [{id: "ae429385", name: "On Deck"}, ...]
project.items # Array<PMS::ProjectItem>
# Move item to a status column
item = project.items.first
item.move_to!('In Review')
# Assign item
item.assign!('octocat')
# Add existing issue to project
PMS::Project.add_item(project_number: 14, issue_repo: :cms_website, issue_number: 123)
# Add draft item
PMS::Project.add_draft_item(project_number: 14, title: 'Draft task', body: 'Details...')
Metadata
All state lives on GitHub. Issue and comment metadata is stored as a hidden HTML comment on the first line of the body:
<!-- pms-metadata:{"gem_version":"0.0.0","app_name":"MyApp","created_at":"2026-03-24T10:00:00Z",...} -->
This is invisible when rendered on GitHub and parsed automatically by the gem into typed objects (PMS::IssueMetadata, PMS::CommentMetadata).
Verify setup
rails plan_my_stuff:verify
Checks token validity, org access, repo access, and project access.
Testing
The gem provides test helpers for consuming apps:
# spec/spec_helper.rb (or rails_helper.rb)
require 'plan_my_stuff/test_helpers'
RSpec.configure do |config|
config.include PlanMyStuff::TestHelpers
end
Available in tests:
PlanMyStuff.test_mode! # stubs all API calls
build_issue(title: 'Test issue')
build_comment(body: 'Test comment')
build_project(title: 'Test board')
expect_pms_issue_created(title: 'Test issue')
expect_pms_comment_created(body: 'Test comment')
expect_pms_item_moved(status: 'In Review')
Engine routes
The engine provides these routes (all under the configured mount point):
| Route | Action |
|---|---|
GET / |
Issue index |
GET /issues/new |
New issue form |
POST /issues |
Create issue |
GET /issues/:id |
Issue detail with comments |
GET /issues/:id/edit |
Edit issue form |
PATCH /issues/:id |
Update issue |
PATCH /issues/:id/close |
Close issue |
PATCH /issues/:id/reopen |
Reopen issue |
POST /issues/:id/comments |
Create comment |
GET /issues/:id/comments/:id/edit |
Edit comment |
PATCH /issues/:id/comments/:id |
Update comment |
POST /issues/:id/labels |
Add label |
DELETE /issues/:id/labels/:name |
Remove label |
GET /projects |
Project list |
GET /projects/:id |
Project board |
POST /projects/:id/items |
Add item to project |
PATCH /projects/:id/items/:id/move |
Move item |
PATCH /projects/:id/items/:id/assign |
Assign item |