Skip to content

Improve-submission-github-calls#615

Merged
jon-bell merged 7 commits intostagingfrom
improve-submission-github-calls
Mar 1, 2026
Merged

Improve-submission-github-calls#615
jon-bell merged 7 commits intostagingfrom
improve-submission-github-calls

Conversation

@jon-bell
Copy link
Contributor

@jon-bell jon-bell commented Feb 19, 2026

  • Remove all createCheckRun/updateCheckRun API calls from webhook, create-submission, and submit-feedback
  • Make repository_check_runs.check_run_id nullable, add unique on (repository_id, sha)
  • Use decoded.actor (GitHub username) for role lookup instead of profile_id
  • Use repository_check_runs.created_at for late detection instead of current time
  • Drop fallback path when check run is missing; fail fast with UserVisibleError

Summary by CodeRabbit

  • New Features

    • Org/method-level circuit breaker pre-checks to fail fast.
    • Enhanced rate-limit detection with installation-aware diagnostics and fingerprinting to reduce noisy retries.
  • Bug Fixes

    • Consistent DB-driven check-run state to avoid conflicting external updates.
    • Clearer user-facing errors and guarded flows for late/empty/maxed submissions and webhook processing.
  • Chores

    • DB migration: allow nullable check-run IDs, dedupe rows, and strengthen idempotency.

…or late detection

- Remove all createCheckRun/updateCheckRun API calls from webhook, create-submission, and submit-feedback
- Make repository_check_runs.check_run_id nullable, add unique on (repository_id, sha)
- Use decoded.actor (GitHub username) for role lookup instead of profile_id
- Use repository_check_runs.created_at for late detection instead of current time
- Drop fallback path when check run is missing; fail fast with UserVisibleError
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

Walkthrough

Adds org/method circuit-breaker checks to GitHub webhook and check_run flows to fast-fail when circuits are open, classifies GitHub rate-limit errors (including installationId), makes repository_check_runs.check_run_id nullable by migration/types, and shifts several autograder handlers from direct GitHub updates to DB-driven state and guarded workflows. (39 words)

Changes

Cohort / File(s) Summary
Webhook + Circuit Breaker
supabase/functions/github-repo-webhook/index.ts
Adds checkCircuitBreakerOpen(...), extracts org from repo name, gates push and check_run trigger flows on circuit state, annotates Sentry with circuit info, and removes prior partial/fallback check_run update/completion paths.
Autograder: create-submission
supabase/functions/autograder-create-submission/index.ts
Removes createCheckRun, getOctoKit, updateCheckRun imports; relies more on DB state for initial check runs; extends rate-limit detection (installationId handling) and simplifies error/late-detection and fallback GitHub interactions.
Autograder: submit-feedback
supabase/functions/autograder-submit-feedback/index.ts
Introduces detectRateLimitType(...) usage, removes external updateCheckRun calls, replaces GitHub completion updates with DB status updates, and fingerprints rate-limit events in Sentry.
Async worker rate-limit helper
supabase/functions/github-async-worker/index.ts
Expands detectRateLimitType to return installationId, handles Octokit AggregateError cases, and applies installation-aware fingerprinting/context for rate-limit events.
Shared types
supabase/functions/_shared/SupabaseTypes.d.ts
Makes repository_check_runs.check_run_id nullable in Row and optional/nullable in Insert to match migration.
Database Migration
supabase/migrations/20260218120000_make_check_run_id_nullable.sql
Removes old unique constraint, dedupes (repository_id, sha) rows, adds unique constraint on (repository_id, sha), makes check_run_id nullable and default NULL.
GitHub wrapper / imports
supabase/functions/_shared/GitHubWrapper.ts, various callers
Removes/adjusts createCheckRun/updateCheckRun usages from callers; propagates rate-limit detection and installationId context across handlers.

Sequence Diagram

sequenceDiagram
    participant Webhook as GitHub Webhook
    participant Handler as Push Handler
    participant CB as Circuit Breaker RPC
    participant DB as Database
    participant Sentry as Sentry
    participant GitHub as GitHub API

    Webhook->>Handler: POST push event / check_run trigger
    Handler->>Handler: extract org from repo
    Handler->>CB: checkCircuitBreakerOpen(adminSupabase, org, method)
    CB->>DB: RPC get_github_circuit_breaker_statuses(org, method)
    DB-->>CB: circuit records (isOpen?, open_until, scope, reason)
    CB-->>Handler: {isOpen, reason, openUntil, circuitScope}
    alt Circuit Open
        Handler->>Sentry: set tags (org, method, circuitScope, openUntil)
        Handler-->>Webhook: fail fast with circuit details
    else Circuit Closed
        Handler->>GitHub: triggerWorkflow / other API calls
        GitHub-->>Handler: success / rate-limit error
        alt Rate-limit error
            Handler->>Sentry: fingerprint ["github-rate-limit", type, method], add installationId in context
            Handler->>DB: only update local DB state (no mid-flight check_run completions)
            Handler-->>Webhook: surface rate-limit error
        else Success
            Handler->>DB: insert/update repository_check_runs, submissions
            DB-->>Handler: ack
            Handler-->>Webhook: success
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

backend, database

Suggested reviewers

  • ironm00n

Poem

A watcher on the gate stands clear and bright,
It reads the limits, hides the noisy fight,
DB keeps the ledger, Sentry holds the clue,
We fail fast now, resume when skies are blue. ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Improve-submission-github-calls' is vague and does not clearly convey the main changes: removing GitHub API calls, making check_run_id nullable, and refactoring submission handling logic. Consider a more descriptive title like 'Refactor GitHub API calls and make check_run_id nullable' or 'Remove check_run GitHub API calls and refactor submission handling' to better communicate the primary changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improve-submission-github-calls

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

Comment @coderabbitai help to get the list of available commands and usage tips.

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.

🧹 Nitpick comments (1)
supabase/functions/github-repo-webhook/index.ts (1)

287-301: Circuit breaker check runs N times inside the loop — move it before the for block.

checkCircuitBreakerOpen is called once per commit, issuing up to two RPC round-trips on each iteration. Since circuit state is stable for the lifetime of the request, all iterations after the first redundantly re-query the same result. Move the check (and its throw) to before the loop.

