Skip to content

feat(instrument-repos): add GitHub-hosted instrument repositories#1383

Merged
joshunrau merged 8 commits into
mainfrom
instrumentRepo
Jun 18, 2026
Merged

feat(instrument-repos): add GitHub-hosted instrument repositories#1383
joshunrau merged 8 commits into
mainfrom
instrumentRepo

Conversation

@thomasbeaudry

@thomasbeaudry thomasbeaudry commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Admins can import instruments from a GitHub repository, sync to pull in new instruments, view a repo's instrument list, and delete repos. Group managers assign repos to groups and opt-in to individual instruments.

Backend:

  • InstrumentRepo model + module/service/controller; instrument provenance tracked via Instrument.sourceRepoId/sourceRepoName
  • private-repo support via an AES-256-GCM encrypted access token
  • re-adding an existing repo re-syncs it (and updates a supplied token) instead of failing; repo name is no longer unique (url is the identity)
  • orphan reconciliation when a source repo is deleted
  • groups only auto-connect non-repo instruments on creation

Frontend:

  • admin instrument-repos page (add/sync/view/delete) and repo assignment by the admin in manage groups;
  • Grouo manager can select instruments with insturment repo buttons displayed in group management
  • Instrument summary & -preview feature added so group manager doesn't have to guess which form is which
  • repo mutations invalidate the instrument list; opt-out flag added to the axios error interceptor so callers can show a single specific error toast

Tests: unit coverage for the repo service (crypto round-trip, secret stripping, delete guards, create/sync/dedup, orphan reconciliation).

Summary by CodeRabbit

Release Notes

  • New Features

    • Added instrument repository management: create/import, sync, list, view, and delete (with optional encrypted access tokens).
    • Admin “Instrument Repositories” page with add/sync/view/delete actions and safety checks when repos are assigned to groups.
    • Groups can link/unlink repositories, updating accessible instruments and exposing provenance for repo-sourced instruments.
  • Bug Fixes

    • Improved group update logic for repository relations and name-collision handling.
    • Added ability/permission coverage for instrument repository management.
  • Tests

    • Added unit tests covering encryption, import/sync/reconciliation, and deletion constraints.
  • Chores

    • Updated CI ordering and Playwright version; improved request error-notification control.

Admins can import instruments from a GitHub repository, sync to pull in
new instruments, view a repo's instrument list, and delete repos. Group
managers assign repos to groups and opt-in to individual instruments.

Backend:
- InstrumentRepo model + module/service/controller; instrument provenance
  tracked via Instrument.sourceRepoId/sourceRepoName
- private-repo support via an AES-256-GCM encrypted access token
- re-adding an existing repo re-syncs it (and updates a supplied token)
  instead of failing; repo name is no longer unique (url is the identity)
- orphan reconciliation when a source repo is deleted
- groups only auto-connect non-repo instruments on creation

Frontend:
- admin instrument-repos page (add/sync/view/delete) and repo assignment
  in admin groups; instrument preview in group management
- repo mutations invalidate the instrument list; opt-out flag added to the
  axios error interceptor so callers can show a single specific error toast

