Skip to content

fix(assignment-delete): atomic GitHub cleanup, FK indexes, lock down RPCs - #1086

Merged
jon-bell merged 7 commits into
stagingfrom
fix/assignment-delete-timeout
Oct 16, 2026
Merged

jon-bell merged 7 commits into
stagingfrom
fix/assignment-delete-timeout

Conversation

@jon-bell

@jon-bell jon-bell commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Incident (2026-10-15)

Deleting CS 3650 fa26 "Project 1: Shell" (assignment 1259, 238 repos) queued 239 archive_repo_and_lock jobs before calling delete_assignment_with_all_data. The RPC then hit authenticator's 8s statement_timeout and rolled back. The archive jobs need only org/repo, so they kept archiving, renaming and removing collaborators from the repos of an assignment that stayed live. An hour later a "sync to handout" queued 238 sync_repo_to_handout jobs against those same repos. Finishing the delete by hand (scripts/oneoff/…) took 81s for an assignment with 0 submissions.

Changes

1. Root cause of the timeout: unindexed FK columns (20261016120000_assignment_delete_atomic_archive.sql)
Every deleted row runs a check for each FK that references it, and without an index that check is a sequential scan. The worst offenders:

FK column references prod size
workflow_events.repository_id repositories 1.19M rows / 471 MB
submissions.repository_check_run_id repository_check_runs 137 MB
repository_check_runs.target_submission_id (partial) submissions 225 MB
submission_files.assignment_group_id (partial) assignment_groups 5.5 GB
submission_file_symbol_index.assignment_group_id (partial) assignment_groups 1.3 GB
grader_result_output.assignment_group_id (partial) assignment_groups 420 MB

At 238 repos × one workflow_events scan each, that adds up to the 81s. Group assignments would be far worse, since every group deleted scans submission_files once. The partial indexes (WHERE … IS NOT NULL) cover columns that are 73–99% NULL, and an FK check's col = $1 can still use them.

2. GitHub cleanup commits with the delete

  • delete_assignment_with_all_data(p_assignment_id, p_class_id, p_enqueue_github_archives default false, p_expected_repository_ids default null). With true, it queues an archive job for every repo it deletes, inside its own transaction. A failed delete queues nothing.
  • The targets come from DELETE … RETURNING on repositories, plus the template and grader repos, so a repo created between the edge function's safety checks and the RPC still gets archived. But it only cleans up repos that passed those checks. The edge function passes the repo IDs it validated (p_expected_repository_ids), and once the rows are locked the RPC refuses with 40001 (HTTP 409, and a retry re-runs the checks) if the set differs. So a repo nobody checked for student work is never archived or deleted.
  • Names are parsed identically on both sides: trim ASCII spaces, exactly one /, and each part must match ^[A-Za-z0-9._-]+$. Org / repo becomes Org/repo, and a tab, a non-breaking space or an internal space is rejected in both places.
  • Before scanning the queue, it locks the assignment's repository rows (FOR UPDATE, in id order, the same lock queue_repository_syncs takes). A concurrent sync press waits until the delete commits, then finds the rows gone.
  • It cancels queued sync_repo_to_handout jobs for those repos on async_calls and async_calls_low_priority. If one is already running, it can't be stopped, so the delete refuses with 55006. Running means claimed (read_ct > 0, still invisible), or carrying the handler's own in_progress marker on the repository row with that msg_id. The marker covers a handler that outlives its visibility timeout. It counts only while its message is still queued, so a crashed worker can't pin the assignment. A message counts as running too if it has been read and its org's drain lease (async_worker_slots) is live. That covers two handlers overlapping on the same repo, where the newer one can overwrite the older one's marker. This last check applies only with org slots enabled, which production has; see Follow-ups. The delete returns HTTP 409 ("try again in a few minutes"). Locking the queue rows FOR UPDATE, with pgmq.read using SKIP LOCKED, means nothing can be claimed while the delete runs.
  • It returns github_repositories, github_archives_queued and handout_syncs_cancelled.
  • Rollout: the old (bigint, bigint) signature is dropped. A two-argument call resolves to false. That caller is a pre-PR edge function that has already queued its own archives, so false skips the claimed-sync refusal and queues nothing. Refusing it would recreate the incident. That is the pre-PR behaviour, minus the timeout.

3. assignment-delete edge function

  • The database delete now runs first, and the function always passes p_enqueue_github_archives: true along with the repo IDs it checked.
  • The Phase 1 repository read is now paged and stops the delete if it fails. Before, it ignored errors and was capped at PostgREST's 1000 rows, so the safety check could cover only the first page.
  • The cleanup strategy is chosen from the targets the RPC actually deleted. For five repos or fewer, it also deletes them outright after the commit. That's only a fast path: every repo is already queued, a failure or a crash part-way leaves the archive jobs to finish the work, and a job for a repo that was deleted is a no-op (archiveRepoAndLock treats a 404 as done).

