Skip to content

feat(multi-repo): support multiple repositories per project (backend + UI)#183

Open
eipasteur wants to merge 9 commits into
mainfrom
feature/multi-repo
Open

feat(multi-repo): support multiple repositories per project (backend + UI)#183
eipasteur wants to merge 9 commits into
mainfrom
feature/multi-repo

Conversation

@eipasteur
Copy link
Copy Markdown
Contributor

Summary

Add support for multiple repositories per project end-to-end (backend + frontend). A project can now link several GitHub repositories — each one becomes a Repository vertex in Neptune connected to the Project vertex via a HAS_REPO edge — with role detection (frontend/backend/infra/shared/docs) and tech-stack inference (languages, frameworks).

Backend (lambda/projects/)

  • CRUD endpoints on /projects/{projectId}/repos:
    • GET — list linked repos for a project
    • POST — add a repo (membership required, owner/admin role for write)
    • DELETE — remove a repo (role-gated)
  • Lazy migration: existing git_repo field auto-creates a Repository vertex on first read.
  • Tech-stack detection via GitHub API (read-only): package.json, requirements.txt, Cargo.toml, etc. Fails non-fatally — repo is added even if detection fails.
  • Server-side URL validation: requires owner/repo format (regex matches GitHub's published constraints — owner ≤39 chars, repo ≤100 chars).
  • Backward compatible: legacy single-repo creation paths preserved; gitRepo field on the project still reflects the primary repo.

Frontend (frontend/src/)

  • GitHubRepoSelect: support multi-select mode (checkboxes + search filter + exclude list to hide repos already linked). Discriminated union on multiple prop preserves the single-select API.
  • ProjectSettings: new Repositories section listing linked repos (URL, role badge, detected stack), bulk-add via the multi-select picker (parallel addRepo calls with Promise.allSettled), per-repo remove with confirmation. Permissions enforced UI-side.
  • services/projects.ts: ProjectRepo type + listRepos / addRepo / removeRepo client methods.

CreateProjectModal stays single-repo for now — users create with one repo and add more from settings. Mirrors the GitLab-side phasing (Phase 3 of multi-repo rollout).

Stacked on

This PR includes #182 (fix/lambda-github-esbuild-runtime) because the multi-repo POST flow calls into the github lambda, which is broken on main until the fix lands. Merge #182 first and this PR will rebase cleanly to drop the merged commits.

Validation

  • ✅ All 153 unit tests pass
  • ✅ TypeScript clean
  • ✅ Frontend npm run build clean
  • ✅ Manual: backend deployed to staging, lambda invokes return 200 JSON
  • ✅ Auth/AuthZ audited:
    • Membership check before any read
    • Role check (owner/admin) before write
    • User token used to call GitHub (server-side only)
  • ✅ Open-source readiness: no internal URLs, no hardcoded account IDs, no secrets

Test plan

# Backend
npm test -w projects-lambda
npm test  # runs all 153 tests in the workspace

# Frontend
cd frontend && npm run build

E2E manual flow on staging:

  1. Create project with single repo (legacy path) → Repository vertex auto-created
  2. Settings → Repositories → Add 2 more repos via multi-select
  3. Verify each repo gets a role + detected stack
  4. Remove a repo → confirm Neptune edge deleted

eipasteur added 9 commits May 21, 2026 12:05
Add ability to link multiple repositories to a project with automatic
tech stack detection via GitHub API. Each repository is stored as a
Neptune vertex connected via HAS_REPO edges.

- Add CRUD endpoints for /projects/{projectId}/repos
- Detect languages and frameworks from repo root files
- Lazy migration: existing git_repo field auto-creates Repository vertex
- Support both legacy single-repo and new multi-repo project creation
- Add DynamoDB access for reading user GitHub tokens
- Add frontend API client types and service methods
# Conflicts:
#	frontend/src/services/projects.ts
#	lambda/projects/index.js
PR #176 migrated lambda/github from CommonJS (require('./shared/...')) to
ESM (import '../shared/response.js'), but the Terraform packaging copied
shared/ at the same level as index.js in the zip. At runtime,
'../shared/response.js' resolved to /var/shared/response.js (one level
above /var/task/), which does not exist, causing ERR_MODULE_NOT_FOUND
on every invocation.

Apply the same pattern as PR #180 (github-issues): build the lambda with
esbuild via the npm workspace and zip the .build output. esbuild inlines
the shared/ modules at build time, eliminating runtime path resolution.

- terraform/modules/api/lambda/main.tf:
  - Replace npm_requirements + prefix_in_zip='shared' with the build/:zip
    pattern, mirroring github_issues_lambda
  - Bump runtime to nodejs24.x for consistency with the build target
    (esbuild --target=node24, package.json already targets node24)

Tests (153 unit tests across 7 files) continue to pass since they import
'../index.js' which resolves to lambda/github/index.js, whose original
'../shared/response.js' import resolves correctly inside the source tree
at lambda/shared/response.js (the same path esbuild uses to bundle).

Closes the runtime regression introduced in #176.
Two layered runtime regressions blocked /api/github/* on staging:

1. PR #176 migrated lambda/github from CommonJS to ESM but the Terraform
   packaging used npm_requirements + prefix_in_zip='shared'. The ESM
   resolver dereferenced '../shared/response.js' to /var/shared/...
   (one level above /var/task/), which doesn't exist, throwing
   ERR_MODULE_NOT_FOUND on every cold start.

2. shared/git-token.js is CommonJS and does require('@aws-sdk/client-ssm')
   at the module top level. esbuild bundles a CJS shim around it, but
   the Node.js ESM runtime rejects the resulting dynamic require:
   'Dynamic require of @aws-sdk/client-ssm is not supported'.

Fix:
- terraform/modules/api/lambda/main.tf: switch github_lambda from
  npm_requirements to the build/:zip pattern (mirroring github_issues
  per PR #180), bumping runtime to nodejs24.x to match the esbuild
  --target=node24 the workspace already uses.
- lambda/github/index.js: inline resolveGitToken locally, mirroring the
  exact pattern adopted by lambda/github-issues. shared/response.js
  remains imported because esbuild can statically bundle it without
  hitting a dynamic require (it has no top-level require of an external
  package).

Validated end-to-end on staging:
- terraform apply succeeded (no drift)
- aws lambda invoke returns statusCode 200 with valid JSON body
  ({connected: false}) for /api/github/status
- 153 unit tests across the repo continue to pass

Closes the runtime regression introduced in #176.
Complete the multi-repo feature by adding the frontend UI to
list/add/remove repositories on a project. Backend was already
shipped (CRUD endpoints on /projects/{projectId}/repos, Repository
vertices in Neptune via HAS_REPO edges).

- GitHubRepoSelect: support multi-select mode with checkbox UI,
  search filter, and exclude list (to hide repos already linked).
  Single-select mode is preserved for backward compatibility via
  a discriminated union on the multiple prop.

- ProjectSettings: replace the single gitRepo input with a
  Repositories section that lists all linked repos (URL, role,
  detected stack), supports bulk add via the multi-select picker
  with parallel addRepo calls (Promise.allSettled), and per-repo
  remove with confirmation.

CreateProjectModal stays single-repo for now: users create a
project with one repo and add additional repos from settings.
This keeps the create flow simple and matches the GitLab-side
phasing (Phase 3 of multi-repo rollout).
Server-side validation guards against malformed input that could
land in URL templates downstream (api.github.com/repos/<url>/...).
Even though detectRepoStack only ever calls api.github.com (no SSRF
to arbitrary hosts), accepting non-conforming inputs leaks 400-class
errors as 5xx and pollutes the audit log.

Pattern follows GitHub's published constraints:
- owner: 1-39 chars, alphanumeric + hyphens (no leading hyphen)
- repo:  1-100 chars, alphanumeric + hyphen/underscore/dot

Returns 400 with explicit message on mismatch.
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