Tests: unit coverage for the repo service (crypto round-trip, secret
stripping, delete guards, create/sync/dedup, orphan reconciliation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thomasbeaudry thomasbeaudry requested a review from joshunrau as a code owner June 16, 2026 05:25
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds full support for GitHub-backed instrument repositories. Includes new InstrumentRepo Prisma model and Zod schemas, a NestJS InstrumentReposService with AES-256-GCM token encryption, tarball-based instrument import, orphan reconciliation, and a REST controller. The web side adds query/mutation hooks, an admin management page, and updates the group manage UI to filter instruments by assigned repos.

Changes

Instrument Repositories Feature

Layer / File(s) Summary
Shared schemas and data contracts
packages/schemas/src/instrument-repo/instrument-repo.ts, packages/schemas/src/core/core.ts, packages/schemas/src/group/group.ts, packages/schemas/src/instrument/instrument.base.ts, packages/schemas/package.json
New $InstrumentRepo and $CreateInstrumentRepoData Zod schemas added; $Group gains instrumentRepoIds; InstrumentInfo gains optional sourceRepo provenance; $AppSubjectName gains InstrumentRepo.
Prisma schema and authorization
apps/api/prisma/schema.prisma, apps/api/src/auth/ability.factory.ts
Group gains repo relation fields; Instrument gains sourceRepoId/sourceRepoName provenance; new InstrumentRepo model with encrypted accessToken and sync metadata; AppSubject enum and CASL ability factory extended.
InstrumentReposService: CRUD, encryption, import, and orphan reconciliation
apps/api/src/instrument-repos/instrument-repos.service.ts, apps/api/src/instrument-repos/dto/create-instrument-repo.dto.ts, apps/api/package.json
New service covering create/sync/delete/find operations with AES-256-GCM token encryption, GitHub tarball download+extraction, per-directory bundling, provenance tagging, and self-healing orphan reconciliation on onModuleInit.
InstrumentReposService unit tests
apps/api/src/instrument-repos/__tests__/instrument-repos.service.spec.ts
Vitest suite covering encryption round-trip, CRUD secret-stripping, deduplication, sync token decryption, and orphan reconciliation re-attribution and manual conversion scenarios.
Controller, module, and app bootstrap
apps/api/src/instrument-repos/instrument-repos.controller.ts, apps/api/src/instrument-repos/instrument-repos.module.ts, apps/api/src/main.ts
REST controller exposing 5 endpoints with RouteAccess and Swagger metadata; module wires controller/service/InstrumentsModule; main.ts registers the new module.
GroupsService refactor
apps/api/src/groups/groups.service.ts, apps/api/src/groups/dto/update-group.dto.ts, apps/api/src/groups/groups.module.ts, apps/api/src/groups/__tests__/groups.service.spec.ts
Drops InstrumentsService injection; uses direct Instrument model filtered to sourceRepoId: null on create; updateById now sets instrumentRepos relation; DTO, module, and test updated accordingly.
InstrumentsService provenance in findInfo
apps/api/src/instruments/instruments.service.ts
findInfo builds a sourceMap via new buildInstrumentSourceMap() and attaches sourceRepo provenance to each InstrumentInfo.
Web utilities
apps/web/src/utils/error.ts, apps/web/src/services/axios.ts, apps/web/src/styles.css
getApiErrorMessage extracts server messages from Axios errors; Axios interceptor gains disableDefaultErrorNotification meta flag; CSS adds selected-row highlight style.
Web hooks: instrument repo query and mutations
apps/web/src/hooks/useInstrumentReposQuery.ts, apps/web/src/hooks/useCreateInstrumentRepoMutation.ts, apps/web/src/hooks/useDeleteInstrumentRepoMutation.ts, apps/web/src/hooks/useSyncInstrumentRepoMutation.ts
Query hook with INSTRUMENT_REPOS_QUERY_KEY; create/delete/sync mutations each invalidate repo and instrument-info caches and show appropriate notifications.
Route tree, nav, and instrument-repos admin page
apps/web/src/routes/_app/admin/instrument-repos/index.tsx, apps/web/src/route-tree.ts, apps/web/src/hooks/useNavItems.ts
New AddRepoForm + RouteComponent with DataTable (Sync/View/Delete), View dialog with animated instrument list, and deletion guard. Route tree fully wired; nav item added for admins.
Groups admin UI: repo assignment
apps/web/src/routes/_app/admin/groups/index.tsx
Groups admin gains repo selection sheet with checkbox list and save. Row interaction changed: single-click highlights, double-click selects/opens. Route loader prefetches both groups and repos.
Users and datahub admin UI: row highlights
apps/web/src/routes/_app/admin/users/index.tsx, apps/web/src/routes/_app/datahub/index.tsx
Users admin adds InstrumentRepo permission subject. Both tables gain highlightedRowId state with data-row-selected markers and click handlers.
Group manage UI: repo-aware selection
apps/web/src/routes/_app/group/manage.tsx
ManageGroupForm overhauled with InstrumentSection (categorized search+checkbox), InstrumentPreviewDialog, and Set-based selection. RouteComponent filters visibility by instrumentRepoIds and computes hiddenAccessibleIds to preserve off-screen selections.
CI workflow and testing improvements
.github/workflows/ci.yaml, testing/e2e/package.json
Lint and unit tests moved before Playwright install; Playwright step now has 10-minute timeout and installs only required browsers (chromium, firefox). E2E Playwright dependency updated.

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant InstrumentReposPage
  participant InstrumentReposService
  participant GitHub
  participant InstrumentsService
  participant MongoDB

  Admin->>InstrumentReposPage: Submit GitHub URL + PAT
  InstrumentReposPage->>InstrumentReposService: POST /v1/instrument-repos
  InstrumentReposService->>InstrumentReposService: parseRepoUrl, encryptToken(AES-256-GCM)
  InstrumentReposService->>MongoDB: upsert InstrumentRepo by url
  InstrumentReposService->>GitHub: GET tarball (main → master fallback)
  GitHub-->>InstrumentReposService: tarball stream
  InstrumentReposService->>InstrumentReposService: extract dirs, bundle instruments
  InstrumentReposService->>InstrumentsService: create(bundledInstrument)
  InstrumentsService-->>InstrumentReposService: instrumentId (or existing id on conflict)
  InstrumentReposService->>MongoDB: tagInstruments(sourceRepoId, sourceRepoName)
  InstrumentReposService->>MongoDB: reconcileOrphanedInstruments
  InstrumentReposService-->>InstrumentReposPage: InstrumentRepo (accessToken stripped)
  InstrumentReposPage->>InstrumentReposPage: invalidate instrument-repos + instrument-info cache
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • DouglasNeuroInformatics/OpenDataCapture#1325: Both PRs modify apps/web/src/routes/_app/group/manage.tsx to change the ManageGroupForm selection/submit logic, with main PR reworking accessibility based on instrumentRepoIds while retrieved PR extends the form with series selections.
  • DouglasNeuroInformatics/OpenDataCapture#1226: Both PRs touch authorization wiring in apps/api/src/auth/ability.factory.ts via createForPayload/CASL rule generation, with main PR extending abilities for the new InstrumentRepo subject.
  • DouglasNeuroInformatics/OpenDataCapture#1009: Both PRs extend AppSubject enum in packages/schemas/src/core/core.ts and modify CASL permission rules, directly affecting the subjects that authorization rules operate on.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changeset: introducing GitHub-hosted instrument repository support with comprehensive backend and frontend implementation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch instrumentRepo

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.

Comment thread apps/api/src/instrument-repos/instrument-repos.service.ts Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (1)
apps/api/src/groups/groups.service.ts (1)

67-80: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the incoming name for duplicate checks in updateById.

Line 77 checks group.name instead of the requested data.name, which can incorrectly throw ConflictException on rename attempts. Compare against the proposed name and skip when unchanged.

Suggested fix
-    const exists = typeof data.name === 'string' && (await this.groupModel.exists({ name: group.name }));
+    const nextName = typeof data.name === 'string' ? data.name : undefined;
+    const exists =
+      typeof nextName === 'string' &&
+      nextName !== group.name &&
+      (await this.groupModel.exists({ name: nextName }));
     if (exists) {
-      throw new ConflictException(`Group with name '${group.name}' already exists!`);
+      throw new ConflictException(`Group with name '${nextName}' already exists!`);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/groups/groups.service.ts` around lines 67 - 80, The duplicate
name check in the `updateById` method is comparing the current group's name
against the database instead of the incoming requested name from `data.name`.
This causes the method to incorrectly throw a ConflictException on rename
attempts even when no actual conflict exists. Fix this by updating the exists
check to use `data.name` instead of `group.name`, and add an additional
condition to skip the duplicate check entirely when the incoming name matches
the current group's name (to avoid false positives when no rename is occurring).
🧹 Nitpick comments (1)
apps/api/src/groups/__tests__/groups.service.spec.ts (1)

30-42: ⚡ Quick win

Add targeted assertions for the new repo-aware behavior.

Current tests don’t verify the new instrumentModel.findMany({ where: { sourceRepoId: null } }) path or instrumentRepoIds relation updates. Add focused tests/assertions for both to lock in this feature behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/groups/__tests__/groups.service.spec.ts` around lines 30 - 42,
The test suite for the groupsService.create method lacks assertions that verify
the new repo-aware instrument behavior. Add targeted assertions within the
existing test cases in the create describe block to verify that when creating a
group, the service correctly calls instrumentModel.findMany with a filter for
sourceRepoId: null and properly updates the instrumentRepoIds relation. Create
one or more focused test cases that mock the instrumentModel.findMany response
and assert that the returned instruments have their instrumentRepoIds relation
updated with the newly created group's ID, ensuring the complete feature
behavior is locked in by tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/auth/ability.factory.ts`:
- Line 30: The ability.can() call for GROUP_MANAGER read access to
InstrumentRepo on line 30 grants unconditional read permissions to all
repositories, which is inconsistent with the constrained manage access on Group
shown on line 28. Add a groupIds condition to the InstrumentRepo read permission
using the hasSome operator to restrict access only to the repositories assigned
to the user's groupIds, matching the pattern already used for the Group manage
constraint.

In `@apps/api/src/instrument-repos/instrument-repos.service.ts`:
- Line 225: The cleanup logic in importInstruments is deleting
path.dirname(repoDir) instead of the actual temporary directory created for this
import. When downloadAndExtractRepo returns tmpDir at line 225, the
importInstruments function should use that exact tmpDir for cleanup at lines
320-321 rather than computing the parent directory with path.dirname(repoDir).
Track and pass the tmpDir value through the import process and ensure cleanup
removes the exact tmpDir that was created, not its parent OS temp directory.
- Around line 214-215: The synchronous execFileSync calls for curl and tar
operations in the downloadAndExtractRepo method are blocking the Node.js event
loop and preventing other requests from being processed. Refactor
downloadAndExtractRepo to be an asynchronous method that delegates these
long-running operations (the curl and tar execFileSync calls) to a worker thread
or background queue, similar to the existing export-worker.js pattern used
elsewhere in the codebase. Update the importInstruments method and its callers
(create() and sync() request handlers) to properly await the refactored async
downloadAndExtractRepo method. This allows the event loop to remain responsive
and handle concurrent requests without blocking for the duration of the curl and
tar operations.

In `@apps/api/src/instruments/instruments.service.ts`:
- Around line 180-181: The buildInstrumentSourceMap() method is performing
full-table scans of all repo-sourced instruments with complete record data on
every findInfo call, which is inefficient. Modify buildInstrumentSourceMap() to
accept the requested instrument IDs as a parameter for filtering and only select
the necessary provenance fields instead of loading full records. Update the
calls to buildInstrumentSourceMap() at line 180 and at lines 260-274 to pass the
relevant instrument ID filters so the method performs scoped queries and returns
minimal data required for provenance information.

In `@apps/web/src/routes/_app/admin/groups/index.tsx`:
- Around line 65-72: The handleSave function closes the sheet by calling
setSelectedGroup(null) immediately after invoking
updateGroupReposMutation.mutate(), without waiting for the mutation to complete.
This causes the user to lose their selection context if the mutation fails. Move
the setSelectedGroup(null) call into the mutation's onSuccess callback so that
the sheet only closes after the save operation completes successfully. You can
configure this by adding an onSuccess handler when calling mutate or by using
the mutation options if they are already set up on the updateGroupReposMutation
definition.
- Around line 41-44: The onSuccess callback in the mutation handler only
invalidates the GROUPS_QUERY_KEY, but this mutation also updates group-to-repo
relationships. Add an additional queryClient.invalidateQueries call to also
invalidate the instrument-repos (or repo-related) cache alongside the existing
GROUPS_QUERY_KEY invalidation. This ensures that repo-management screens will
refetch and display the correct "in use" state after group assignments change.

In `@apps/web/src/routes/_app/admin/instrument-repos/index.tsx`:
- Around line 127-129: The warning message passed to the addNotification call in
the delete-guard logic is user-facing but lacks localization, unlike other UI
text in the application. Wrap the message string for `"${repo.name}" is assigned
to one or more groups. Remove it from those groups before deleting.` with the
appropriate translation function (such as the i18n hook or t() function used
elsewhere in the codebase) to ensure it is properly localized for different
languages.
- Around line 294-302: The structure violates HTML semantics by placing a header
`<div>` with className="grid grid-cols-[1fr_8rem_5rem] gap-x-6 font-bold" and an
`<hr>` element directly inside the `<ul>` tag. HTML `<ul>` elements should only
contain `<li>` children directly. Move the header `<div>` and `<hr>` outside and
above the `<ul>` opening tag (keeping them within the AnimatePresence wrapper if
needed), so that only the mapped instrument items remain as direct children of
the `<ul>`, wrapped appropriately in `<li>` elements.

In `@apps/web/src/routes/_app/group/manage.tsx`:
- Around line 122-131: The icon-only preview button (which contains only the
EyeIcon component and calls onPreview on click) lacks an accessible name, making
it inaccessible to screen readers. Add an aria-label attribute to the button
element with an appropriate descriptive label such as "Preview" to provide
screen reader users with context about the button's purpose.
- Around line 224-225: The selectedIds state on line 224 is only initialized
once from initialSelectedIds and won't update if the prop changes later, causing
stale data to be submitted. Add a useEffect hook that watches for changes to the
initialSelectedIds dependency and calls setSelectedIds with the new value
whenever initialSelectedIds changes, ensuring the component always has current
data from upstream group changes.

---

Outside diff comments:
In `@apps/api/src/groups/groups.service.ts`:
- Around line 67-80: The duplicate name check in the `updateById` method is
comparing the current group's name against the database instead of the incoming
requested name from `data.name`. This causes the method to incorrectly throw a
ConflictException on rename attempts even when no actual conflict exists. Fix
this by updating the exists check to use `data.name` instead of `group.name`,
and add an additional condition to skip the duplicate check entirely when the
incoming name matches the current group's name (to avoid false positives when no
rename is occurring).

---

Nitpick comments:
In `@apps/api/src/groups/__tests__/groups.service.spec.ts`:
- Around line 30-42: The test suite for the groupsService.create method lacks
assertions that verify the new repo-aware instrument behavior. Add targeted
assertions within the existing test cases in the create describe block to verify
that when creating a group, the service correctly calls instrumentModel.findMany
with a filter for sourceRepoId: null and properly updates the instrumentRepoIds
relation. Create one or more focused test cases that mock the
instrumentModel.findMany response and assert that the returned instruments have
their instrumentRepoIds relation updated with the newly created group's ID,
ensuring the complete feature behavior is locked in by tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a856d89c-81ba-428e-a499-e8164d48a595

📥 Commits

Reviewing files that changed from the base of the PR and between fc59c42 and f1fba80.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (33)
  • apps/api/package.json
  • apps/api/prisma/schema.prisma
  • apps/api/src/auth/ability.factory.ts
  • apps/api/src/groups/__tests__/groups.service.spec.ts
  • apps/api/src/groups/dto/update-group.dto.ts
  • apps/api/src/groups/groups.module.ts
  • apps/api/src/groups/groups.service.ts
  • apps/api/src/instrument-repos/__tests__/instrument-repos.service.spec.ts
  • apps/api/src/instrument-repos/dto/create-instrument-repo.dto.ts
  • apps/api/src/instrument-repos/instrument-repos.controller.ts
  • apps/api/src/instrument-repos/instrument-repos.module.ts
  • apps/api/src/instrument-repos/instrument-repos.service.ts
  • apps/api/src/instruments/instruments.service.ts
  • apps/api/src/main.ts
  • apps/web/src/hooks/useCreateInstrumentRepoMutation.ts
  • apps/web/src/hooks/useDeleteInstrumentRepoMutation.ts
  • apps/web/src/hooks/useInstrumentReposQuery.ts
  • apps/web/src/hooks/useNavItems.ts
  • apps/web/src/hooks/useSyncInstrumentRepoMutation.ts
  • apps/web/src/route-tree.ts
  • apps/web/src/routes/_app/admin/groups/index.tsx
  • apps/web/src/routes/_app/admin/instrument-repos/index.tsx
  • apps/web/src/routes/_app/admin/users/index.tsx
  • apps/web/src/routes/_app/datahub/index.tsx
  • apps/web/src/routes/_app/group/manage.tsx
  • apps/web/src/services/axios.ts
  • apps/web/src/styles.css
  • apps/web/src/utils/error.ts
  • packages/schemas/package.json
  • packages/schemas/src/core/core.ts
  • packages/schemas/src/group/group.ts
  • packages/schemas/src/instrument-repo/instrument-repo.ts
  • packages/schemas/src/instrument/instrument.base.ts
💤 Files with no reviewable changes (1)
  • apps/api/src/groups/groups.module.ts

Comment thread apps/api/src/auth/ability.factory.ts Outdated
Comment thread apps/api/src/instrument-repos/instrument-repos.service.ts Outdated
Comment thread apps/api/src/instrument-repos/instrument-repos.service.ts Outdated
Comment thread apps/api/src/instruments/instruments.service.ts Outdated
Comment thread apps/web/src/routes/_app/admin/groups/index.tsx
Comment thread apps/web/src/routes/_app/admin/groups/index.tsx
Comment thread apps/web/src/routes/_app/admin/instrument-repos/index.tsx
Comment thread apps/web/src/routes/_app/admin/instrument-repos/index.tsx Outdated
Comment thread apps/web/src/routes/_app/group/manage.tsx
Comment thread apps/web/src/routes/_app/group/manage.tsx
CodeQL flagged `url.replace(/\/+$/, '')` as a polynomial regular
expression on uncontrolled data. Replace the trailing-slash strip with a
linear manual scan and trim the `.git` suffix via endsWith. Behavior is
unchanged; add tests covering the normalization cases and a large
many-trailing-slash input.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/.tanstack/tmp/ce07c9c8-8fde80335118f8a81687bb2fb01d6220 (1)

1-9: ⚡ Quick win

Remove this generated temp route file from version control.

This duplicates the /_app/group/manage route in a temporary .tanstack/tmp path with placeholder UI, which can create route drift and maintenance confusion. Keep the canonical implementation only in apps/web/src/routes/_app/group/manage.tsx and ignore temp artifacts.

Proposed cleanup
-import { createFileRoute } from '`@tanstack/react-router`'
-
-export const Route = createFileRoute('/_app/group/manage')({
-  component: RouteComponent,
-})
-
-function RouteComponent() {
-  return <div>Hello "/_app/group/manage"!</div>
-}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/.tanstack/tmp/ce07c9c8-8fde80335118f8a81687bb2fb01d6220` around
lines 1 - 9, Remove the temporary generated route file at
apps/web/.tanstack/tmp/ce07c9c8-8fde80335118f8a81687bb2fb01d6220 from version
control as it is a duplicate of the canonical route implementation in
apps/web/src/routes/_app/group/manage.tsx. Delete this temporary file from the
commit and ensure the .tanstack/tmp directory is listed in the project's
.gitignore to prevent similar temp artifacts from being committed in the future.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/web/.tanstack/tmp/ce07c9c8-8fde80335118f8a81687bb2fb01d6220`:
- Around line 1-9: Remove the temporary generated route file at
apps/web/.tanstack/tmp/ce07c9c8-8fde80335118f8a81687bb2fb01d6220 from version
control as it is a duplicate of the canonical route implementation in
apps/web/src/routes/_app/group/manage.tsx. Delete this temporary file from the
commit and ensure the .tanstack/tmp directory is listed in the project's
.gitignore to prevent similar temp artifacts from being committed in the future.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f05c694b-3924-4e08-9167-ec09478035cc

📥 Commits

Reviewing files that changed from the base of the PR and between f1fba80 and 3cc4b41.

📒 Files selected for processing (3)
  • apps/api/src/instrument-repos/__tests__/instrument-repos.service.spec.ts
  • apps/api/src/instrument-repos/instrument-repos.service.ts
  • apps/web/.tanstack/tmp/ce07c9c8-8fde80335118f8a81687bb2fb01d6220
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/src/instrument-repos/tests/instrument-repos.service.spec.ts
  • apps/api/src/instrument-repos/instrument-repos.service.ts

API:
- delete the exact temp dir after import (was removing path.dirname, i.e.
  the OS temp root, when the repo extracted without a single subdir)
- run curl/tar via async execFile so imports don't block the event loop
- scope buildInstrumentSourceMap to the requested instrument ids and
  select only provenance fields (avoids loading every bundle)
- constrain GROUP_MANAGER read on InstrumentRepo to their own groups
- groups.updateById: check the requested name (not the current one) and
  skip when unchanged, so renames no longer always conflict

Web:
- admin groups: close the edit sheet only after a successful save and
  invalidate the instrument-repos cache so "in use" state refreshes
- localize the delete-guard warning
- instrument list dialog: only <li> children inside <ul> (header/hr moved out)
- add aria-label to the icon-only instrument preview button
- resync group instrument selection when upstream group data changes

Tests: add groups.service assertions (non-repo connect, repo relation,
rename collision) and normalizeUrl cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thomasbeaudry

Copy link
Copy Markdown
Collaborator Author

Thanks for the review! Addressed all the CodeRabbit findings (and the earlier CodeQL alert) in 78cea14.

Backend

  • Temp-dir cleanup: import now removes the exact tmpDir it created. Previously, when a repo extracted without a single subdirectory, cleanup ran rmSync(path.dirname(repoDir)) — i.e. the OS temp root. downloadAndExtractRepo now returns { repoDir, tmpDir } and the caller deletes only tmpDir.
  • Event-loop blocking: curl/tar switched from execFileSync to promisified async execFile, so imports no longer block the event loop.
  • buildInstrumentSourceMap: scoped to the requested instrument ids and selects only the provenance fields, so it no longer loads every record (incl. bundle) on each findInfo.
  • GROUP_MANAGER access: read InstrumentRepo is now constrained to { groupIds: { hasSome: groupIds } }, matching the Group permission pattern.
  • groups.updateById rename check: now compares the requested name (not the current one, which always self-matched) and skips the check when unchanged — fixes the spurious ConflictException on rename.

Frontend

  • Admin groups: the edit sheet now closes only after a successful save (selection is preserved on failure), and the mutation also invalidates the instrument-repos cache so the "in use" state refreshes.
  • Localization: the delete-guard warning is now wrapped in t({ en, fr }).
  • List semantics: the instrument-list dialog's <ul> now contains only <li> children (header/<hr> moved out).
  • A11y: added an aria-label to the icon-only instrument preview button.
  • Stale selection: added a useEffect to resync the group's instrument selection when the upstream group data changes.

Tests: added groups.service assertions (non-repo-only connect, repo relation set, rename collision) plus normalizeUrl cases.

CodeQL (ReDoS): normalizeUrl no longer uses the /\/+$/ regex — trailing slashes are trimmed with a linear scan and .git via endsWith.

On the one item I did not change — moving the import to a worker thread/queue: the async execFile change unblocks the event loop, and importing a repo is an infrequent admin-only action, so a worker-thread refactor felt out of scope here. Happy to do it as a follow-up if preferred.

The `playwright install --with-deps` step has hung past the 6h job limit.
Make CI resilient:
- run lint + unit tests before the Playwright step so code feedback isn't
  blocked by a flaky browser download
- cap the install with timeout-minutes: 10 so a hang fails fast
- set DEBIAN_FRONTEND=noninteractive to avoid an apt prompt stalling
- install only chromium + firefox (the only browsers the e2e suite uses)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
apps/api/src/groups/groups.service.ts (1)

92-95: ⚠️ Potential issue | 🔴 Critical

Add authorization check before assigning instrumentRepoIds in group updates.

GROUP_MANAGER users can currently assign repos because the service only validates Group update permission without checking InstrumentRepo manage capability. Lines 92–95 should enforce that only ADMIN-level callers can assign instrumentRepoIds. Either:

  • Filter instrumentRepoIds through ability before assignment, or
  • Reject updates containing instrumentRepoIds unless ability.can('manage', 'InstrumentRepo') is true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/groups/groups.service.ts` around lines 92 - 95, The group update
logic in groups.service.ts at lines 92-95 (the instrumentRepos assignment block)
is missing an authorization check to ensure only users with InstrumentRepo
manage capability can assign instrumentRepoIds. Before assigning
instrumentRepoIds through the set operation, validate that the caller has
permission by checking ability.can('manage', 'InstrumentRepo'). If this check
fails, either filter out the instrumentRepoIds from the update or reject the
entire update request with an appropriate authorization error. This ensures
GROUP_MANAGER users cannot escalate privileges by assigning repos they should
not be able to manage.
🧹 Nitpick comments (1)
apps/api/src/groups/groups.service.ts (1)

26-28: ⚡ Quick win

Select only id when fetching non-repo instruments for group creation.

This query only feeds connect IDs, so fetching full instrument records is unnecessary and can inflate memory/DB payload.

Proposed diff
-    const nonRepoInstruments = await this.instrumentModel.findMany({
-      where: { sourceRepoId: null }
-    });
+    const nonRepoInstruments = await this.instrumentModel.findMany({
+      select: { id: true },
+      where: { sourceRepoId: null }
+    });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/groups/groups.service.ts` around lines 26 - 28, The findMany
query in the nonRepoInstruments assignment is fetching complete instrument
records when only the IDs are needed for the subsequent connect operation.
Optimize this query by adding a select option to the findMany call to retrieve
only the id field from the instruments table, reducing unnecessary memory and
database payload usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/api/src/groups/groups.service.ts`:
- Around line 92-95: The group update logic in groups.service.ts at lines 92-95
(the instrumentRepos assignment block) is missing an authorization check to
ensure only users with InstrumentRepo manage capability can assign
instrumentRepoIds. Before assigning instrumentRepoIds through the set operation,
validate that the caller has permission by checking ability.can('manage',
'InstrumentRepo'). If this check fails, either filter out the instrumentRepoIds
from the update or reject the entire update request with an appropriate
authorization error. This ensures GROUP_MANAGER users cannot escalate privileges
by assigning repos they should not be able to manage.

---

Nitpick comments:
In `@apps/api/src/groups/groups.service.ts`:
- Around line 26-28: The findMany query in the nonRepoInstruments assignment is
fetching complete instrument records when only the IDs are needed for the
subsequent connect operation. Optimize this query by adding a select option to
the findMany call to retrieve only the id field from the instruments table,
reducing unnecessary memory and database payload usage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cef45596-2c2e-4633-9cbd-ca49d16c1007

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc4b41 and 4e9ad51.

📒 Files selected for processing (9)
  • .github/workflows/ci.yaml
  • apps/api/src/auth/ability.factory.ts
  • apps/api/src/groups/__tests__/groups.service.spec.ts
  • apps/api/src/groups/groups.service.ts
  • apps/api/src/instrument-repos/instrument-repos.service.ts
  • apps/api/src/instruments/instruments.service.ts
  • apps/web/src/routes/_app/admin/groups/index.tsx
  • apps/web/src/routes/_app/admin/instrument-repos/index.tsx
  • apps/web/src/routes/_app/group/manage.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/api/src/instruments/instruments.service.ts
  • apps/web/src/routes/_app/group/manage.tsx
  • apps/web/src/routes/_app/admin/instrument-repos/index.tsx
  • apps/web/src/routes/_app/admin/groups/index.tsx
  • apps/api/src/instrument-repos/instrument-repos.service.ts

thomasbeaudry and others added 2 commits June 16, 2026 11:02
The browser download hangs on a stalled CDN socket (Chromium completes,
then the next download sits with no progress until the job/step timeout).
Split OS deps from the browser download and wrap the download in a
per-attempt `timeout 180` + 3x retry; Playwright skips already-downloaded
browsers, so each retry resumes. apt deps stay in their own quick step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The browser install hung at 100% under Node 24 (lts/krypton) — a known
Playwright/Node 24 download issue fixed in newer releases. Bump to 1.60.0
(1.61.0 is blocked by the 7-day minimumReleaseAge policy). Also simplify
the CI Playwright step back to a single capped install now that the
version bump, not a retry hack, is the fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@joshunrau joshunrau left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

looks great! A few minor comments. Also, French translations missing in quite a few places. Can you please just have the agent review and make sure everything is translted.

ability.can('manage', 'Assignment', { groupId: { in: groupIds } });
ability.can('manage', 'Group', { id: { in: groupIds } });
ability.can('read', 'Instrument');
ability.can('read', 'InstrumentRepo', { groupIds: { hasSome: groupIds } });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Doesn't standard user also need to be able to read this?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Otherwise, could cause problems. Did you verify everything works for standard users?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Verified — standard users don't need read InstrumentRepo. The provenance shown in the UI (sourceRepo) is denormalized onto each Instrument record and served via /instruments/info, which is gated only by read Instrument (standard users have that unconstrained). buildInstrumentSourceMap reads sourceRepoId/sourceRepoName straight off the Instrument — it never queries InstrumentRepo or checks that ability. The /instrument-repos endpoints are consumed solely by the two admin-only pages (useInstrumentReposQuery is imported only there), so standard users never hit them. I checked the manage-group / datahub / accessible-instruments flows and they work for standard users. Granting standard users repo read would just be over-permissive, so I left the ability as-is.

curlArgs.push('-H', `Authorization: Bearer ${token}`);
}
curlArgs.push(tarballUrl, '-o', tarballPath);
await execFileAsync('curl', curlArgs, { timeout: 60_000 });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

do we know that curl is installed in the docker containers? I don't think it will be. Is there a Node-based way to do this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's definitely a node-based way to do this.

Comment thread apps/web/src/hooks/useCreateInstrumentRepoMutation.ts Outdated
Comment thread apps/web/src/hooks/useCreateInstrumentRepoMutation.ts Outdated
Comment thread apps/web/src/hooks/useDeleteInstrumentRepoMutation.ts
Comment thread apps/web/src/routes/_app/admin/groups/index.tsx
onSubmit(result.data);
};

return (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is there a reason the libui form cannot be used here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Intentional — this is a plain <form> rather than the libui Form because of autoComplete="off" plus the non-semantic field names, which are needed to stop browsers/password managers from autofilling the URL and access-token fields (there's a comment to that effect above the form). Happy to switch to libui Form if you'd prefer and we find another way to suppress autofill.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

find another way to suppress autofill.

We can add that to libUI if its not already available.

Comment thread apps/web/src/routes/_app/admin/instrument-repos/index.tsx
Comment thread apps/web/src/routes/_app/admin/users/index.tsx
Comment thread apps/web/src/routes/_app/datahub/index.tsx
API:
- replace curl/tar with Node fetch + JSZip in downloadAndExtractRepo so
  imports no longer depend on curl/tar being present in the runtime
  container; add jszip dependency and a zip-slip-guarded extractor
- seed the default DouglasNeuroInformatics/ODC_Instruments repo on fresh
  install (initApp), with graceful failure and a test-env skip

Web:
- translate the create/delete/sync instrument-repo mutation toasts and the
  admin groups repo-update error fallback via useTranslation

Chore:
- remove the accidentally-committed .tanstack/tmp route file and gitignore
  .tanstack

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread apps/api/src/instrument-repos/instrument-repos.service.ts Fixed
The user-supplied repo URL flowed into the GitHub API request URL, which
CodeQL flagged as server-side request forgery. parseGitHubUrl now requires
the github.com host and validates owner/repo against a strict allowlist
(rejecting @, :, ?, #, etc.), throwing BadRequestException otherwise, and
the zipball URL encodes both components. Add tests for the rejected cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@joshunrau joshunrau merged commit 55eb9f6 into main Jun 18, 2026
6 checks passed
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.

4 participants