4. github-async-worker

  • sync_repo_to_handout now drops a job whose repository row no longer exists. A missing row used to read as "no baseline" and fall through to a full sync.
  • The row lookup's error is no longer ignored.

5. Lock down both RPCs

  • delete_assignment_with_all_data and enqueue_github_archive_repo are SECURITY DEFINER with no caller check, and both were executable by anon and authenticated (PUBLIC default).
  • I hotfixed them in prod on 2026-10-15, running as supabase_admin, since a REVOKE run as postgres was a silent no-op.
  • This migration revokes EXECUTE from PUBLIC, anon and authenticated, and grants it to service_role and postgres. Migrations run as supabase_admin, so the revoke takes effect.

6. scripts/oneoff/2026-10-15-delete-assignment-1259.sh, the script used to finish the incident cleanup, kept for the record.

Testing

Migration applied with psql --single-transaction as supabase_admin to supabase/postgres:17.4.1.075 (the prod image), loaded with a schema-only dump of prod public. The test assignment had 241 repos, including one malformed name and one case-only duplicate. workflow_events held 1.2M rows, none of them on the test assignment.

  • Timing: the RPC took 27.8s without the index and 0.22s with it.
  • Atomicity: a forced statement_timeout left 0 archive jobs queued and the assignment intact.
  • Successful delete:
    • It queued 240 jobs (238 student + template + solution). The malformed and duplicate names were skipped.
    • Debug ids match buildAssignmentDeleteArchiveDebugId, and each job has its api_gateway_calls row.
    • It cancelled exactly 238 syncs. Another assignment's queued sync and its repos and events were untouched.
  • Review fixes:
    • A claimed sync makes the delete fail with 55006 and changes nothing. So does a sync whose VT has expired while its marker is still planted. A stale, finished or malformed marker doesn't block. A concurrent marker write blocks until the delete commits, then matches 0 rows. A delayed requeue is cancelled.
    • A concurrent pgmq.read during an open delete claimed none of the locked syncs.
    • The producer's locking read blocked for about 7s until the delete committed, then saw 0 rows. No syncs were left for the deleted repos.
    • A legacy two-argument call with a claimed sync succeeds and queues 0 archives.
    • A one-repo assignment queues archives for both its student and template repos.
    • Names stored as " Org / spaced-repo " and " Org /handout-doomed " normalize and get queued. Names containing a tab, a non-breaking space or an internal space are dropped.
    • The snapshot check: a late repo, a stale ID or an empty snapshot each refuse with 40001 and change nothing. A matching snapshot succeeds in any order. An assignment with no repos and an empty snapshot succeeds.
  • Old callers: a two-argument call queues nothing and still reports the targets.
  • Permissions: anon and authenticated get permission denied on both functions.
  • Static checks:
    • jest assignment-delete-repository-cleanup passes, 14 tests (5 new).
    • prettier and shellcheck are clean.
    • deno check shows the same 17 errors as main, all in github-async-worker and _shared, none in assignment-delete.

The worker change has no unit test. processEnvelope has no harness in tests/unit.

Deploy notes

Merge after #1087. This migration is numbered 20261016120000 so that it runs after #1087's 20261015120000_lock_down_security_definer_functions.sql. migrate.sh rejects duplicate versions, and #1087 revokes grants on the (bigint, bigint) signature this migration drops. I applied both, in that order, to the prod schema: both functions end up executable only by service_role and postgres, and #1087's CheckFunctionGrants reports 0 errors and 0 warnings on the combined tree.

⚠️ Build the indexes by hand with CONCURRENTLY before deploying to prod. The migration's plain CREATE INDEX blocks writes for as long as the build runs, which matters most on submission_files (submission path, 5.5 GB). The CONCURRENTLY statements are in the migration header. With the indexes already built, the migration's IF NOT EXISTS makes it a no-op.

Follow-ups (not in this PR)

  • With org slots disabled (orgSlotGlobalCap: 0, the chart default), an older handler that outlives its VT while a newer one for the same repo has already cleared the marker can't be detected. Closing that needs a per-handler lease in the worker. The repos involved have passed the only-template-commits check, so the worst case is a sync writing into a repo that's being archived.
  • class_metrics_workflow_events_trg is row-level, about 22µs per deleted workflow_events row. That's roughly 4s for an assignment with 200k events, so it's the next thing that would push a large delete toward the timeout.
  • archive_repo_and_lock removes collaborators one at a time (~34 per repo at ~3s each), so a 238-repo delete takes about 2.5 hours to drain the queue.

🤖 Generated with Claude Code

…RPCs

