Skip to content

fix(careers): scan the repo for install locations instead of guessing - #189

Merged
ralyodio merged 1 commit into
masterfrom
feat/careers-install-candidates
Aug 4, 2026
Merged

ralyodio merged 1 commit into
masterfrom
feat/careers-install-candidates

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

The bug

On /projects/:id/stats/careers, "Add to my repo" → Check repo blind-probes ten hardcoded paths at a single root:

astro.config.{mjs,ts,js,cjs}
app/layout.{tsx,jsx,js}
src/app/layout.{tsx,jsx,js}

Any other layout — a site at web/, frontend/, packages/site/, or any monorepo path the customer didn't type into the subdirectory box — returns null and the UI says "No Next.js App Router or Astro site found there." That reads as a missing feature rather than a missed guess.

The stats tracker installer already solved this. findInstallCandidates scans, ranks, and hands the UI a picker with a manual-path escape hatch. This brings the careers flow to the same shape.

Changes

  • lib/github/repos.ts — new listRepoTree: one recursive git-trees request for the whole file list. The contents API can only confirm paths you already know.
  • lib/github/install-careers.ts
    • careersCandidatesFromTree derives every possible route directory from that listing and ranks it with the tracker's heuristics (apps/ and sites/ up, examples//fixtures down, root-level app first). Nested route layouts (app/blog/layout.tsx, app/(marketing)/layout.tsx) are correctly ignored — only a root layout marks an app dir.
    • findCareersCandidates wraps it and still falls back to the direct probe when the tree is unavailable or GitHub truncated it, so a huge repo degrades to the old behaviour rather than claiming the site is missing.
    • Locations that already have a careers page are listed and flagged, not hidden — "you already have this" beats an empty list.
    • verifyCareersDir re-probes for the framework marker before any write.
  • app/api/.../install-careers/route.ts — mode=candidates (read-only: no PR, no project_pr_runs row) and target_dir on submit.
  • careers-install.tsx — the ranked list replaces the single detect line, best match preselected, with a manual directory box for truncated repos. Button is now Scan repo / Rescan.

Security note

A directory chosen in the browser is user input. submit never trusts target_dir: it re-probes for the framework marker server-side and 422s if there isn't one, and rejects .. traversal. The installer's promise — it only writes where it can positively see a site — is unchanged.

Testing

17 new cases in tests/careers-install-pr.test.ts covering monorepo discovery, ranking order, Astro config → pages mapping, nested-layout rejection, vendored-path skipping, rootPath filtering, existing-page flagging, the truncated/unavailable-tree fallbacks, and verifyCareersDir accept/reject/traversal.

  • npm test — 1374 passed, 7 skipped, 0 failures (112 files)
  • npm run typecheck — clean

Not verified against a live GitHub repo; the tree API interaction is covered by mocks only.

🤖 Generated with Claude Code

The careers PR flow blind-probed ten hardcoded paths at a single root
(astro.config.*, app/layout.*, src/app/layout.*). Anything else — a site
at web/, frontend/, packages/site/, or any monorepo path the customer
didn't type into the subdirectory box — came back "No Next.js App Router
or Astro site found there", which reads as a missing feature rather than
a missed guess.

The stats tracker installer already solved this: findInstallCandidates
scans, ranks, and hands the UI a picker with a manual-path escape hatch.
This brings the careers flow to the same shape.

- repos: add listRepoTree, one recursive git-trees request for the whole
  file list. The contents API can only confirm paths you already know.
- install-careers: careersCandidatesFromTree derives every possible route
  directory from that listing and ranks it with the tracker's heuristics
  (apps/ and sites/ up, examples/fixtures down, root-level app first).
  findCareersCandidates wraps it and still falls back to the direct probe
  when the tree is unavailable or GitHub truncated it, so a huge repo
  degrades to the old behaviour rather than claiming the site is missing.
  Locations that already have a careers page are listed and flagged, not
  hidden — "you already have this" beats an empty list.
- route: add mode=candidates (read-only, no PR, no run row) and accept
  target_dir on submit. A directory chosen in the browser is user input,
  so verifyCareersDir re-probes for the framework marker before any
  write; the installer's promise is that it only writes where it can
  positively see a site.
- ui: replace the single detect line with the ranked list, preselecting
  the best match, plus a manual directory box for truncated repos.

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

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

54 finding(s)

HIGH/CRITICAL: 10 | MEDIUM: 44

Severity Rule Location
HIGH secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
HIGH js-ssrf-outbound-request lib/sp/platforms/facebook.ts:115
HIGH secret-generic-credential lib/sp/platforms/linkedin.ts:25
HIGH js-ssrf-outbound-request lib/sp/platforms/telegram.ts:63
HIGH js-ssrf-outbound-request lib/sp/platforms/threads.ts:138
HIGH manifest-typosquat package.json:59
HIGH secret-generic-credential tests/contract/coinpay.test.ts:4
HIGH secret-generic-credential tests/contract/posthog-integration.test.ts:13
HIGH secret-generic-credential tests/lead-campaign.test.ts:16
MEDIUM js-unescaped-html-sink app/(app)/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM sql-template-interpolation app/(app)/projects/[id]/autoblog/actions.tsx:96
MEDIUM js-unescaped-html-sink app/(app)/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM sql-template-interpolation app/(app)/projects/[id]/autoblog/setup/form.tsx:504
MEDIUM sql-template-interpolation app/(app)/projects/[id]/uptime/monitor-actions.tsx:28
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM sql-template-interpolation app/actions/admin.ts:114
MEDIUM sql-template-interpolation app/actions/orgs.ts:328
MEDIUM sql-template-interpolation app/api/lx/keywords/regenerate/route.ts:59
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:201
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM sql-template-interpolation lib/audit/checks/security.ts:48
MEDIUM redos-nested-quantifier lib/careers/jobs.ts:139
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:130
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:93
MEDIUM sql-template-interpolation lib/lx/articleGen.ts:367
MEDIUM sql-template-interpolation lib/lx/articleGen.ts:379
MEDIUM sql-template-interpolation lib/lx/articleGen.ts:380
MEDIUM sql-template-interpolation lib/lx/articleGen.ts:1340
MEDIUM sql-template-interpolation lib/lx/articleGen.ts:1362
MEDIUM sql-template-interpolation lib/lx/guestPostGen.ts:109
MEDIUM tls-verification-disabled lib/onion.ts:47
MEDIUM sql-template-interpolation lib/sp/platforms/linkedin.ts:177
MEDIUM sql-template-interpolation scripts/delete-archived-projects.mjs:97

…and 4 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio marked this pull request as ready for review August 4, 2026 08:40
@ralyodio
ralyodio merged commit 2fbc90d into master Aug 4, 2026
8 checks passed
@ralyodio
ralyodio deleted the feat/careers-install-candidates branch August 4, 2026 08:40
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