♻️ Proposed fix
+  // Check circuit breaker once before processing commits - fail fast if rate limited
+  const circuitStatus = await checkCircuitBreakerOpen(adminSupabase, org, "create_repo", scope);
+  if (circuitStatus.isOpen) {
+    const openUntil = circuitStatus.openUntil ? new Date(circuitStatus.openUntil).toLocaleString() : "unknown";
+    const scopeInfo = circuitStatus.circuitScope === "org_method" ? ` (method: create_repo)` : "";
+    throw new Error(
+      `Circuit breaker open for org ${org}${scopeInfo}: GitHub API operations temporarily unavailable. ` +
+        `Reason: ${circuitStatus.reason || "Rate limit or error threshold exceeded"}. ` +
+        `Open until: ${openUntil}`
+    );
+  }
+
   for (const commit of payload.commits) {
     maybeCrash("push.student.for_each_commit.before_lookup");
-
-    // Check circuit breaker before making GitHub API calls - fail fast if rate limited
-    // Check both org-level and create_repo method-specific circuit breakers
-    const circuitStatus = await checkCircuitBreakerOpen(adminSupabase, org, "create_repo", scope);
-    if (circuitStatus.isOpen) {
-      const openUntil = circuitStatus.openUntil ? new Date(circuitStatus.openUntil).toLocaleString() : "unknown";
-      const scopeInfo = circuitStatus.circuitScope === "org_method" ? ` (method: create_repo)` : "";
-      throw new Error(
-        `Circuit breaker open for org ${org}${scopeInfo}: GitHub API operations temporarily unavailable. ` +
-          `Reason: ${circuitStatus.reason || "Rate limit or error threshold exceeded"}. ` +
-          `Open until: ${openUntil}`
-      );
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/github-repo-webhook/index.ts` around lines 287 - 301, The
circuit breaker check using checkCircuitBreakerOpen(adminSupabase, org,
"create_repo", scope) is being executed inside the for (const commit of
payload.commits) loop causing redundant RPCs; move the entire check (including
the isOpen handling and the throw that builds openUntil/scopeInfo) to just
before the for-loop so the circuit state is fetched once per request and the
loop only processes commits; keep the same variables (adminSupabase, org, scope)
and preserve the exact error message construction when extracting
circuitStatus.isOpen, circuitStatus.openUntil and circuitStatus.reason.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@supabase/functions/github-repo-webhook/index.ts`:
- Around line 287-301: The circuit breaker check using
checkCircuitBreakerOpen(adminSupabase, org, "create_repo", scope) is being
executed inside the for (const commit of payload.commits) loop causing redundant
RPCs; move the entire check (including the isOpen handling and the throw that
builds openUntil/scopeInfo) to just before the for-loop so the circuit state is
fetched once per request and the loop only processes commits; keep the same
variables (adminSupabase, org, scope) and preserve the exact error message
construction when extracting circuitStatus.isOpen, circuitStatus.openUntil and
circuitStatus.reason.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 19, 2026
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

Caution

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

⚠️ Outside diff range comments (2)
supabase/functions/_shared/SupabaseTypes.d.ts (1)

6156-6205: ⚠️ Potential issue | 🟡 Minor

Add | null to repository_check_runs.Update.check_run_id to match DB nullability.

Row and Insert permit check_run_id: number | null, but Update restricts it to number, preventing callers from clearing the field. This type inconsistency forces unnecessary casts or workarounds.

Fix
         Update: {
           assignment_group_id?: number | null;
           auto_promote_result?: boolean | null;
-          check_run_id?: number;
+          check_run_id?: number | null;
           class_id?: number;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_shared/SupabaseTypes.d.ts` around lines 6156 - 6205, The
Update type for repository_check_runs currently declares check_run_id as number
which contradicts Row and Insert nullability; update
repository_check_runs.Update.check_run_id to be number | null so callers can
clear the field (mirror Row/Insert), and ensure any generated types or
converters that produce Update reflect this nullable change.
supabase/functions/autograder-submit-feedback/index.ts (1)

766-780: ⚠️ Potential issue | 🟡 Minor

Missing error handling on the DB update for check run completion.

The await adminSupabase.from("repository_check_runs").update(...) at lines 773–778 discards its result without checking for errors. Every other DB operation in this function checks for errors and either throws or logs to Sentry. If this update fails silently, the check run will appear stuck in a non-completed state with no indication of what went wrong.

Proposed fix
-        await adminSupabase
+        const { error: checkRunUpdateError } = await adminSupabase
           .from("repository_check_runs")
           .update({
             status: newStatus
           })
           .eq("id", checkRun.id);
+        if (checkRunUpdateError) {
+          console.error(checkRunUpdateError);
+          Sentry.captureException(checkRunUpdateError, scope);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/autograder-submit-feedback/index.ts` around lines 766 -
780, The DB update that sets repository_check_runs.status to newStatus (inside
the block gated by submission_id && !isRegressionRerun && !isE2ERun when
checkRun is present) currently awaits
adminSupabase.from("repository_check_runs").update(...).eq("id", checkRun.id)
but ignores the result; modify this to capture the response, check for
response.error (or !response.data), and handle failures consistently with other
DB calls in this file—either throw an Error or log to Sentry (and include
context like checkRun.id, submission_id and newStatus) so failures aren’t
silent.
🧹 Nitpick comments (1)
supabase/functions/github-repo-webhook/index.ts (1)

282-294: Circuit breaker check is inside the per-commit loop — consider hoisting it.

checkCircuitBreakerOpen makes 1–2 RPC calls each invocation. Placing it inside the for (const commit of payload.commits) loop means those DB round-trips run for every commit in the push. A push with 20 commits will fire 20–40 extra RPCs even though the circuit state is unlikely to change within a single webhook invocation.

Moving the check before the loop (right after line 278) retains the same protection without the repeated overhead.

Proposed fix
   // Extract org for circuit breaker check
   const org = repoName.split("/")[0];

+  // Check circuit breaker before making GitHub API calls - fail fast if rate limited
+  const circuitStatus = await checkCircuitBreakerOpen(adminSupabase, org, "create_repo", scope);
+  if (circuitStatus.isOpen) {
+    const openUntil = circuitStatus.openUntil ? new Date(circuitStatus.openUntil).toLocaleString() : "unknown";
+    const scopeInfo = circuitStatus.circuitScope === "org_method" ? ` (method: create_repo)` : "";
+    throw new Error(
+      `Circuit breaker open for org ${org}${scopeInfo}: GitHub API operations temporarily unavailable. ` +
+        `Reason: ${circuitStatus.reason || "Rate limit or error threshold exceeded"}. ` +
+        `Open until: ${openUntil}`
+    );
+  }
+
   for (const commit of payload.commits) {
     maybeCrash("push.student.for_each_commit.before_lookup");
-
-    // Check circuit breaker before making GitHub API calls - fail fast if rate limited
-    // Check both org-level and create_repo method-specific circuit breakers
-    const circuitStatus = await checkCircuitBreakerOpen(adminSupabase, org, "create_repo", scope);
-    if (circuitStatus.isOpen) {
-      const openUntil = circuitStatus.openUntil ? new Date(circuitStatus.openUntil).toLocaleString() : "unknown";
-      const scopeInfo = circuitStatus.circuitScope === "org_method" ? ` (method: create_repo)` : "";
-      throw new Error(
-        `Circuit breaker open for org ${org}${scopeInfo}: GitHub API operations temporarily unavailable. ` +
-          `Reason: ${circuitStatus.reason || "Rate limit or error threshold exceeded"}. ` +
-          `Open until: ${openUntil}`
-      );
-    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/github-repo-webhook/index.ts` around lines 282 - 294, The
circuit breaker check (checkCircuitBreakerOpen) is being executed inside the
per-commit loop (for (const commit of payload.commits)), causing redundant RPCs;
move the call to run once before iterating commits—immediately after you set
org/scope (right after where payload is parsed around line ~278) and keep the
existing error handling that throws if circuitStatus.isOpen; remove the check
from inside the loop so the loop only processes commits and relies on the single
pre-loop circuitStatus result (use the same variables adminSupabase, org, scope,
and circuitStatus).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@supabase/functions/autograder-create-submission/index.ts`:
- Around line 604-636: The code performs user role DB lookups using
initialCheckRun/classId before verifying the upstream query result; move the
check for checkRunError and the guard that throws when initialCheckRun is null
to occur immediately after fetching initialCheckRun/checkRunError and before
computing classId or querying user_roles; then only compute classId (use
initialCheckRun?.class_id) and call
adminSupabase.from("users")/.from("user_roles") when there is no checkRunError
and initialCheckRun is present, assigning userRoles otherwise leave undefined —
update the block around initialCheckRun, checkRunError, classId, userRoles so
unnecessary DB calls are avoided and errors are handled first.

---

Outside diff comments:
In `@supabase/functions/_shared/SupabaseTypes.d.ts`:
- Around line 6156-6205: The Update type for repository_check_runs currently
declares check_run_id as number which contradicts Row and Insert nullability;
update repository_check_runs.Update.check_run_id to be number | null so callers
can clear the field (mirror Row/Insert), and ensure any generated types or
converters that produce Update reflect this nullable change.

In `@supabase/functions/autograder-submit-feedback/index.ts`:
- Around line 766-780: The DB update that sets repository_check_runs.status to
newStatus (inside the block gated by submission_id && !isRegressionRerun &&
!isE2ERun when checkRun is present) currently awaits
adminSupabase.from("repository_check_runs").update(...).eq("id", checkRun.id)
but ignores the result; modify this to capture the response, check for
response.error (or !response.data), and handle failures consistently with other
DB calls in this file—either throw an Error or log to Sentry (and include
context like checkRun.id, submission_id and newStatus) so failures aren’t
silent.

---

Nitpick comments:
In `@supabase/functions/github-repo-webhook/index.ts`:
- Around line 282-294: The circuit breaker check (checkCircuitBreakerOpen) is
being executed inside the per-commit loop (for (const commit of
payload.commits)), causing redundant RPCs; move the call to run once before
iterating commits—immediately after you set org/scope (right after where payload
is parsed around line ~278) and keep the existing error handling that throws if
circuitStatus.isOpen; remove the check from inside the loop so the loop only
processes commits and relies on the single pre-loop circuitStatus result (use
the same variables adminSupabase, org, scope, and circuitStatus).

@argos-ci
Copy link

argos-ci bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) 👍 Changes approved 65 changed Mar 1, 2026, 2:50 AM

@jon-bell
Copy link
Contributor Author

jon-bell commented Mar 1, 2026

@coderabbitai full-review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

@jon-bell: I'll perform a full review of all the changes in this PR.

✅ Actions performed

Full review triggered.

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: 4

Caution

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

⚠️ Outside diff range comments (3)
supabase/functions/_shared/SupabaseTypes.d.ts (1)

6173-6194: ⚠️ Potential issue | 🟠 Major

Add | null to Update.check_run_id for schema consistency.

At Line 6193, Update.check_run_id is typed as number but should be number | null to match the Row (Line 6159) and Insert (Line 6176) definitions. This type mismatch prevents valid null assignments like update({ check_run_id: null }) even though the database schema allows null.

Proposed fix
       Update: {
         assignment_group_id?: number | null;
         auto_promote_result?: boolean | null;
-        check_run_id?: number;
+        check_run_id?: number | null;
         class_id?: number;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_shared/SupabaseTypes.d.ts` around lines 6173 - 6194,
Update the Update type for the relevant table so check_run_id allows nulls like
the Row and Insert types; change the type of Update.check_run_id from number to
number | null (the same nullable signature as Row.check_run_id and
Insert.check_run_id) to permit calling update({ check_run_id: null }) without a
type error.
supabase/functions/github-repo-webhook/index.ts (1)

387-403: ⚠️ Potential issue | 🟠 Major

Handle unique-conflict insert as idempotent success.

Between Line 361 and Line 387, concurrent deliveries can race. With the new unique key, one insert will hit 23505; treating that as fatal causes avoidable webhook failures.

Idempotent conflict handling
 const { error: checkRunError } = await adminSupabase.from("repository_check_runs").insert({
   repository_id: studentRepo.id,
   check_run_id: null,
   class_id: studentRepo.class_id,
   assignment_group_id: studentRepo.assignment_group_id,
   commit_message: commit.message,
   sha: commit.id,
   profile_id: studentRepo.profile_id,
   status: status as unknown as Json
 });
 if (checkRunError) {
+  if (checkRunError.code === "23505") {
+    // Duplicate (repository_id, sha): already processed by another delivery.
+    continue;
+  }
   console.error(checkRunError);
   scope.setTag("error_source", "repository_check_run_insert_failed");
   scope.setTag("error_context", "Could not create repository_check_run");
   Sentry.captureException(checkRunError, scope);
   throw checkRunError;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/github-repo-webhook/index.ts` around lines 387 - 403, When
inserting into repository_check_runs via
adminSupabase.from("repository_check_runs").insert(...) treat a Postgres
unique-constraint conflict (error code "23505") as an idempotent success instead
of a fatal error: in the existing error handling for checkRunError, detect if
checkRunError.code === "23505" (or equivalent Postgres unique violation
identifier returned by Supabase) and swallow it (optionally log an
info/debug-level message with context like repository_id/sha), but only call
Sentry.captureException and throw for other error codes; keep the existing scope
tags and error path for non-23505 cases.
supabase/functions/autograder-submit-feedback/index.ts (1)

856-862: ⚠️ Potential issue | 🟠 Major

Check the DB update result when marking check runs completed.

At Line 856, the update response is ignored. If this write fails, the API still returns success with inconsistent state.

Safe error handling
-        await adminSupabase
+        const { error: checkRunUpdateError } = await adminSupabase
           .from("repository_check_runs")
           .update({
             status: newStatus
           })
           .eq("id", checkRun.id);
+        if (checkRunUpdateError) {
+          Sentry.captureException(checkRunUpdateError, scope);
+          throw new UserVisibleError(
+            `Internal error: Failed to finalize check run status: ${checkRunUpdateError.message}`
+          );
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/autograder-submit-feedback/index.ts` around lines 856 -
862, The update to repository_check_runs using adminSupabase
(.update({...}).eq("id", checkRun.id)) ignores the response; change it to
capture the result (data, error) and handle failures: check for error or
affected row count, log the failure with context (checkRun.id and newStatus) and
propagate or return an error/non-200 response instead of continuing; ensure this
check is applied in the same function where adminSupabase and checkRun are used
so the API does not report success when the DB write failed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@supabase/functions/autograder-create-submission/index.ts`:
- Around line 726-727: The code currently sets pushTime using
checkRun?.created_at or TZDate.tz(timeZone), which falls back to now and
reintroduces "now"-based late-detection; instead, fail fast when created_at is
missing: remove the TZDate.tz(timeZone) fallback and throw or return an error
(or reject the request) when checkRun?.created_at is undefined so pushTime is
only constructed from a real push timestamp (use the existing
TZDate(checkRun.created_at, timeZone) path and handle the missing created_at by
raising an explicit error with context).
- Around line 643-655: The actor-role lookup uses case-sensitive .eq and ignores
query errors, which can misclassify staff as students; update the users query to
use a case-insensitive match (e.g., .ilike("github_username", decoded.actor))
instead of .eq, and check the Supabase response error fields for both the users
and user_roles queries (the objects returned from
adminSupabase.from("users").select(...).maybeSingle() and
adminSupabase.from("user_roles").select(...).maybeSingle()); if either query
returns an error, throw or return the error (do not treat it as "no role"), and
only set userRoles = userRolesData ?? undefined when there was no error and the
data is valid. Ensure you reference the adminSupabase calls, the
user/userRolesData variables, and the user_roles lookup in your fix.

In `@supabase/functions/github-repo-webhook/index.ts`:
- Around line 347-353: The circuit-breaker check is using the wrong method key
("create_repo") before a GitHub call that actually triggers a workflow; update
the checkCircuitBreakerOpen invocation to use the workflow-specific method key
(e.g., "triggerWorkflow") and update the scopeInfo string generation to
reference the correct method name so method-level rate-limiting applies to
triggerWorkflow flows; specifically modify the call to
checkCircuitBreakerOpen(adminSupabase, org, "create_repo", scope) and the
related scope/messaging that uses `create_repo` to use `triggerWorkflow` and its
matching display text.

In `@supabase/migrations/20260218120000_make_check_run_id_nullable.sql`:
- Around line 11-12: The ALTER TABLE adding UNIQUE (repository_id, sha) on
repository_check_runs can fail if duplicate historical rows exist; before adding
the constraint (in the same migration, prior to
repository_check_runs_repository_id_sha_key creation) run a dedupe step that
identifies duplicates by (repository_id, sha) and deletes all but a single
canonical row (e.g., keep the row with the smallest id or earliest created_at) —
implement this with a CTE using ROW_NUMBER() OVER (PARTITION BY repository_id,
sha ORDER BY id ASC) and delete where rn > 1, then proceed to ADD CONSTRAINT
repository_check_runs_repository_id_sha_key UNIQUE (repository_id, sha).

---

Outside diff comments:
In `@supabase/functions/_shared/SupabaseTypes.d.ts`:
- Around line 6173-6194: Update the Update type for the relevant table so
check_run_id allows nulls like the Row and Insert types; change the type of
Update.check_run_id from number to number | null (the same nullable signature as
Row.check_run_id and Insert.check_run_id) to permit calling update({
check_run_id: null }) without a type error.

In `@supabase/functions/autograder-submit-feedback/index.ts`:
- Around line 856-862: The update to repository_check_runs using adminSupabase
(.update({...}).eq("id", checkRun.id)) ignores the response; change it to
capture the result (data, error) and handle failures: check for error or
affected row count, log the failure with context (checkRun.id and newStatus) and
propagate or return an error/non-200 response instead of continuing; ensure this
check is applied in the same function where adminSupabase and checkRun are used
so the API does not report success when the DB write failed.

In `@supabase/functions/github-repo-webhook/index.ts`:
- Around line 387-403: When inserting into repository_check_runs via
adminSupabase.from("repository_check_runs").insert(...) treat a Postgres
unique-constraint conflict (error code "23505") as an idempotent success instead
of a fatal error: in the existing error handling for checkRunError, detect if
checkRunError.code === "23505" (or equivalent Postgres unique violation
identifier returned by Supabase) and swallow it (optionally log an
info/debug-level message with context like repository_id/sha), but only call
Sentry.captureException and throw for other error codes; keep the existing scope
tags and error path for non-23505 cases.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14a4890 and 7853ed0.

📒 Files selected for processing (6)
  • supabase/functions/_shared/SupabaseTypes.d.ts
  • supabase/functions/autograder-create-submission/index.ts
  • supabase/functions/autograder-submit-feedback/index.ts
  • supabase/functions/github-async-worker/index.ts
  • supabase/functions/github-repo-webhook/index.ts
  • supabase/migrations/20260218120000_make_check_run_id_nullable.sql

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 1, 2026
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.

🧹 Nitpick comments (2)
supabase/functions/github-repo-webhook/index.ts (1)

33-87: Third copy of detectRateLimitType - consolidation recommended.

This is the third implementation of detectRateLimitType across the codebase (also in autograder-submit-feedback/index.ts and autograder-create-submission/index.ts). Consider extracting to _shared/GitHubWrapper.ts alongside the existing SecondaryRateLimitError and PrimaryRateLimitError classes.

Suggested approach
// In _shared/GitHubWrapper.ts
+export function detectRateLimitType(error: unknown): {
+  type: "secondary" | "primary" | "extreme" | null;
+  retryAfter?: number;
+  installationId?: string;
+} {
+  // ... consolidated implementation
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/github-repo-webhook/index.ts` around lines 33 - 87,
detectRateLimitType is duplicated in multiple functions; extract and reuse a
single implementation by moving the function into a shared module (e.g.,
_shared/GitHubWrapper.ts) alongside SecondaryRateLimitError and
PrimaryRateLimitError, export it, and import it from
github-repo-webhook/index.ts (and from autograder-submit-feedback/index.ts and
autograder-create-submission/index.ts) so each file calls the centralized
detectRateLimitType; ensure the exported function signature and behavior
(handling AggregateError messages, headers, status checks, and retryAfter logic)
remain unchanged and update imports/uses accordingly.
supabase/functions/autograder-submit-feedback/index.ts (1)

28-82: Consider extracting detectRateLimitType to a shared module.

This function is duplicated across multiple files (autograder-submit-feedback/index.ts, autograder-create-submission/index.ts, and github-repo-webhook/index.ts). While the implementations have minor variations, centralizing this logic in _shared/GitHubWrapper.ts would reduce maintenance burden and ensure consistent rate-limit detection behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/autograder-submit-feedback/index.ts` around lines 28 - 82,
Extract the duplicated detectRateLimitType function into a shared module (e.g.,
_shared/GitHubWrapper.ts): move the full implementation of detectRateLimitType
along with any type exports it needs, ensure it exports the function (and any
helper types) and imports or references SecondaryRateLimitError and
PrimaryRateLimitError from their canonical source; then update each location
that currently defines its own copy (the functions named detectRateLimitType in
the three modules) to import and call the shared detectRateLimitType instead,
preserving the exact behavior (including returned shape: type | retryAfter |
installationId) and keep handling of AggregateError/err.message, headers
parsing, and status checks identical so behavior remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@supabase/functions/autograder-submit-feedback/index.ts`:
- Around line 28-82: Extract the duplicated detectRateLimitType function into a
shared module (e.g., _shared/GitHubWrapper.ts): move the full implementation of
detectRateLimitType along with any type exports it needs, ensure it exports the
function (and any helper types) and imports or references
SecondaryRateLimitError and PrimaryRateLimitError from their canonical source;
then update each location that currently defines its own copy (the functions
named detectRateLimitType in the three modules) to import and call the shared
detectRateLimitType instead, preserving the exact behavior (including returned
shape: type | retryAfter | installationId) and keep handling of
AggregateError/err.message, headers parsing, and status checks identical so
behavior remains unchanged.

In `@supabase/functions/github-repo-webhook/index.ts`:
- Around line 33-87: detectRateLimitType is duplicated in multiple functions;
extract and reuse a single implementation by moving the function into a shared
module (e.g., _shared/GitHubWrapper.ts) alongside SecondaryRateLimitError and
PrimaryRateLimitError, export it, and import it from
github-repo-webhook/index.ts (and from autograder-submit-feedback/index.ts and
autograder-create-submission/index.ts) so each file calls the centralized
detectRateLimitType; ensure the exported function signature and behavior
(handling AggregateError messages, headers, status checks, and retryAfter logic)
remain unchanged and update imports/uses accordingly.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7853ed0 and 93fb639.

📒 Files selected for processing (4)
  • supabase/functions/autograder-create-submission/index.ts
  • supabase/functions/autograder-submit-feedback/index.ts
  • supabase/functions/github-repo-webhook/index.ts
  • supabase/migrations/20260218120000_make_check_run_id_nullable.sql

@jon-bell jon-bell merged commit e28b40a into staging Mar 1, 2026
9 checks passed
@jon-bell jon-bell deleted the improve-submission-github-calls branch March 1, 2026 16:43
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.

1 participant