Add GitHub App installation and PR review models
github_app_installations mirrors installations of the siGit Code GitHub App (synced from installation webhooks, soft-deleted on uninstall) and caches the 1h installation access token in-row so Puma and the jobs process share it. github_pr_reviews tracks one automated review per (repo, PR, head SHA) — the unique index is the idempotency guard against webhook redeliveries and synchronize storms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WhPzinYJUYgBed63XMvJX1
Claude committed
Jul 2, 2026 at 12:23 UTC
1c834c2672757904de949e610895ac42f1ad8862
5 files changed
+135
-1
app/models/github_app_installation.rb
+33
new file mode 100644
index 0000000..0a9141c
--- /dev/null
+++ b/app/models/github_app_installation.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# One installation of the siGit Code GitHub App on a github.com account
+# (user or org). Synced from `installation` webhooks; soft-deleted on
+# uninstall so late-arriving PR webhooks and in-flight jobs no-op instead of
+# hitting a missing row. Also caches the short-lived (1h) installation access
+# token in-row, so the Puma and jobs processes share it without relying on
+# Rails.cache.
+class GithubAppInstallation < ApplicationRecord
+ has_many :github_pr_reviews, dependent: :destroy
+
+ scope :active, -> { where(suspended_at: nil, deleted_at: nil) }
+
+ # Refresh the token when it has less than this long left, so a token that
+ # expires mid-review can't fail the job.
+ TOKEN_EXPIRY_MARGIN = 5.minutes
+
+ def active?
+ suspended_at.nil? && deleted_at.nil?
+ end
+
+ # The cached installation token, or nil when missing or too close to expiry.
+ def usable_access_token
+ return nil if access_token.blank? || access_token_expires_at.blank?
+ return nil if access_token_expires_at <= TOKEN_EXPIRY_MARGIN.from_now
+
+ access_token
+ end
+
+ def clear_access_token!
+ update!(access_token: nil, access_token_expires_at: nil)
+ end
+end
app/models/github_pr_review.rb
+24
new file mode 100644
index 0000000..80e2639
--- /dev/null
+++ b/app/models/github_pr_review.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# One automated review of one head SHA of one github.com pull request. The
+# unique (repo, pr, sha) index is the idempotency guard: GithubPrReviewJob
+# claims a row with create_or_find_by before doing any work, so webhook
+# redeliveries and rapid `synchronize` storms collapse into a single review.
+class GithubPrReview < ApplicationRecord
+ belongs_to :github_app_installation
+
+ enum :status, %w[pending running completed failed skipped].index_by(&:itself)
+
+ # A `running` row normally means another execution owns this SHA — but a
+ # worker killed mid-review (deploys kill -9) would leave the row running
+ # forever, so treat a long-stale claim as abandoned and re-enter.
+ STALE_RUNNING_AFTER = 30.minutes
+
+ def claimable?
+ pending? || (running? && started_at.present? && started_at < STALE_RUNNING_AFTER.ago)
+ end
+
+ def skip!(reason)
+ update!(status: "skipped", skip_reason: reason, completed_at: Time.current)
+ end
+end
db/migrate/20260702000001_create_github_app_installations.rb
+20
new file mode 100644
index 0000000..7385c01
--- /dev/null
+++ b/db/migrate/20260702000001_create_github_app_installations.rb
@@ -0,0 +1,20 @@
+class CreateGithubAppInstallations < ActiveRecord::Migration[8.1]
+ def change
+ create_table :github_app_installations do |t|
+ t.bigint :installation_id, null: false # GitHub's installation id
+ t.string :account_login, null: false # org/user the app is installed on
+ t.string :account_type # "User" | "Organization"
+ t.bigint :account_id
+ t.string :repository_selection # "all" | "selected"
+ t.datetime :suspended_at
+ t.datetime :deleted_at # soft delete on installation.deleted
+ t.string :access_token # cached installation token (expires within 1h)
+ t.datetime :access_token_expires_at
+
+ t.timestamps
+ end
+
+ add_index :github_app_installations, :installation_id, unique: true
+ add_index :github_app_installations, :account_login
+ end
+end
db/migrate/20260702000002_create_github_pr_reviews.rb
+22
new file mode 100644
index 0000000..8ba8a72
--- /dev/null
+++ b/db/migrate/20260702000002_create_github_pr_reviews.rb
@@ -0,0 +1,22 @@
+class CreateGithubPrReviews < ActiveRecord::Migration[8.1]
+ def change
+ create_table :github_pr_reviews do |t|
+ t.references :github_app_installation, null: false, foreign_key: true
+ t.string :repo_full_name, null: false # "owner/repo" on github.com
+ t.integer :pr_number, null: false
+ t.string :head_sha, null: false
+ t.string :status, null: false, default: "pending"
+ t.string :skip_reason # "draft", "too_large", "superseded", ...
+ t.text :error_message
+ t.integer :comment_count
+ t.string :model # onde tier that produced the review
+ t.datetime :started_at
+ t.datetime :completed_at
+
+ t.timestamps
+ end
+
+ add_index :github_pr_reviews, [ :repo_full_name, :pr_number, :head_sha ],
+ unique: true, name: "index_github_pr_reviews_on_repo_pr_sha"
+ end
+end
db/schema.rb
+36
-1
index 182a008..cd31f62 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.1].define(version: 2025_01_01_000010) do
+ActiveRecord::Schema[8.1].define(version: 2026_07_02_000002) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@@ -56,6 +56,40 @@ ActiveRecord::Schema[8.1].define(version: 2025_01_01_000010) do
t.index ["user_id"], name: "index_comments_on_user_id"
end
+ create_table "github_app_installations", force: :cascade do |t|
+ t.string "access_token"
+ t.datetime "access_token_expires_at"
+ t.bigint "account_id"
+ t.string "account_login", null: false
+ t.string "account_type"
+ t.datetime "created_at", null: false
+ t.datetime "deleted_at"
+ t.bigint "installation_id", null: false
+ t.string "repository_selection"
+ t.datetime "suspended_at"
+ t.datetime "updated_at", null: false
+ t.index ["account_login"], name: "index_github_app_installations_on_account_login"
+ t.index ["installation_id"], name: "index_github_app_installations_on_installation_id", unique: true
+ end
+
+ create_table "github_pr_reviews", force: :cascade do |t|
+ t.integer "comment_count"
+ t.datetime "completed_at"
+ t.datetime "created_at", null: false
+ t.text "error_message"
+ t.bigint "github_app_installation_id", null: false
+ t.string "head_sha", null: false
+ t.string "model"
+ t.integer "pr_number", null: false
+ t.string "repo_full_name", null: false
+ t.string "skip_reason"
+ t.datetime "started_at"
+ t.string "status", default: "pending", null: false
+ t.datetime "updated_at", null: false
+ t.index ["github_app_installation_id"], name: "index_github_pr_reviews_on_github_app_installation_id"
+ t.index ["repo_full_name", "pr_number", "head_sha"], name: "index_github_pr_reviews_on_repo_pr_sha", unique: true
+ end
+
create_table "issues", force: :cascade do |t|
t.text "body"
t.datetime "closed_at"
@@ -157,6 +191,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_01_01_000010) do
add_foreign_key "cloud_sessions", "users"
add_foreign_key "cloud_usages", "users"
add_foreign_key "comments", "users"
+ add_foreign_key "github_pr_reviews", "github_app_installations"
add_foreign_key "issues", "repositories"
add_foreign_key "issues", "users"
add_foreign_key "pull_requests", "repositories"