Skip to content

Conversation

myftija
Copy link
Member

@myftija myftija commented Sep 2, 2025

This PR implements the GitHub installation flow for the new Trigger.dev app.
An httpOnly state cookie is used to verify the installation session.

We cache some basic details about the installation repos to avoid querying the
GitHub API when we need to interact with the repos.

Copy link

changeset-bot bot commented Sep 2, 2025

⚠️ No Changeset found

Latest commit: 9a7eb49

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

coderabbitai bot commented Sep 2, 2025

Walkthrough

Adds GitHub App support: environment schema now includes a discriminated union gated by GITHUB_APP_ENABLED. Introduces GitHub App services (initialization, link/update installation, repo pagination), a session-based install flow (create/validate/destroy cookie session), two new Remix routes for install and callback handling, utility rename/strengthening (safeRedirect → sanitizeRedirectPath), Prisma schema/models and SQL migration for GithubAppInstallation and GithubRepository, an octokit dependency, and a small import/migration comment tweak.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Reasoning: Wide-ranging, heterogeneous changes touching environment validation, security-sensitive session handling, new route control flow, external API integration (Octokit), DB schema + migration, and data persistence logic — each requires careful, separate review.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch gh-app-installation-flow

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/webapp/app/routes/auth.github.ts (1)

6-16: Harden redirect and cookie: prevent open redirects; set secure cookie in production.

  • Validate redirectTo to avoid open redirects (use remix-utils safeRedirect).
  • Mark the cookie as secure in production and set sameSite.

Apply:

@@
-import { type ActionFunction, type LoaderFunction, redirect, createCookie } from "@remix-run/node";
+import { type ActionFunction, type LoaderFunction, redirect, createCookie } from "@remix-run/node";
+import { safeRedirect } from "remix-utils";
+import { env } from "~/env.server";
@@
   const url = new URL(request.url);
-  const redirectTo = url.searchParams.get("redirectTo");
+  const redirectTo = safeRedirect(url.searchParams.get("redirectTo") ?? "/", "/");
@@
 export const redirectCookie = createCookie("redirect-to", {
   maxAge: 60 * 60, // 1 hour
   httpOnly: true,
+  sameSite: "lax",
+  secure: env.NODE_ENV === "production",
 });

Also applies to: 29-33

🧹 Nitpick comments (10)
apps/webapp/package.json (1)

160-160: Octokit usage is server‐only; consider scoped packages for GitHub App auth

Optionally:

  • Use @octokit/app, @octokit/auth-app, and @octokit/rest + @octokit/plugin-paginate-rest instead of the umbrella octokit to narrow bundle and typings.
  • Confirm your engines.node meets Octokit v3.2.1 requirements (Node ≥ 16).
apps/webapp/app/env.server.ts (1)

248-250: Nit: comment typo.

“refix 250 tokens” → “refill 250 tokens”.

-    API_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(250), // refix 250 tokens every 10 seconds
+    API_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(250), // refill 250 tokens every 10 seconds
apps/webapp/app/routes/_app.github.install/route.tsx (1)

2-2: Consistency nit: use the same redirect import source as the callback route.

The callback route uses "@remix-run/node". Consider standardizing import source for redirect across routes.

apps/webapp/app/routes/_app.github.callback/route.tsx (1)

42-44: Redirect to a safe default on invalid session.

Optional: consider redirecting to the last known redirect target if available; otherwise "/" is fine.

apps/webapp/app/services/gitHubSession.server.ts (3)

25-27: Also require GITHUB_APP_SLUG when the app feature is enabled.
Defensive check in case env parsing ever regresses.

Add a refinement in env.server.ts:

// apps/webapp/app/env.server.ts
GITHUB_APP_ENABLED: z.enum(["0","1"]),
GITHUB_APP_SLUG: z.string().optional(),
}).refine(
  (e) => e.GITHUB_APP_ENABLED === "0" || !!e.GITHUB_APP_SLUG,
  { message: "GITHUB_APP_SLUG is required when GITHUB_APP_ENABLED=1" }
);

107-112: Consider consuming the session on successful validation.
Prevents replay within the 1h window if the callback is hit twice.

You could add a consume helper that validates then returns a Set-Cookie header to clear:

export async function consumeGitHubAppInstallSession(cookieHeader: string | null, state: string) {
  const result = await validateGitHubAppInstallSession(cookieHeader, state);
  const destroyHeader = await destroyGitHubAppInstallSession(cookieHeader);
  return { ...result, destroyHeader };
}

2-2: Use node:crypto import.
Minor Node ESM best practice and avoids bundler shims.

-import { randomBytes } from "crypto";
+import { randomBytes } from "node:crypto";
internal-packages/database/prisma/schema.prisma (2)

2247-2269: Prefer BigInt for GitHub numeric IDs (future-proof).
GitHub IDs aren’t guaranteed to stay within 32-bit. Using BigInt avoids overflow risk.

 model GithubAppInstallation {
   id String @id @default(cuid())

-  appInstallationId   Int                       @unique
-  targetId            Int
+  appInstallationId   BigInt                    @unique
+  targetId            BigInt
   targetType          String
   accountHandle       String
   permissions         Json?
   repositorySelection GithubRepositorySelection
   installedBy         String?
   ...
 }

2271-2289: Indexes and types for repository lookups.

  • Use BigInt for githubId.
  • Add indexes you’ll likely need (by githubId and fullName).
 model GithubRepository {
   id String @id @default(cuid())

-  githubId      Int
+  githubId      BigInt
   name          String
   fullName      String
   htmlUrl       String
   private       Boolean
   defaultBranch String
   ...
-  @@unique([installationId, githubId])
-  @@index([installationId])
+  @@unique([installationId, githubId])
+  @@index([installationId])
+  @@index([githubId])
+  @@index([fullName(ops: raw("text_pattern_ops"))], type: BTree)
 }
internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql (1)

5-7: Match SQL types to BigInt if you adopt the Prisma change.
Keeps Prisma and DB in sync and future-proofs IDs.

-    "appInstallationId" INTEGER NOT NULL,
-    "targetId" INTEGER NOT NULL,
+    "appInstallationId" BIGINT NOT NULL,
+    "targetId" BIGINT NOT NULL,
...
-    "githubId" INTEGER NOT NULL,
+    "githubId" BIGINT NOT NULL,

Optional helpful indexes:

+CREATE INDEX "GithubRepository_githubId_idx" ON "public"."GithubRepository"("githubId");
+CREATE INDEX "GithubRepository_fullName_idx" ON "public"."GithubRepository" USING btree ("fullName" text_pattern_ops);
+CREATE INDEX "GithubAppInstallation_active_idx" ON "public"."GithubAppInstallation"("organizationId") WHERE "deletedAt" IS NULL AND "suspendedAt" IS NULL;

Also applies to: 23-24

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ddbae6b and 7507028.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • apps/webapp/app/env.server.ts (1 hunks)
  • apps/webapp/app/routes/_app.github.callback/route.tsx (1 hunks)
  • apps/webapp/app/routes/_app.github.install/route.tsx (1 hunks)
  • apps/webapp/app/routes/auth.github.ts (1 hunks)
  • apps/webapp/app/services/gitHub.server.ts (1 hunks)
  • apps/webapp/app/services/gitHubSession.server.ts (1 hunks)
  • apps/webapp/package.json (2 hunks)
  • internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql (1 hunks)
  • internal-packages/database/prisma/migrations/migration_lock.toml (1 hunks)
  • internal-packages/database/prisma/schema.prisma (3 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code
For TypeScript, we usually use types over interfaces
Avoid enums
No default exports, use function declarations

Files:

  • apps/webapp/app/services/gitHub.server.ts
  • apps/webapp/app/routes/_app.github.callback/route.tsx
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/routes/_app.github.install/route.tsx
  • apps/webapp/app/routes/auth.github.ts
  • apps/webapp/app/services/gitHubSession.server.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

We use zod a lot in packages/core and in the webapp

Files:

  • apps/webapp/app/services/gitHub.server.ts
  • apps/webapp/app/routes/_app.github.callback/route.tsx
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/routes/_app.github.install/route.tsx
  • apps/webapp/app/routes/auth.github.ts
  • apps/webapp/app/services/gitHubSession.server.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

When importing from @trigger.dev/core in the webapp, never import the root package path; always use one of the documented subpath exports from @trigger.dev/core’s package.json

Files:

  • apps/webapp/app/services/gitHub.server.ts
  • apps/webapp/app/routes/_app.github.callback/route.tsx
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/routes/_app.github.install/route.tsx
  • apps/webapp/app/routes/auth.github.ts
  • apps/webapp/app/services/gitHubSession.server.ts
{apps/webapp/app/**/*.server.{ts,tsx},apps/webapp/app/routes/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access environment variables only via the env export from app/env.server.ts; do not reference process.env directly

Files:

  • apps/webapp/app/services/gitHub.server.ts
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/routes/auth.github.ts
  • apps/webapp/app/services/gitHubSession.server.ts
apps/webapp/app/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Modules intended for test consumption under apps/webapp/app/**/*.ts must not read environment variables; accept configuration via options instead

Files:

  • apps/webapp/app/services/gitHub.server.ts
  • apps/webapp/app/env.server.ts
  • apps/webapp/app/routes/auth.github.ts
  • apps/webapp/app/services/gitHubSession.server.ts
🧠 Learnings (2)
📚 Learning: 2025-08-29T10:06:49.283Z
Learnt from: CR
PR: triggerdotdev/trigger.dev#0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-08-29T10:06:49.283Z
Learning: Applies to {apps/webapp/**/__tests__/**/*.{ts,tsx},apps/webapp/**/*.{test,spec}.{ts,tsx}} : Do not import app/env.server.ts into tests, either directly or indirectly

Applied to files:

  • apps/webapp/app/env.server.ts
📚 Learning: 2025-08-29T10:06:49.283Z
Learnt from: CR
PR: triggerdotdev/trigger.dev#0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-08-29T10:06:49.283Z
Learning: Applies to {apps/webapp/app/**/*.server.{ts,tsx},apps/webapp/app/routes/**/*.ts} : Access environment variables only via the env export from app/env.server.ts; do not reference process.env directly

Applied to files:

  • apps/webapp/app/env.server.ts
🧬 Code graph analysis (5)
apps/webapp/app/services/gitHub.server.ts (1)
apps/webapp/app/env.server.ts (1)
  • env (1157-1157)
apps/webapp/app/routes/_app.github.callback/route.tsx (6)
apps/webapp/app/routes/_app.github.install/route.tsx (1)
  • loader (15-49)
apps/webapp/app/models/message.server.ts (2)
  • redirectWithErrorMessage (181-198)
  • redirectWithSuccessMessage (162-179)
apps/webapp/app/services/gitHubSession.server.ts (1)
  • validateGitHubAppInstallSession (50-112)
apps/webapp/app/services/session.server.ts (1)
  • requireUser (39-62)
apps/webapp/app/db.server.ts (1)
  • $replica (103-106)
apps/webapp/app/services/gitHub.server.ts (1)
  • linkGitHubAppInstallation (19-56)
apps/webapp/app/env.server.ts (2)
apps/webapp/app/utils/db.ts (1)
  • isValidDatabaseUrl (1-18)
apps/webapp/app/utils/boolEnv.ts (1)
  • BoolEnv (12-14)
apps/webapp/app/routes/_app.github.install/route.tsx (5)
apps/webapp/app/routes/_app.github.callback/route.tsx (1)
  • loader (17-95)
apps/webapp/app/services/session.server.ts (1)
  • requireUser (39-62)
apps/webapp/app/db.server.ts (1)
  • $replica (103-106)
apps/webapp/app/utils/pathBuilder.ts (1)
  • newOrganizationPath (96-98)
apps/webapp/app/services/gitHubSession.server.ts (1)
  • createGitHubAppInstallSession (21-45)
apps/webapp/app/services/gitHubSession.server.ts (2)
apps/webapp/app/models/message.server.ts (1)
  • createCookieSessionStorage (19-28)
apps/webapp/app/env.server.ts (1)
  • env (1157-1157)
🔇 Additional comments (8)
internal-packages/database/prisma/migrations/migration_lock.toml (1)

2-3: LGTM: comment tweak only, no behavioral change.

The wording update is fine; provider remains unchanged.

apps/webapp/package.json (1)

57-57: Dependency reordering is harmless.

No functional impact from moving @internal/redis below @internal/cache.

apps/webapp/app/routes/auth.github.ts (1)

1-1: Import consolidation looks good.

Type-only specifiers with runtime imports from @remix-run/node are fine.

apps/webapp/app/services/gitHub.server.ts (1)

36-56: Use upsert for idempotent installation linking

Confirmed appInstallationId is unique; switch to upsert to avoid unique-constraint errors and duplicate repositories.

-  await prisma.githubAppInstallation.create({
-    data: {
+  await prisma.githubAppInstallation.upsert({
+    where: { appInstallationId: installationId },
+    create: {
       appInstallationId: installationId,
       organizationId,
       targetId: installation.target_id,
       targetType: installation.target_type,
       accountHandle: installation.account
         ? "login" in installation.account
           ? installation.account.login
           : "slug" in installation.account
           ? installation.account.slug
           : "-"
         : "-",
       permissions: installation.permissions,
       repositorySelection,
       repositories: {
         create: repositories,
       },
-    },
+    },
+    update: {
+      organizationId,
+      targetId: installation.target_id,
+      targetType: installation.target_type,
+      accountHandle: installation.account
+        ? "login" in installation.account
+          ? installation.account.login
+          : "slug" in installation.account
+          ? installation.account.slug
+          : "-"
+        : "-",
+      permissions: installation.permissions,
+      repositorySelection,
+      repositories: {
+        deleteMany: {},
+        create: repositories,
+      },
+    },
   });
apps/webapp/app/services/gitHubSession.server.ts (1)

6-16: Cookie settings look solid (httpOnly, secure in prod, 1h TTL).
Good alignment with our session policy.

internal-packages/database/prisma/schema.prisma (3)

154-166: Whitespace-only change.
No action needed.


213-214: Nice: back-reference from Organization to installations.
Keeps navigation cheap for org-scoped queries.


2242-2246: Enum aligns with GitHub’s repository_selection.
Looks good.

@myftija myftija force-pushed the gh-app-installation-flow branch from 8c5ebd1 to 2809981 Compare September 2, 2025 11:31
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/webapp/app/env.server.ts (1)

6-26: Good fix: preprocess restores safe default for discriminated union

Prepending GITHUB_APP_ENABLED: "0" via z.preprocess resolves the prior parse failure when the key is unset. ✔︎

apps/webapp/app/services/gitHubSession.server.ts (1)

31-36: Sanitize redirectTo (acknowledging maintainer decision)

Storing redirectTo without validation can enable open redirects on callback. If you reconsider, gate to safe relative paths before saving.

-  session.set("redirectTo", redirectTo);
+  const safeRedirectTo =
+    typeof redirectTo === "string" &&
+    redirectTo.startsWith("/") &&
+    !redirectTo.startsWith("//") &&
+    !redirectTo.includes("://")
+      ? redirectTo
+      : "/app";
+  session.set("redirectTo", safeRedirectTo);
🧹 Nitpick comments (7)
internal-packages/database/prisma/schema.prisma (4)

2258-2260: Consider cascading on org delete/update for consistency.
Many models here cascade from Organization; match that behavior unless you intentionally restrict deletes.

-  organization   Organization @relation(fields: [organizationId], references: [id])
+  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)

2247-2256: Prefer an enum for targetType.
GitHub installation targets are “User” or “Organization”. An enum prevents drift and eases querying.

+enum GithubInstallationTargetType {
+  USER
+  ORGANIZATION
+}
...
-  targetType          String
+  targetType          GithubInstallationTargetType

2287-2289: Add a lookup index by fullName under an installation (optional).
If you commonly resolve repos by owner/name, this speeds queries; keep githubId as the source of truth.

   @@unique([installationId, githubId])
-  @@index([installationId])
+  @@index([installationId])
+  @@index([installationId, fullName])

2276-2276: Naming nit: “private” can be confusing in TS contexts.
It’s valid, but consider isPrivate for clarity.

-  private       Boolean
+  isPrivate     Boolean @map("private")

Note: Keep the column name stable via @Map.

apps/webapp/app/env.server.ts (2)

9-11: Nit: avoid overriding intentional empty-string values

Use a nullish check so empty strings don’t get replaced by "0".

-    if (!obj || !obj.GITHUB_APP_ENABLED) {
+    if (obj == null || obj.GITHUB_APP_ENABLED == null) {
       return { ...obj, GITHUB_APP_ENABLED: "0" };
     }

15-24: Optional: validate shapes more strictly when enabled

If helpful, coerce/validate fields tighter (e.g., numeric App ID, PK format).

-      GITHUB_APP_ID: z.string(),
-      GITHUB_APP_PRIVATE_KEY: z.string(),
+      GITHUB_APP_ID: z.coerce.number().int().positive().transform(String),
+      GITHUB_APP_PRIVATE_KEY: z.string().refine((v) => v.includes("BEGIN") && v.includes("END"), "Expected a PEM-formatted private key"),
apps/webapp/app/services/gitHubSession.server.ts (1)

95-105: Defensive numeric coercion for createdAt

Session values are JSON-serialized; coercing guards against unexpected types and future edits.

-  const expirationTime = createdAt + 60 * 60 * 1000;
+  const createdAtMs = Number(createdAt);
+  if (!Number.isFinite(createdAtMs)) {
+    logger.warn("GitHub App installation session has non-numeric createdAt", { createdAt });
+    return { valid: false, error: "invalid_session_data" };
+  }
+  const expirationTime = createdAtMs + 60 * 60 * 1000;
-    logger.warn("GitHub App installation session expired", {
-      createdAt: new Date(createdAt),
+    logger.warn("GitHub App installation session expired", {
+      createdAt: new Date(createdAtMs),
       now: new Date(),
     });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8c5ebd1 and 6410676.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • apps/webapp/app/env.server.ts (1 hunks)
  • apps/webapp/app/routes/_app.github.callback/route.tsx (1 hunks)
  • apps/webapp/app/routes/_app.github.install/route.tsx (1 hunks)
  • apps/webapp/app/routes/auth.github.ts (1 hunks)
  • apps/webapp/app/services/gitHub.server.ts (1 hunks)
  • apps/webapp/app/services/gitHubSession.server.ts (1 hunks)
  • apps/webapp/package.json (1 hunks)
  • internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql (1 hunks)
  • internal-packages/database/prisma/migrations/migration_lock.toml (1 hunks)
  • internal-packages/database/prisma/schema.prisma (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • internal-packages/database/prisma/migrations/migration_lock.toml
🚧 Files skipped from review as they are similar to previous changes (6)
  • apps/webapp/app/routes/_app.github.install/route.tsx
  • apps/webapp/app/routes/auth.github.ts
  • apps/webapp/package.json
  • apps/webapp/app/services/gitHub.server.ts
  • apps/webapp/app/routes/_app.github.callback/route.tsx
  • internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code
For TypeScript, we usually use types over interfaces
Avoid enums
No default exports, use function declarations

Files:

  • apps/webapp/app/services/gitHubSession.server.ts
  • apps/webapp/app/env.server.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

We use zod a lot in packages/core and in the webapp

Files:

  • apps/webapp/app/services/gitHubSession.server.ts
  • apps/webapp/app/env.server.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

When importing from @trigger.dev/core in the webapp, never import the root package path; always use one of the documented subpath exports from @trigger.dev/core’s package.json

Files:

  • apps/webapp/app/services/gitHubSession.server.ts
  • apps/webapp/app/env.server.ts
{apps/webapp/app/**/*.server.{ts,tsx},apps/webapp/app/routes/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access environment variables only via the env export from app/env.server.ts; do not reference process.env directly

Files:

  • apps/webapp/app/services/gitHubSession.server.ts
  • apps/webapp/app/env.server.ts
apps/webapp/app/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Modules intended for test consumption under apps/webapp/app/**/*.ts must not read environment variables; accept configuration via options instead

Files:

  • apps/webapp/app/services/gitHubSession.server.ts
  • apps/webapp/app/env.server.ts
🧠 Learnings (6)
📓 Common learnings
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/routes/_app.github.callback/route.tsx:33-44
Timestamp: 2025-09-02T11:27:36.319Z
Learning: In the GitHub App installation callback flow in apps/webapp/app/routes/_app.github.callback/route.tsx, the install session cookie is not cleared after use due to interface limitations with redirectWithSuccessMessage/redirectWithErrorMessage not supporting custom headers. The maintainer accepts this design as the 1-hour cookie expiration provides sufficient protection against replay attacks.
📚 Learning: 2025-09-02T11:27:36.319Z
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/routes/_app.github.callback/route.tsx:33-44
Timestamp: 2025-09-02T11:27:36.319Z
Learning: In the GitHub App installation callback flow in apps/webapp/app/routes/_app.github.callback/route.tsx, the install session cookie is not cleared after use due to interface limitations with redirectWithSuccessMessage/redirectWithErrorMessage not supporting custom headers. The maintainer accepts this design as the 1-hour cookie expiration provides sufficient protection against replay attacks.

Applied to files:

  • apps/webapp/app/services/gitHubSession.server.ts
📚 Learning: 2025-09-02T11:18:06.562Z
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/services/gitHubSession.server.ts:31-36
Timestamp: 2025-09-02T11:18:06.562Z
Learning: In the GitHub App installation flow in apps/webapp/app/services/gitHubSession.server.ts, the redirectTo parameter stored in httpOnly session cookies is considered acceptable without additional validation by the maintainer, as the httpOnly cookie provides sufficient security for this use case.

Applied to files:

  • apps/webapp/app/services/gitHubSession.server.ts
📚 Learning: 2025-08-29T10:06:49.283Z
Learnt from: CR
PR: triggerdotdev/trigger.dev#0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-08-29T10:06:49.283Z
Learning: Applies to {apps/webapp/app/**/*.server.{ts,tsx},apps/webapp/app/routes/**/*.ts} : Access environment variables only via the env export from app/env.server.ts; do not reference process.env directly

Applied to files:

  • apps/webapp/app/env.server.ts
📚 Learning: 2025-08-29T10:06:49.283Z
Learnt from: CR
PR: triggerdotdev/trigger.dev#0
File: .cursor/rules/webapp.mdc:0-0
Timestamp: 2025-08-29T10:06:49.283Z
Learning: Applies to {apps/webapp/**/__tests__/**/*.{ts,tsx},apps/webapp/**/*.{test,spec}.{ts,tsx}} : Do not import app/env.server.ts into tests, either directly or indirectly

Applied to files:

  • apps/webapp/app/env.server.ts
📚 Learning: 2025-08-14T18:35:44.370Z
Learnt from: nicktrn
PR: triggerdotdev/trigger.dev#2390
File: apps/webapp/app/env.server.ts:764-765
Timestamp: 2025-08-14T18:35:44.370Z
Learning: The BoolEnv helper in apps/webapp/app/utils/boolEnv.ts uses z.preprocess with inconsistent default value types across the codebase - some usages pass boolean defaults (correct) while others pass string defaults (incorrect), leading to type confusion. The helper should enforce boolean-only defaults or have clearer documentation.

Applied to files:

  • apps/webapp/app/env.server.ts
🧬 Code graph analysis (2)
apps/webapp/app/services/gitHubSession.server.ts (2)
apps/webapp/app/models/message.server.ts (1)
  • createCookieSessionStorage (19-28)
apps/webapp/app/env.server.ts (1)
  • env (1176-1176)
apps/webapp/app/env.server.ts (3)
apps/webapp/app/utils/db.ts (1)
  • isValidDatabaseUrl (1-18)
apps/webapp/app/v3/services/bulk/BulkActionV2.server.ts (1)
  • process (87-357)
apps/webapp/app/utils/boolEnv.ts (1)
  • BoolEnv (12-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
🔇 Additional comments (7)
internal-packages/database/prisma/schema.prisma (3)

154-156: Minor relation formatting change looks fine.
No functional impact spotted.


213-214: Organization → GithubAppInstallation inverse relation: LGTM.
Adds needed navigation from orgs to installations.


2242-2246: Enum for repository selection: LGTM.
Simple, future-proof discriminator.

apps/webapp/app/env.server.ts (1)

1173-1173: Schema composition looks correct

Intersecting with GithubAppEnvSchema cleanly gates the app settings. No issues.

apps/webapp/app/services/gitHubSession.server.ts (3)

6-16: Cookie settings are solid for this flow

httpOnly + 1h max-age + SameSite=Lax + prod-secure is appropriate for the installation round-trip.


21-27: Guard on feature flag is correct

Early exit when the app is disabled prevents accidental entry into the flow.


114-124: Destroy helper is fine

API returns a Set-Cookie header string; returning empty string on missing header is acceptable.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/webapp/app/routes/_app.github.install/route.tsx (1)

1-9: Gracefully handle disabled GitHub App instead of bubbling 500.

Same concern as previously raised; catch the "not enabled" error and show a friendly message.

-import { redirect } from "remix-typedjson";
+import { redirect } from "remix-typedjson";
+import { redirectWithErrorMessage } from "~/models/message.server";
-  const { url, cookieHeader } = await createGitHubAppInstallSession(org.id, redirect_to);
-
-  return redirect(url, {
-    headers: {
-      "Set-Cookie": cookieHeader,
-    },
-  });
+  try {
+    const { url, cookieHeader } = await createGitHubAppInstallSession(org.id, redirectTo);
+    return redirect(url, {
+      headers: { "Set-Cookie": cookieHeader },
+    });
+  } catch (error) {
+    logger.warn("GitHub App install session could not be created", { error, orgId: org.id });
+    return redirectWithErrorMessage(redirectTo, request, "GitHub App is not enabled");
+  }

Also applies to: 45-51

🧹 Nitpick comments (11)
apps/webapp/app/utils.ts (2)

11-11: Fix JSDoc param wording ("to" → "path").

Minor doc mismatch.

- * @param {string} defaultRedirect The redirect to use if the to is unsafe.
+ * @param {string} defaultRedirect The redirect to use if the path is unsafe.

25-31: Remove redundant absolute-URL parse.

Given the earlier startsWith("/") and not startsWith("//") checks, new URL(path) will always throw; this block never returns and adds overhead.

-  try {
-    // should not parse as a full URL
-    new URL(path);
-    return defaultRedirect;
-  } catch {}
apps/webapp/app/routes/_app.github.install/route.tsx (2)

11-16: Make redirect_to optional and sanitize after parse; tighten org_slug.

Avoid hard-failing when redirect_to is absent, and ensure non-empty slug.

-const QuerySchema = z.object({
-  org_slug: z.string(),
-  redirect_to: z.string().refine((value) => value === sanitizeRedirectPath(value), {
-    message: "Invalid redirect path",
-  }),
-});
+const QuerySchema = z.object({
+  org_slug: z.string().min(1),
+  redirect_to: z.string().optional(),
+});
-  const { org_slug, redirect_to } = parsed.data;
+  const { org_slug, redirect_to } = parsed.data;
+  const redirectTo = sanitizeRedirectPath(redirect_to);
-  const { url, cookieHeader } = await createGitHubAppInstallSession(org.id, redirect_to);
+  const { url, cookieHeader } = await createGitHubAppInstallSession(org.id, redirectTo);

22-28: Log structured params, not URLSearchParams instance.

Ensures values are captured predictably in logs.

-    logger.warn("GitHub App installation redirect with invalid params", {
-      searchParams,
-      error: parsed.error,
-    });
+    logger.warn("GitHub App installation redirect with invalid params", {
+      searchParams: Object.fromEntries(searchParams),
+      error: parsed.error,
+    });
internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql (7)

7-7: Constrain targetType to expected values.

Use an enum or CHECK to avoid arbitrary text and data drift (e.g., "User" vs "user").

+-- Constrain installation target type
+CREATE TYPE "public"."GithubInstallationTarget" AS ENUM ('ACCOUNT', 'ORGANIZATION');
+ALTER TABLE "public"."GithubAppInstallation"
+  ALTER COLUMN "targetType" TYPE "public"."GithubInstallationTarget" USING
+    (CASE UPPER("targetType") WHEN 'ACCOUNT' THEN 'ACCOUNT'::"public"."GithubInstallationTarget"
+                              WHEN 'ORGANIZATION' THEN 'ORGANIZATION'::"public"."GithubInstallationTarget"
+                              ELSE 'ORGANIZATION'::"public"."GithubInstallationTarget" END);

9-9: Normalize permissions JSONB (default and not null).

Prevents tri-state semantics and simplifies queries.

+ALTER TABLE "public"."GithubAppInstallation"
+  ALTER COLUMN "permissions" SET DEFAULT '{}'::jsonb;
+UPDATE "public"."GithubAppInstallation" SET "permissions" = '{}'::jsonb WHERE "permissions" IS NULL;
+ALTER TABLE "public"."GithubAppInstallation"
+  ALTER COLUMN "permissions" SET NOT NULL;

38-38: Add partial index for active installations lookups.

Likely hot path: "by organization where not deleted/suspended".

+-- Speed up queries for active installations per org
+CREATE INDEX "GithubAppInstallation_org_active_idx"
+  ON "public"."GithubAppInstallation"("organizationId")
+  WHERE "deletedAt" IS NULL AND "suspendedAt" IS NULL;

21-31: Add lookup indexes for repositories by githubId/fullName.

Octokit payloads often use repo ID or slug; dedicated indexes avoid seq scans.

+-- Common lookup patterns
+CREATE INDEX "GithubRepository_githubId_idx" ON "public"."GithubRepository"("githubId");
+CREATE INDEX "GithubRepository_fullName_idx" ON "public"."GithubRepository"("fullName");

42-42: Confirm uniqueness scope for repositories across installations.

If a repo can’t belong to two installations simultaneously, consider enforcing uniqueness on githubId (optionally partial on active rows). If cross-install duplicates are expected (e.g., transfer history), keep as-is.

Option if needed:

+-- Enforce one active row per GitHub repo id
+CREATE UNIQUE INDEX "GithubRepository_githubId_unique_active"
+  ON "public"."GithubRepository"("githubId")
+  -- Add "deletedAt" column first if you choose soft-deletes; otherwise omit WHERE.
+  -- WHERE "deletedAt" IS NULL
+;

15-16: updatedAt has no DB default/trigger.

Assuming Prisma manages it (@updatedat). If other writers (raw SQL, jobs) touch these tables, consider a DB trigger to avoid nulls/stale values.

+-- Optional: keep updatedAt in sync at the DB layer
+CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$
+BEGIN NEW."updatedAt" = CURRENT_TIMESTAMP; RETURN NEW; END;
+$$ LANGUAGE plpgsql;
+CREATE TRIGGER set_updated_at_gai BEFORE UPDATE ON "public"."GithubAppInstallation"
+FOR EACH ROW EXECUTE PROCEDURE set_updated_at();
+CREATE TRIGGER set_updated_at_gr BEFORE UPDATE ON "public"."GithubRepository"
+FOR EACH ROW EXECUTE PROCEDURE set_updated_at();

Also applies to: 30-31


1-46: Nit: Consistent “GitHub” casing in identifiers.

Consider standardizing to “GitHub” across enum/table/index names for polish; no functional impact.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6410676 and 441d01a.

📒 Files selected for processing (6)
  • apps/webapp/app/routes/_app.github.callback/route.tsx (1 hunks)
  • apps/webapp/app/routes/_app.github.install/route.tsx (1 hunks)
  • apps/webapp/app/routes/auth.github.callback.tsx (1 hunks)
  • apps/webapp/app/utils.ts (1 hunks)
  • internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql (1 hunks)
  • internal-packages/database/prisma/schema.prisma (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/webapp/app/routes/_app.github.callback/route.tsx
  • internal-packages/database/prisma/schema.prisma
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code
For TypeScript, we usually use types over interfaces
Avoid enums
No default exports, use function declarations

Files:

  • apps/webapp/app/routes/auth.github.callback.tsx
  • apps/webapp/app/utils.ts
  • apps/webapp/app/routes/_app.github.install/route.tsx
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

We use zod a lot in packages/core and in the webapp

Files:

  • apps/webapp/app/routes/auth.github.callback.tsx
  • apps/webapp/app/utils.ts
  • apps/webapp/app/routes/_app.github.install/route.tsx
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

When importing from @trigger.dev/core in the webapp, never import the root package path; always use one of the documented subpath exports from @trigger.dev/core’s package.json

Files:

  • apps/webapp/app/routes/auth.github.callback.tsx
  • apps/webapp/app/utils.ts
  • apps/webapp/app/routes/_app.github.install/route.tsx
apps/webapp/app/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Modules intended for test consumption under apps/webapp/app/**/*.ts must not read environment variables; accept configuration via options instead

Files:

  • apps/webapp/app/utils.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/routes/_app.github.callback/route.tsx:33-44
Timestamp: 2025-09-02T11:27:36.319Z
Learning: In the GitHub App installation callback flow in apps/webapp/app/routes/_app.github.callback/route.tsx, the install session cookie is not cleared after use due to interface limitations with redirectWithSuccessMessage/redirectWithErrorMessage not supporting custom headers. The maintainer accepts this design as the 1-hour cookie expiration provides sufficient protection against replay attacks.
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/services/gitHubSession.server.ts:31-36
Timestamp: 2025-09-02T11:18:06.562Z
Learning: In the GitHub App installation flow in apps/webapp/app/services/gitHubSession.server.ts, the redirectTo parameter stored in httpOnly session cookies is considered acceptable without additional validation by the maintainer, as the httpOnly cookie provides sufficient security for this use case.
📚 Learning: 2025-09-02T11:18:06.562Z
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/services/gitHubSession.server.ts:31-36
Timestamp: 2025-09-02T11:18:06.562Z
Learning: In the GitHub App installation flow in apps/webapp/app/services/gitHubSession.server.ts, the redirectTo parameter stored in httpOnly session cookies is considered acceptable without additional validation by the maintainer, as the httpOnly cookie provides sufficient security for this use case.

Applied to files:

  • apps/webapp/app/routes/auth.github.callback.tsx
  • apps/webapp/app/routes/_app.github.install/route.tsx
📚 Learning: 2025-09-02T11:27:36.319Z
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/routes/_app.github.callback/route.tsx:33-44
Timestamp: 2025-09-02T11:27:36.319Z
Learning: In the GitHub App installation callback flow in apps/webapp/app/routes/_app.github.callback/route.tsx, the install session cookie is not cleared after use due to interface limitations with redirectWithSuccessMessage/redirectWithErrorMessage not supporting custom headers. The maintainer accepts this design as the 1-hour cookie expiration provides sufficient protection against replay attacks.

Applied to files:

  • apps/webapp/app/routes/auth.github.callback.tsx
  • apps/webapp/app/routes/_app.github.install/route.tsx
🧬 Code graph analysis (2)
apps/webapp/app/routes/auth.github.callback.tsx (2)
apps/webapp/app/routes/auth.github.ts (1)
  • redirectCookie (29-32)
apps/webapp/app/utils.ts (1)
  • sanitizeRedirectPath (13-42)
apps/webapp/app/routes/_app.github.install/route.tsx (5)
apps/webapp/app/utils.ts (1)
  • sanitizeRedirectPath (13-42)
apps/webapp/app/services/session.server.ts (1)
  • requireUser (39-62)
apps/webapp/app/db.server.ts (1)
  • $replica (103-106)
apps/webapp/app/utils/pathBuilder.ts (1)
  • newOrganizationPath (96-98)
apps/webapp/app/services/gitHubSession.server.ts (1)
  • createGitHubAppInstallSession (21-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (3)
apps/webapp/app/utils.ts (1)

13-24: Solid redirect hardening; clear allowlist semantics.

Strict absolute-path checks and defaulting behavior meaningfully reduce open-redirect risk. Nice improvement.

apps/webapp/app/routes/auth.github.callback.tsx (1)

13-14: Good call: sanitize cookie-provided redirect before use.

This closes an open-redirect vector without altering the existing flow.

internal-packages/database/prisma/migrations/20250829124138_add_gh_installation_schema/migration.sql (1)

44-44: Verify FK delete behavior for Organization.

ON DELETE RESTRICT blocks org deletion if any installations exist. If product semantics prefer cascading cleanup, switch to CASCADE; otherwise, keep RESTRICT.

@myftija myftija force-pushed the gh-app-installation-flow branch from 441d01a to d07e605 Compare September 2, 2025 13:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/webapp/app/services/gitHub.server.ts (2)

6-15: GitHub App instantiation LGTM.

Gated by env and using env import correctly. appId as string is supported by Octokit.


59-91: Prefer full pagination; if intentionally capped, persist truncation metadata.

Fetching only 3 pages can silently under-populate the cache for large installations. Use octokit.paginate to get all repos.

-  const iterator = octokit.paginate.iterator(octokit.rest.apps.listReposAccessibleToInstallation, {
-    installation_id: installationId,
-    per_page: 100,
-  });
-  const allRepos = [];
-  const maxPages = 3;
-  let pageCount = 0;
-  for await (const { data } of iterator) {
-    pageCount++;
-    allRepos.push(...data.repositories);
-    if (maxPages && pageCount >= maxPages) { /* warn */ break; }
-  }
-  return allRepos.map(/* ... */);
+  const repos = await octokit.paginate(
+    octokit.rest.apps.listReposAccessibleToInstallation,
+    { installation_id: installationId, per_page: 100 },
+    (response) => response.data.repositories
+  );
+  return repos.map(/* ...including BigInt conversion... */);

If you keep the cap, set GithubAppInstallation.reposSyncTruncated and reposSyncedAt as suggested in the schema.

I can provide a differential updater to avoid delete+recreate for repos if you prefer.

🧹 Nitpick comments (5)
internal-packages/database/prisma/schema.prisma (3)

2259-2261: Cascade delete GithubAppInstallation when an Organization is deleted.

Most relations to Organization use onDelete: Cascade. Add it here for consistency and to prevent orphaned installations.

-  organization   Organization @relation(fields: [organizationId], references: [id])
+  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)

2248-2270: Track repo sync state to support partial/truncated caching.

Given the fetch currently caps pages in code, add lightweight fields to indicate staleness.

 model GithubAppInstallation {
   id String @id @default(cuid())
   appInstallationId   BigInt                    @unique
   targetId            BigInt
   targetType          String
   accountHandle       String
   permissions         Json?
   repositorySelection GithubRepositorySelection
   installedBy         String?
+  reposSyncedAt       DateTime?
+  reposSyncTruncated  Boolean @default(false)

   organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
   organizationId String

2272-2290: Add helpful indexes for common lookups.

If you’ll frequently resolve repos by full name or by GitHub ID independently, add indexes to avoid seq scans.

 model GithubRepository {
   id String @id @default(cuid())
   githubId      BigInt
   name          String
   fullName      String
   htmlUrl       String
   private       Boolean
   defaultBranch String
   installation   GithubAppInstallation @relation(fields: [installationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
   installationId String
   createdAt DateTime @default(now())
   updatedAt DateTime @updatedAt

   @@unique([installationId, githubId])
   @@index([installationId])
+  @@index([installationId, fullName])
+  @@index([githubId])
 }
internal-packages/database/prisma/migrations/20250902133449_add_gh_installation_schema/migration.sql (2)

44-46: Use ON DELETE CASCADE for org → installation to prevent orphans.

Matches the suggested Prisma relation change; update and re-generate migration.

-ALTER TABLE "public"."GithubAppInstallation" ADD CONSTRAINT "GithubAppInstallation_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+ALTER TABLE "public"."GithubAppInstallation" ADD CONSTRAINT "GithubAppInstallation_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "public"."Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;

40-43: Add indexes for common repo lookups.

If queries hit by (installationId, fullName) or githubId, indexing here will help.

 CREATE INDEX "GithubRepository_installationId_idx" ON "public"."GithubRepository"("installationId");
+CREATE INDEX "GithubRepository_installationId_fullName_idx" ON "public"."GithubRepository"("installationId","fullName");
+CREATE INDEX "GithubRepository_githubId_idx" ON "public"."GithubRepository"("githubId");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 441d01a and d07e605.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • apps/webapp/app/env.server.ts (1 hunks)
  • apps/webapp/app/routes/_app.github.callback/route.tsx (1 hunks)
  • apps/webapp/app/routes/_app.github.install/route.tsx (1 hunks)
  • apps/webapp/app/routes/auth.github.callback.tsx (1 hunks)
  • apps/webapp/app/routes/auth.github.ts (1 hunks)
  • apps/webapp/app/services/gitHub.server.ts (1 hunks)
  • apps/webapp/app/services/gitHubSession.server.ts (1 hunks)
  • apps/webapp/app/utils.ts (1 hunks)
  • apps/webapp/package.json (1 hunks)
  • internal-packages/database/prisma/migrations/20250902133449_add_gh_installation_schema/migration.sql (1 hunks)
  • internal-packages/database/prisma/migrations/migration_lock.toml (1 hunks)
  • internal-packages/database/prisma/schema.prisma (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • internal-packages/database/prisma/migrations/migration_lock.toml
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/webapp/app/routes/_app.github.callback/route.tsx
  • apps/webapp/package.json
  • apps/webapp/app/routes/auth.github.callback.tsx
  • apps/webapp/app/services/gitHubSession.server.ts
  • apps/webapp/app/utils.ts
  • apps/webapp/app/routes/_app.github.install/route.tsx
  • apps/webapp/app/routes/auth.github.ts
  • apps/webapp/app/env.server.ts
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code
For TypeScript, we usually use types over interfaces
Avoid enums
No default exports, use function declarations

Files:

  • apps/webapp/app/services/gitHub.server.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

We use zod a lot in packages/core and in the webapp

Files:

  • apps/webapp/app/services/gitHub.server.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

When importing from @trigger.dev/core in the webapp, never import the root package path; always use one of the documented subpath exports from @trigger.dev/core’s package.json

Files:

  • apps/webapp/app/services/gitHub.server.ts
{apps/webapp/app/**/*.server.{ts,tsx},apps/webapp/app/routes/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access environment variables only via the env export from app/env.server.ts; do not reference process.env directly

Files:

  • apps/webapp/app/services/gitHub.server.ts
apps/webapp/app/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Modules intended for test consumption under apps/webapp/app/**/*.ts must not read environment variables; accept configuration via options instead

Files:

  • apps/webapp/app/services/gitHub.server.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/routes/_app.github.callback/route.tsx:33-44
Timestamp: 2025-09-02T11:27:36.319Z
Learning: In the GitHub App installation callback flow in apps/webapp/app/routes/_app.github.callback/route.tsx, the install session cookie is not cleared after use due to interface limitations with redirectWithSuccessMessage/redirectWithErrorMessage not supporting custom headers. The maintainer accepts this design as the 1-hour cookie expiration provides sufficient protection against replay attacks.
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/services/gitHubSession.server.ts:31-36
Timestamp: 2025-09-02T11:18:06.562Z
Learning: In the GitHub App installation flow in apps/webapp/app/services/gitHubSession.server.ts, the redirectTo parameter stored in httpOnly session cookies is considered acceptable without additional validation by the maintainer, as the httpOnly cookie provides sufficient security for this use case.
📚 Learning: 2025-09-02T10:59:03.798Z
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/services/gitHub.server.ts:5-14
Timestamp: 2025-09-02T10:59:03.798Z
Learning: Octokit App constructor's appId parameter accepts both number and string types (number | string), so explicit conversion from string to number using Number() is not required when passing environment variables.

Applied to files:

  • apps/webapp/app/services/gitHub.server.ts
🧬 Code graph analysis (1)
apps/webapp/app/services/gitHub.server.ts (1)
apps/webapp/app/env.server.ts (1)
  • env (1176-1176)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (23)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
internal-packages/database/prisma/schema.prisma (1)

2243-2246: Enum looks good.

Enum values match GitHub’s repository selection modes and align with code usage.

apps/webapp/app/services/gitHub.server.ts (1)

20-57: Unable to locate the GithubAppInstallationRepository model in the Prisma schema – repositories are stored in the GithubRepository model instead. The field in that model is named githubId, not githubId on a separate installation-repository table. Ensure you convert repositories entries’ id property from GitHub’s API to JS BigInt and assign it to the githubId field in each created GithubRepository.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/webapp/app/services/gitHub.server.ts (1)

127-134: Convert GitHub ID to BigInt to match Prisma schema
In apps/webapp/app/services/gitHub.server.ts (lines 127–134), wrap repo.id with BigInt(String(...)) to align with the BigInt field in your Prisma schema and avoid precision/type errors:

   return allRepos.map((repo) => ({
-    githubId: repo.id,
+    githubId: BigInt(String(repo.id)),
     name: repo.name,
     fullName: repo.full_name,
     htmlUrl: repo.html_url,
     private: repo.private,
     defaultBranch: repo.default_branch,
   }));
🧹 Nitpick comments (4)
apps/webapp/app/services/gitHub.server.ts (4)

72-75: Prefer findUnique over findFirst for unique key lookups

Slightly safer and communicates intent if appInstallationId is unique.

-  const existingInstallation = await prisma.githubAppInstallation.findFirst({
-    where: { appInstallationId: installationId },
-  });
+  const existingInstallation = await prisma.githubAppInstallation.findUnique({
+    where: { appInstallationId: installationId },
+  });

59-61: Fix comment: this function updates, it doesn’t link

Minor doc correction.

- * Links a GitHub App installation to a Trigger organization
+ * Updates a GitHub App installation’s metadata

82-100: Avoid writing suspendedAt to the same value (redundant/no-op)

Either omit the field or set from the API if available (installation.suspended_at).

       permissions: installation.permissions,
-      suspendedAt: existingInstallation?.suspendedAt,
       repositorySelection,

If you want to sync from API:

// suspendedAt: installation.suspended_at ?? existingInstallation?.suspendedAt,

103-125: Don’t truncate repo pagination; fetch all or persist a “truncated” marker

Capping at 3 pages can leave the repo cache incomplete and cause downstream mismatches. Either fetch all pages or persist a truncated flag and queue a background completion.

-  const iterator = octokit.paginate.iterator(octokit.rest.apps.listReposAccessibleToInstallation, {
-    installation_id: installationId,
-    per_page: 100,
-  });
-
-  const allRepos = [];
-  const maxPages = 3;
-  let pageCount = 0;
-
-  for await (const { data } of iterator) {
-    pageCount++;
-    allRepos.push(...data);
-
-    if (maxPages && pageCount >= maxPages) {
-      logger.warn("GitHub installation repository fetch truncated", {
-        installationId,
-        maxPages,
-        totalReposFetched: allRepos.length,
-      });
-      break;
-    }
-  }
+  const allRepos = await octokit.paginate(
+    octokit.rest.apps.listReposAccessibleToInstallation,
+    { installation_id: installationId, per_page: 100 }
+  );

Note: Your current use of paginate.iterator and spreading data is correct; this change only simplifies and removes truncation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d07e605 and 9a7eb49.

📒 Files selected for processing (2)
  • apps/webapp/app/routes/_app.github.callback/route.tsx (1 hunks)
  • apps/webapp/app/services/gitHub.server.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/webapp/app/routes/_app.github.callback/route.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Always prefer using isomorphic code like fetch, ReadableStream, etc. instead of Node.js specific code
For TypeScript, we usually use types over interfaces
Avoid enums
No default exports, use function declarations

Files:

  • apps/webapp/app/services/gitHub.server.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

We use zod a lot in packages/core and in the webapp

Files:

  • apps/webapp/app/services/gitHub.server.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

When importing from @trigger.dev/core in the webapp, never import the root package path; always use one of the documented subpath exports from @trigger.dev/core’s package.json

Files:

  • apps/webapp/app/services/gitHub.server.ts
{apps/webapp/app/**/*.server.{ts,tsx},apps/webapp/app/routes/**/*.ts}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access environment variables only via the env export from app/env.server.ts; do not reference process.env directly

Files:

  • apps/webapp/app/services/gitHub.server.ts
apps/webapp/app/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Modules intended for test consumption under apps/webapp/app/**/*.ts must not read environment variables; accept configuration via options instead

Files:

  • apps/webapp/app/services/gitHub.server.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/routes/_app.github.callback/route.tsx:33-44
Timestamp: 2025-09-02T11:27:36.319Z
Learning: In the GitHub App installation callback flow in apps/webapp/app/routes/_app.github.callback/route.tsx, the install session cookie is not cleared after use due to interface limitations with redirectWithSuccessMessage/redirectWithErrorMessage not supporting custom headers. The maintainer accepts this design as the 1-hour cookie expiration provides sufficient protection against replay attacks.
📚 Learning: 2025-09-02T10:59:03.798Z
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/services/gitHub.server.ts:5-14
Timestamp: 2025-09-02T10:59:03.798Z
Learning: Octokit App constructor's appId parameter accepts both number and string types (number | string), so explicit conversion from string to number using Number() is not required when passing environment variables.

Applied to files:

  • apps/webapp/app/services/gitHub.server.ts
📚 Learning: 2025-09-02T14:00:49.205Z
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/services/gitHub.server.ts:59-91
Timestamp: 2025-09-02T14:00:49.205Z
Learning: In Octokit's paginate plugin, the `octokit.paginate.iterator` method normalizes responses from endpoints like `listReposAccessibleToInstallation`. Even though the raw GitHub API returns `{ total_count, repositories, repository_selection }`, the paginate plugin extracts and returns just the repositories array directly in `response.data`. Therefore, spreading `...data` directly is correct, not `...data.repositories`.

Applied to files:

  • apps/webapp/app/services/gitHub.server.ts
📚 Learning: 2025-09-02T11:27:36.319Z
Learnt from: myftija
PR: triggerdotdev/trigger.dev#2463
File: apps/webapp/app/routes/_app.github.callback/route.tsx:33-44
Timestamp: 2025-09-02T11:27:36.319Z
Learning: In the GitHub App installation callback flow in apps/webapp/app/routes/_app.github.callback/route.tsx, the install session cookie is not cleared after use due to interface limitations with redirectWithSuccessMessage/redirectWithErrorMessage not supporting custom headers. The maintainer accepts this design as the 1-hour cookie expiration provides sufficient protection against replay attacks.

Applied to files:

  • apps/webapp/app/services/gitHub.server.ts
🧬 Code graph analysis (1)
apps/webapp/app/services/gitHub.server.ts (1)
apps/webapp/app/env.server.ts (1)
  • env (1176-1176)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (23)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (1)
apps/webapp/app/services/gitHub.server.ts (1)

6-15: GitHub App initialization looks good

  • Correct env gating via env import (not process.env).
  • Passing appId as string is fine; Octokit accepts number | string.

@myftija myftija merged commit 436d951 into main Sep 2, 2025
31 checks passed
@myftija myftija deleted the gh-app-installation-flow branch September 2, 2025 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants