Detect a tech stack from a GitHub repo URL
By Flavio Copes
Detect a project's tech stack from a public GitHub repo without cloning. GitHub API branch lookup, parallel raw manifest fetches, and signal mapping.
Paste a GitHub URL. Get back framework, database hints, deploy targets — without cloning the repo.
StackPlan does this to prefill its intake form. The scan runs on Cloudflare Workers. No git binary. Just HTTP.
Parse the URL safely
Never trust raw input. Parse with the URL constructor, allow bare github.com/owner/repo, strip a .git suffix, and reject anything that isn’t github.com:
function parseGitHubUrl(raw) {
const trimmed = raw.trim()
if (!trimmed) return null
let url
try {
url = new URL(trimmed.includes('://') ? trimmed : `https://${trimmed}`)
} catch {
return null
}
if (url.hostname !== 'github.com' && url.hostname !== 'www.github.com') {
return null
}
const parts = url.pathname.replace(/^\/+|\/+$/g, '').split('/')
if (parts.length < 2) return null
const owner = parts[0]
const repo = parts[1].replace(/\.git$/i, '')
if (!owner || !repo) return null
return { owner, repo }
}
Reject dot-only repo names. They would break the raw file URL path.
Resolve the default branch
Manifest files live on a branch. main isn’t always right.
Hit the GitHub REST API once:
async function resolveDefaultBranch(owner, repo) {
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
headers: {
Accept: 'application/vnd.github+json',
'User-Agent': 'MyApp-RepoScan',
},
})
if (!res.ok) return null
const data = await res.json()
return data.default_branch ?? null
}
Fall back to main, then master, if the API call fails. Try each branch until you find files.
Fetch manifests in parallel
Clone would be slow and heavy. raw.githubusercontent.com serves file contents over HTTPS:
https://raw.githubusercontent.com/{owner}/{repo}/{branch}/package.json
Pick a fixed list of high-signal paths: package.json, requirements.txt, go.mod, wrangler.toml, fly.toml, Dockerfile, prisma/schema.prisma, and so on. Cap how many you request — twelve is enough for most repos.
Fetch them all with Promise.all:
async function fetchRawManifest(owner, repo, branch, path, signal) {
const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`
const res = await fetch(url, { signal, headers: { Accept: 'text/plain' } })
if (!res.ok) return null
const text = await res.text()
return text.length > 0 && text.length < 512_000 ? text : null
}
const results = await Promise.all(
paths.map((path) => fetchRawManifest(owner, repo, branch, path, signal))
)
Sequential fetches hurt on 404-heavy repos. Parallel keeps the scan under a few seconds.
Wrap each fetch in a timeout with AbortController. Four seconds per file is plenty.
Map files to stack signals
This part is plain string matching. No LLM required.
From package.json, collect dependency names from dependencies, devDependencies, and peerDependencies. Check for next, astro, hono, @prisma/client, drizzle-orm, better-auth, and so on.
From requirements.txt, look for django, fastapi, celery. From go.mod, look for gin-gonic/gin. From config files, infer deploy targets — wrangler.toml means Cloudflare Workers, fly.toml means Fly.io.
Return a structured object:
{
language: 'JavaScript/TypeScript',
framework: 'Astro',
databases: ['PostgreSQL'],
orms: ['Drizzle'],
deployTargets: ['Cloudflare Workers'],
findings: ['Found package.json', 'Wrangler config found'],
}
Turn that into form defaults: app type, needs-database flags, a short assumptions list.
Rate limits and failures
Unauthenticated GitHub API calls allow sixty requests per hour per IP. A single scan uses one API call plus up to twelve raw fetches. Raw URLs don’t count against the REST limit the same way, but don’t hammer them in a loop.
If nothing comes back, tell the user the repo is empty, private, or mistyped. Don’t throw — return null and let the form stay manual.
An optional LLM pass can refine the app name from a text summary. The deterministic scan already gets you most of the way there.
That’s the whole pipeline: validate URL, resolve branch, parallel manifest fetch, rule-based detection. No clone, no GitHub App, no tarball download.
Related posts about node: