Skip to content

Commit 8279ef7

Browse files
committed
feat: fetch well-known files via tree listings, retire REST security probe
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
1 parent 44ec9d8 commit 8279ef7

2 files changed

Lines changed: 28 additions & 66 deletions

File tree

services/apps/packages_worker/src/enricher/fetchLightRepo.ts

Lines changed: 27 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { getServiceChildLogger } from '@crowd/logging'
22

33
import { FetchError, LightRepoResult } from './types'
4+
import {
5+
RepoTrees,
6+
TreeEntryNode,
7+
classifyWellKnownFiles,
8+
deriveSecurityFileEnabled,
9+
} from './wellKnownFiles'
410

511
const log = getServiceChildLogger('fetch-light-repo')
612

@@ -24,6 +30,15 @@ const REPO_QUERY = `
2430
createdAt
2531
isSecurityPolicyEnabled
2632
defaultBranchRef { name }
33+
rootTree: object(expression: "HEAD:") {
34+
... on Tree { entries { name type oid } }
35+
}
36+
githubTree: object(expression: "HEAD:.github") {
37+
... on Tree { entries { name type oid } }
38+
}
39+
docsTree: object(expression: "HEAD:docs") {
40+
... on Tree { entries { name type oid } }
41+
}
2742
}
2843
}
2944
`
@@ -34,67 +49,6 @@ export function parseGithubUrl(url: string): { owner: string; name: string } {
3449
return { owner: match[1], name: match[2] }
3550
}
3651

37-
// community/profile API doesn't reliably return files.security — use Contents API instead.
38-
async function fetchSecurityFileEnabled(
39-
url: string,
40-
owner: string,
41-
name: string,
42-
token: string,
43-
timeoutMs: number,
44-
): Promise<boolean | null> {
45-
const headers = { Authorization: `bearer ${token}`, Accept: 'application/vnd.github+json' }
46-
const check = async (path: string): Promise<boolean> => {
47-
const controller = new AbortController()
48-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
49-
try {
50-
const response = await fetch(`${GITHUB_API_URL}/repos/${owner}/${name}/contents/${path}`, {
51-
headers,
52-
signal: controller.signal,
53-
})
54-
if (response.status === 200) return true
55-
if (response.status === 404) return false
56-
if (response.status === 403) {
57-
const body = await response.text()
58-
if (body.toLowerCase().includes('rate limit')) {
59-
// REST secondary limits send retry-after; primary limits send x-ratelimit-reset
60-
const retryAfterSec = parseInt(response.headers.get('retry-after') ?? '0', 10)
61-
const resetSec = parseInt(response.headers.get('x-ratelimit-reset') ?? '0', 10)
62-
const resetMs = retryAfterSec
63-
? Date.now() + retryAfterSec * 1000
64-
: resetSec
65-
? resetSec * 1000 + 5_000
66-
: Date.now() + 65_000
67-
throw new FetchError('RATE_LIMIT', `Contents API rate limited on ${path}`, resetMs)
68-
}
69-
}
70-
throw new Error(`Unexpected status ${response.status} for ${path}`)
71-
} finally {
72-
clearTimeout(timeoutId)
73-
}
74-
}
75-
76-
try {
77-
const [root, dotGithub] = await Promise.all([
78-
check('SECURITY.md'),
79-
check('.github/SECURITY.md'),
80-
])
81-
return root || dotGithub
82-
} catch (err) {
83-
// Rate limits propagate so the caller can park the installation and requeue the repo
84-
if (err instanceof FetchError && err.kind === 'RATE_LIMIT') throw err
85-
log.warn(
86-
{
87-
url,
88-
errName: (err as Error).name,
89-
errMsg: (err as Error).message,
90-
errStack: (err as Error).stack,
91-
},
92-
'Security file check failed — securityFileEnabled will be null',
93-
)
94-
return null
95-
}
96-
}
97-
9852
interface BranchProtection {
9953
enabled: boolean | null
10054
requiredReviews: number | null
@@ -214,6 +168,9 @@ interface RepoGraphqlResponse {
214168
createdAt: string
215169
isSecurityPolicyEnabled: boolean
216170
defaultBranchRef: { name: string } | null
171+
rootTree: { entries?: TreeEntryNode[] } | null
172+
githubTree: { entries?: TreeEntryNode[] } | null
173+
docsTree: { entries?: TreeEntryNode[] } | null
217174
} | null
218175
}
219176
errors?: Array<{ type?: string; message?: string }>
@@ -262,10 +219,7 @@ export async function fetchLightRepo(
262219
if (response.status === 404) throw new FetchError('NOT_FOUND', `404 for ${url}`)
263220
if (response.status >= 500) throw new FetchError('TRANSIENT', `${response.status} for ${url}`)
264221

265-
const [json, securityFileEnabled] = await Promise.all([
266-
response.json() as Promise<RepoGraphqlResponse>,
267-
fetchSecurityFileEnabled(url, owner, name, token, timeoutMs),
268-
])
222+
const json = (await response.json()) as RepoGraphqlResponse
269223

270224
if (json.errors?.length) {
271225
const err = json.errors[0]
@@ -280,6 +234,12 @@ export async function fetchLightRepo(
280234
const repo = json.data?.repository
281235
if (!repo) throw new FetchError('NOT_FOUND', `No repository data for ${url}`)
282236

237+
const trees: RepoTrees = {
238+
root: repo.rootTree?.entries ?? null,
239+
github: repo.githubTree?.entries ?? null,
240+
docs: repo.docsTree?.entries ?? null,
241+
}
242+
283243
const defaultBranch = repo.defaultBranchRef?.name ?? null
284244
const branchProtection = defaultBranch
285245
? await fetchBranchProtection(url, owner, name, defaultBranch, token, timeoutMs)
@@ -305,7 +265,8 @@ export async function fetchLightRepo(
305265
isFork: repo.isFork ?? null,
306266
createdAt: repo.createdAt ?? null,
307267
securityPolicyEnabled: repo.isSecurityPolicyEnabled ?? null,
308-
securityFileEnabled,
268+
securityFileEnabled: deriveSecurityFileEnabled(trees),
269+
wellKnownFiles: classifyWellKnownFiles(trees),
309270
branchProtectionEnabled: branchProtection.enabled,
310271
branchProtectionRequiredReviews: branchProtection.requiredReviews,
311272
branchProtectionRequiresStatusChecks: branchProtection.requiresStatusChecks,

services/apps/packages_worker/src/enricher/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface LightRepoResult {
1919
createdAt: string | null
2020
securityPolicyEnabled: boolean | null
2121
securityFileEnabled: boolean | null
22+
wellKnownFiles: WellKnownFileEntry[]
2223
branchProtectionEnabled: boolean | null
2324
branchProtectionRequiredReviews: number | null
2425
branchProtectionRequiresStatusChecks: boolean | null

0 commit comments

Comments
 (0)