On 2026-10-15 deleting CS 3650 assignment 1259 (238 repos) queued 239
archive_repo_and_lock jobs before calling delete_assignment_with_all_data,
which then hit the 8s statement timeout and rolled back. The archive jobs
kept running against an assignment that stayed live, and a later "sync to
handout" queued 238 syncs against repos being archived.

- Index six unindexed FK columns. Each deleted repository seq-scanned
  workflow_events (1.19M rows) for its FK check: 81s in production.
  Production-shaped test: 27.8s -> 0.22s.
- delete_assignment_with_all_data(..., p_enqueue_github_archives) queues
  the archive jobs inside its own transaction, from the rows it actually
  deletes, and cancels queued sync_repo_to_handout jobs for them. It
  returns the GitHub targets it removed.
- assignment-delete deletes the DB first. Synchronous GitHub deletes run
  after the commit and fall back to an archive job on failure.
- github-async-worker drops sync_repo_to_handout when the repository row
  no longer exists instead of syncing with no baseline.
- Revoke EXECUTE from PUBLIC/anon/authenticated on
  delete_assignment_with_all_data and enqueue_github_archive_repo. Both
  are SECURITY DEFINER with no caller check and were callable with the
  anon key (hotfixed in production 2026-10-15).
- One-off script used to finish the 1259 cleanup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jon-bell
jon-bell requested a review from a team October 16, 2026 14:04
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review: 1
  • staging

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1d6c971c-95fb-4abb-a197-b6e5ff46c0e8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@jon-bell
jon-bell deployed to preview-provision October 16, 2026 14:04 — with GitHub Actions Active
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T16:19:14.499342Z ad4d0a6 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@jon-bell
jon-bell deployed to preview-build October 16, 2026 14:06 — with GitHub Actions Active
@jon-bell
jon-bell deployed to preview-publish October 16, 2026 14:06 — with GitHub Actions Active

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 056256c813

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20261015120000_assignment_delete_atomic_archive.sql Outdated
Comment thread supabase/migrations/20261015120000_assignment_delete_atomic_archive.sql Outdated
…t race claimed syncs

Review feedback on #1086:

- The RPC's target filter rejected names the TypeScript parser accepts
  ("Org / repo" -> Org/repo), so a template/solution that passed the edge
  function's validation could be silently left out of cleanup. Parse in
  SQL exactly like parseGitHubRepoFullName: trim, split on '/', exactly
  two trimmed non-empty parts.
- Deleting the queue row of a sync_repo_to_handout job a worker has
  already claimed does not stop that worker. Lock the assignment's sync
  rows FOR UPDATE (pgmq.read claims with SKIP LOCKED, so nothing can be
  claimed mid-delete) and refuse with 55006 / HTTP 409 if any is already
  claimed (read_ct > 0, vt in the future). Delayed requeues (read_ct 0)
  are still cancelled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ea013b6cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20261015120000_assignment_delete_atomic_archive.sql Outdated
Comment thread supabase/migrations/20261015120000_assignment_delete_atomic_archive.sql Outdated
Comment thread supabase/functions/assignment-delete/index.ts Outdated
…ue archives; safe legacy mode

Second round of review feedback on #1086:

- Sync producers: queue_repository_syncs locks repository rows, not queue
  rows, so it could enqueue after the delete's queue scan. The RPC now
  locks the assignment's repository rows (FOR UPDATE, id order, as the
  producer does) before scanning, so a concurrent sync press waits and
  then finds the rows gone. Also scans async_calls_low_priority.
- Stale strategy: the RPC queues an archive job for EVERY target whenever
  the caller passes true, and the edge function picks immediate-delete vs
  archive-only from the targets the RPC actually deleted. Immediate
  deletes are now only a fast path: a failure, timeout or crash part-way
  still leaves every repository queued (archiveRepoAndLock treats a 404
  as done). The edge function always passes true.
- Rollout: a two-argument call is an old assignment-delete that has
  already committed its own archives. Refusing it would recreate the
  incident, so false skips the claimed-sync refusal and queues nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bcbef0d0d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20261015120000_assignment_delete_atomic_archive.sql Outdated
@jon-bell
jon-bell deployed to preview-provision October 16, 2026 14:45 — with GitHub Actions Active
…k; strict name parsing

Third round of review feedback on #1086:

- A repository created after the edge function's only-template-commits
  check was swept into DELETE ... RETURNING and archived, or deleted
  outright on the synchronous path, without ever being checked. The edge
  function now passes the repository ids it validated
  (p_expected_repository_ids), and the RPC refuses with 40001 (HTTP 409,
  retry re-checks) if the locked set differs.
- The Phase 1 repository read ignored its error and was capped at
  PostgREST's 1000 rows; it is now paged and a failure stops the delete.
- JS trim() and SQL btrim() disagree on tabs and Unicode whitespace. Both
  sides now trim ASCII spaces only and require GitHub's name characters
  ([A-Za-z0-9._-]), so unusual values are rejected identically.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0577324f47

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20261015120000_assignment_delete_atomic_archive.sql Outdated
… running

Fourth round of review feedback on #1086: "claimed" was read_ct > 0 and
vt in the future, but a handler can outlive its visibility timeout (a
batch of 4 measured ~420s against the 300s chart default), and then its
message looks unclaimed while the worker is still mutating GitHub.

A queued sync now counts as running if it is claimed OR its repository
carries the handler's own in_progress marker naming that msg_id. The
marker lasts exactly as long as the handler; markers whose message has
left the queue are stale and block nothing, so a crashed worker cannot
pin an assignment. The pre-marker window is already closed by the
repository row lock (the marker write blocks, then matches no row).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8069d6be01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20261015120000_assignment_delete_atomic_archive.sql Outdated
…r org drain lease

Fifth round of review feedback on #1086: the in_progress marker is one
value per repository, so with two overlapping handlers (H2/H3) the newer
can overwrite and clear it while the older, past its VT, still runs.

A sync message that has been read at least once now also counts as
running while a live async_worker_slots lease exists for the org the
slot allocator charges it to (owner of args.repository_full_name,
lowercased, same queue). A leaseholder renews its slot while it works and
releases it only after every in-flight message finishes, and the delete
holds the message rows locked so no new handler can start on them.

With org slots disabled (chart default orgSlotGlobalCap 0) there is no
lease to consult; production runs with them enabled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@argos-ci

argos-ci Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

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

Build Status Details Updated (UTC)
default (Inspect) 👍 Approved by Jonathan Bell 1 changed, 2 ignored Oct 16, 2026, 4:47 PM

@jon-bell
jon-bell deployed to preview-provision October 16, 2026 16:06 — with GitHub Actions Active
@jon-bell
jon-bell deployed to preview-publish October 16, 2026 16:11 — with GitHub Actions Active
@jon-bell
jon-bell deployed to preview-build October 16, 2026 16:11 — with GitHub Actions Active
#1087 also uses version 20261015120000, which migrate.sh rejects as a
duplicate. Its migration revokes grants on the (bigint, bigint) signature
this one drops, so this one has to sort after it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jon-bell
jon-bell changed the base branch from main to staging October 16, 2026 16:13

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad4d0a649f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +249 to +250
WHERE q.message->>'method' = 'sync_repo_to_handout'
AND q.message->'args'->>'repository_id' = ANY (v_repository_ids::text[])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Cancel repository-creation jobs before deleting

When an assignment still has a queued or running create_repo message, this scan ignores it because it selects only sync_repo_to_handout. The create_repo handler performs the slow GitHub creation and permission sync before updating the repository row, so the delete can remove that row and enqueue an archive concurrently; the archive may observe a 404 and finish while creation later succeeds, or a running handler may retry after the archive renamed the first repository, leaving a new active repository with student access after the assignment is gone. Lock/refuse running creation jobs and cancel queued ones, or make the worker verify that its repository and assignment still exist before its first GitHub mutation.

Useful? React with 👍 / 👎.

@jon-bell
jon-bell merged commit e372f39 into staging Oct 16, 2026
20 of 33 checks passed
@jon-bell
jon-bell deployed to preview-teardown October 16, 2026 17:00 — with GitHub Actions Active
@jon-bell
jon-bell deployed to preview-provision October 16, 2026 17:36 — with GitHub Actions Active
@jon-bell
jon-bell deployed to preview-build October 16, 2026 17:40 — with GitHub Actions Active
@jon-bell
jon-bell deployed to preview-publish October 16, 2026 17:40 — with GitHub Actions Active
@jon-bell
jon-bell deleted the fix/assignment-delete-timeout branch October 16, 2026 17:43
@jon-bell
jon-bell deployed to preview-deploy October 16, 2026 17:53 — with GitHub Actions Active
@github-actions

Copy link
Copy Markdown

🚀 Preview deployed for pr-1086-ad4d0a6

Sample login emails

Role Email
Instructor instructor@pawtograder.net
Grader grader@pawtograder.net
Student student@pawtograder.net

Shared password lives in OpenBao at kv/apps/pawtograder/preview-shared (key seed_user_password).

The preview tears down automatically when this PR closes.

This branch was successfully deployed

5 active deployments
preview-deploy — ad4d0a64 Deployed Oct 16, 2026 by jon-bell via deploy #1409
preview-build — ad4d0a64 Deployed Oct 16, 2026 by jon-bell via build-web #1409
preview-publish — ad4d0a64 Deployed Oct 16, 2026 by jon-bell via publish-e2e-bundle #1409
preview-provision — ad4d0a64 Deployed Oct 16, 2026 by jon-bell via secrets #1409
preview-teardown — ad4d0a64 Deployed Oct 16, 2026 by jon-bell via destroy #1411
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