@preventive/triage 1.0.0-alpha.47 → 1.0.0-alpha.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/dependency-paths.d.ts +2 -1
- package/client/dependency-paths.js +16 -4
- package/common/managed/agent-tasks.ts +12 -14
- package/common/managed/github-agents.ts +7 -6
- package/out/activity.js +4 -4
- package/out/{chunk-NOZJULWS.js → chunk-6LNQTKAV.js} +1 -1
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/{chunk-5AHIQFFG.js → chunk-C2AVODGZ.js} +1 -1
- package/out/{chunk-I2MDGSQX.js → chunk-IO7JTZ6O.js} +1 -1
- package/out/chunk-JEAUAJVF.js +232 -0
- package/out/chunk-O6M574JI.js +1 -0
- package/out/{chunk-VL43BTSO.js → chunk-PBSOL2SL.js} +1 -1
- package/out/{chunk-ZCS4UYHI.js → chunk-PBZPYIG7.js} +1 -1
- package/out/{chunk-6WF3KNTL.js → chunk-SZD5CW5R.js} +2 -2
- package/out/{chunk-SPEFPML6.js → chunk-TQJDYHVE.js} +1 -1
- package/out/{chunk-UENXT2HK.js → chunk-ULIF7A7N.js} +1 -1
- package/out/{chunk-FH24VHSJ.js → chunk-WYLQWYLA.js} +1 -1
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +26 -26
- package/out/compare.js +13 -13
- package/out/dialogs.js +17 -17
- package/out/graph.js +1 -1
- package/out/index.html +7 -7
- package/out/scan.js +1 -1
- package/out/terminal.js +214 -214
- package/out/view.css +1 -1
- package/out/view.js +15 -15
- package/package.json +8 -5
- package/risk/npm/score.js +1 -1
- package/server-managed/agent-merges.ts +38 -22
- package/server-managed/app-agents.ts +6 -8
- package/server-managed/cache-storage.ts +3 -2
- package/server-managed/commit-coauthors.ts +7 -2
- package/server-managed/commit-lockfiles.ts +22 -21
- package/server-managed/db-methods.ts +52 -38
- package/server-managed/db-neon.ts +21 -5
- package/server-managed/db.ts +2 -0
- package/server-managed/dependabot-access.ts +1 -1
- package/server-managed/dependency-review-comments.ts +7 -4
- package/server-managed/dependency-reviews.ts +71 -33
- package/server-managed/github-accounts.ts +28 -1
- package/server-managed/github-app.ts +27 -23
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +8 -1
- package/server-managed/github-metadata.ts +2 -2
- package/server-managed/http-response.ts +28 -10
- package/server-managed/http.ts +751 -273
- package/server-managed/index.ts +2 -2
- package/server-managed/managed-issues.ts +3 -0
- package/server-managed/npm-delta.ts +16 -0
- package/server-managed/npm-kept.ts +32 -5
- package/server-managed/npm-loads.ts +21 -13
- package/server-managed/npm-manifest.ts +1 -1
- package/server-managed/npm-packages.ts +13 -4
- package/server-managed/object-storage.ts +4 -3
- package/server-managed/public-workspace.ts +3 -3
- package/server-managed/pull-comment-reviews.ts +1 -1
- package/server-managed/pull-dependencies.ts +84 -36
- package/server-managed/pull-dependency-reviews.ts +28 -18
- package/server-managed/rename-diff-worker.js +1 -1
- package/server-managed/repository-agents.ts +9 -6
- package/server-managed/repository-browser.ts +13 -3
- package/server-managed/repository-code.ts +153 -7
- package/server-managed/repository-dependency-reviews.ts +12 -4
- package/server-managed/repository-history.ts +85 -152
- package/server-managed/repository-issues.ts +27 -13
- package/server-managed/repository-pulse.ts +31 -11
- package/server-managed/repository-squashes.ts +145 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +3 -2
- package/server-managed/session.ts +34 -12
- package/server-managed/sql.ts +16 -8
- package/server-managed/storage-encryption.ts +16 -8
- package/server-managed/storage-maintenance.ts +5 -2
- package/server-managed/storage-payload.ts +18 -0
- package/server-managed/storage-stores.ts +3 -2
- package/server-managed/team-feed.ts +16 -11
- package/server-managed/team-npm-scopes.ts +29 -0
- package/server-managed/team-reports.ts +68 -16
- package/out/chunk-6ANSHRFQ.js +0 -6
- package/out/chunk-S6U2J62U.js +0 -232
- package/out/chunk-ZGNMDGSB.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preventive/triage",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.49",
|
|
4
4
|
"description": "Client & relay server for triaging of automated reports",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": {
|
|
@@ -130,6 +130,7 @@
|
|
|
130
130
|
"server-managed/github-actions.ts",
|
|
131
131
|
"server-managed/github-app.ts",
|
|
132
132
|
"server-managed/github-errors.ts",
|
|
133
|
+
"server-managed/github-graphql.ts",
|
|
133
134
|
"server-managed/github-metadata.ts",
|
|
134
135
|
"server-managed/github-issue-oauth.ts",
|
|
135
136
|
"server-managed/github-issues.ts",
|
|
@@ -187,9 +188,11 @@
|
|
|
187
188
|
"server-managed/repository-changes.ts",
|
|
188
189
|
"server-managed/repository-code.ts",
|
|
189
190
|
"server-managed/repository-dependency-reviews.ts",
|
|
191
|
+
"server-managed/request-timing.ts",
|
|
190
192
|
"server-managed/reviewed-dependencies.ts",
|
|
191
193
|
"server-managed/repository-agents.ts",
|
|
192
194
|
"server-managed/repository-history.ts",
|
|
195
|
+
"server-managed/repository-squashes.ts",
|
|
193
196
|
"server-managed/repository-issues.ts",
|
|
194
197
|
"server-managed/repository-pulse.ts",
|
|
195
198
|
"server-managed/repository-aliases.ts",
|
|
@@ -401,16 +404,16 @@
|
|
|
401
404
|
"typescript": "^7.0.2"
|
|
402
405
|
},
|
|
403
406
|
"dependencies": {
|
|
404
|
-
"@exodus/stasis": "1.0.0-beta.
|
|
405
|
-
"@exodus/stasis-core": "1.0.0-beta.
|
|
406
|
-
"@preventive/archive": "^1.
|
|
407
|
+
"@exodus/stasis": "1.0.0-beta.11",
|
|
408
|
+
"@exodus/stasis-core": "1.0.0-beta.11",
|
|
409
|
+
"@preventive/archive": "^1.2.0",
|
|
407
410
|
"@preventive/compare": "^1.2.11",
|
|
408
411
|
"@preventive/diff": "^1.1.0",
|
|
409
412
|
"@preventive/lockfile": "^1.2.0",
|
|
410
413
|
"@preventive/publishers": "^1.1.0",
|
|
411
414
|
"@preventive/report": "^1.0.0",
|
|
412
415
|
"@preventive/sourcemap": "1.0.0-alpha.1",
|
|
413
|
-
"@preventive/upstream": "^1.
|
|
416
|
+
"@preventive/upstream": "^1.3.0",
|
|
414
417
|
"css-tree": "^3.2.1",
|
|
415
418
|
"oxc-minify": "^0.152.0",
|
|
416
419
|
"oxc-parser": "^0.152.0",
|
package/risk/npm/score.js
CHANGED
|
@@ -7,7 +7,7 @@ import { DAY_MS } from './downloads.js'
|
|
|
7
7
|
// How much each finding raises the score, by its severity: a version's
|
|
8
8
|
// findings combined as independent chances are (1 − Π(1 − weight)), so that
|
|
9
9
|
// each adds less the more there are, none past 100. One red finding alone is
|
|
10
|
-
// 50; two,
|
|
10
|
+
// 50; two are 75, and one with two orange 68, as attacks show.
|
|
11
11
|
const WEIGHTS = { critical: 0.5, high: 0.2, moderate: 0.08, low: 0.03, notice: 0 }
|
|
12
12
|
// What the version's age tells instead (`too-new`), as exposure does below.
|
|
13
13
|
const TOLD_BY_AGE = new Set(['too-new'])
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// pull request's branch alone naming it (app-agents.ts
|
|
5
5
|
// AGENT_BRANCH_PREFIXES), or a label one who ran it gave it naming the agent
|
|
6
6
|
// (agent-tasks.ts labelAgents), as the Agents tab tells an agent's work; or
|
|
7
|
-
// its author, an account agents work as (github-agents.ts AGENT_OPERATED)
|
|
7
|
+
// its author, an account agents work as (github-agents.ts AGENT_OPERATED),
|
|
8
|
+
// or any GitHub App's bot.
|
|
8
9
|
import { labelAgents } from '../common/managed/agent-tasks.ts'
|
|
9
10
|
import { GITHUB_AGENTS, agentOperated } from '../common/managed/github-agents.ts'
|
|
10
|
-
import { AGENT_BRANCH_PREFIXES
|
|
11
|
+
import { AGENT_BRANCH_PREFIXES } from './app-agents.ts'
|
|
11
12
|
import { type HistoryCommit, githubGraphql } from './repository-history.ts'
|
|
12
13
|
|
|
13
14
|
// The merged pull requests read an answer, a page a query, as many queries
|
|
@@ -22,7 +23,7 @@ const MERGED_LABELS = 100
|
|
|
22
23
|
// How long before or after a pull request rebased onto the branch was merged
|
|
23
24
|
// its copies are committed.
|
|
24
25
|
const REBASE_SKEW_MS = 10 * 60_000
|
|
25
|
-
const LABEL_AGENTS =
|
|
26
|
+
const LABEL_AGENTS = Object.keys(GITHUB_AGENTS)
|
|
26
27
|
const SHA = /^[a-f\d]{40}$/u
|
|
27
28
|
|
|
28
29
|
const count = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0
|
|
@@ -36,14 +37,18 @@ const timeOf = (value: unknown) => {
|
|
|
36
37
|
// rebased onto the branch by their last commit there, when merged, and
|
|
37
38
|
// their commits' subjects, the oldest first (agentCommits); when the newest
|
|
38
39
|
// read was last updated, those updated since read again (`through`, null
|
|
39
|
-
// till all are read once)
|
|
40
|
-
//
|
|
41
|
-
//
|
|
40
|
+
// till all are read once), and where those read reach back no further than
|
|
41
|
+
// a period needed, when the oldest of them was updated (`reach`), every
|
|
42
|
+
// pull request merged since read (one is updated when merged, or after);
|
|
43
|
+
// and a read not finished: where it goes on (`after`), when the newest it
|
|
44
|
+
// began with was updated (`from`), the oldest it reads to (`stop`), how many
|
|
45
|
+
// it has read (`read`) and when the oldest of them was updated (`to`). And
|
|
46
|
+
// of every pull
|
|
42
47
|
// request merged, an agent's or not, its merge commit and when (`pulls`),
|
|
43
48
|
// and one rebased so (`pullRebases`), for the commits pushed to the branch
|
|
44
49
|
// directly to be told (directCommits).
|
|
45
|
-
export interface AgentMerges { merges: [string, number][]; rebases: Rebase[]; through: number | null
|
|
46
|
-
next: { after: string; from: number; stop: number; read: number } | null; pulls?: [string, number][]; pullRebases?: Rebase[] }
|
|
50
|
+
export interface AgentMerges { merges: [string, number][]; rebases: Rebase[]; through: number | null; reach?: number
|
|
51
|
+
next: { after: string; from: number; stop: number; read: number; to?: number } | null; pulls?: [string, number][]; pullRebases?: Rebase[] }
|
|
47
52
|
// A pull request rebased onto the branch: its last commit there, when it was
|
|
48
53
|
// merged, its latest commits' subjects, the oldest first, and how many more
|
|
49
54
|
// it had than those read (`more`).
|
|
@@ -53,14 +58,14 @@ const MERGED_QUERY = `query($owner: String!, $name: String!, $after: String) {
|
|
|
53
58
|
repository(owner: $owner, name: $name) {
|
|
54
59
|
pullRequests(states: MERGED, first: ${MERGED_PAGE}, after: $after, orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
55
60
|
pageInfo { hasNextPage endCursor }
|
|
56
|
-
nodes { updatedAt mergedAt headRefName author { login ... on User { databaseId } } labels(first: ${MERGED_LABELS}) { nodes { name } }
|
|
61
|
+
nodes { updatedAt mergedAt headRefName author { __typename login ... on User { databaseId } } labels(first: ${MERGED_LABELS}) { nodes { name } }
|
|
57
62
|
mergeCommit { oid parents { totalCount } }
|
|
58
63
|
commits(last: ${MERGED_COMMITS}) { totalCount nodes { commit { oid messageHeadline changedFilesIfAvailable } } } }
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
}`
|
|
62
67
|
type MergedPage = { repository?: { pullRequests?: { pageInfo?: { hasNextPage?: unknown; endCursor?: unknown }
|
|
63
|
-
nodes?: ({ updatedAt?: unknown; mergedAt?: unknown; headRefName?: unknown; author?: { login?: unknown; databaseId?: unknown } | null
|
|
68
|
+
nodes?: ({ updatedAt?: unknown; mergedAt?: unknown; headRefName?: unknown; author?: { __typename?: unknown; login?: unknown; databaseId?: unknown } | null
|
|
64
69
|
labels?: { nodes?: ({ name?: unknown } | null)[] } | null
|
|
65
70
|
mergeCommit?: { oid?: unknown; parents?: { totalCount?: unknown } | null } | null
|
|
66
71
|
commits?: { totalCount?: unknown; nodes?: ({ commit?: { oid?: unknown; messageHeadline?: unknown; changedFilesIfAvailable?: unknown } | null } | null)[] } | null
|
|
@@ -68,9 +73,10 @@ type MergedPage = { repository?: { pullRequests?: { pageInfo?: { hasNextPage?: u
|
|
|
68
73
|
const agentBranch = (name: unknown) => typeof name === 'string' && AGENT_BRANCH_PREFIXES.some(prefix => name.startsWith(prefix))
|
|
69
74
|
type MergedNode = NonNullable<NonNullable<NonNullable<NonNullable<MergedPage['repository']>['pullRequests']>['nodes']>[number]>
|
|
70
75
|
// Whether a pull request is an agent's: by its branch, a label naming one,
|
|
71
|
-
// or its author, an account agents work as
|
|
76
|
+
// or its author, an account agents work as or a bot (GitHub's GraphQL API
|
|
77
|
+
// naming a bot by its app's login alone, its type telling it).
|
|
72
78
|
const agentPull = ({ headRefName, labels, author }: MergedNode) => agentBranch(headRefName)
|
|
73
|
-
|| labelAgents((labels?.nodes ?? []).map(label => label?.name), LABEL_AGENTS).length > 0
|
|
79
|
+
|| labelAgents((labels?.nodes ?? []).map(label => label?.name), LABEL_AGENTS).length > 0 || author?.__typename === 'Bot'
|
|
74
80
|
|| agentOperated(typeof author?.login === 'string' ? author.login : null, Number.isSafeInteger(author?.databaseId) ? author!.databaseId as number : null)
|
|
75
81
|
|
|
76
82
|
// The commits the pull requests merged since `since` from an agent's branch,
|
|
@@ -84,20 +90,24 @@ const agentPull = ({ headRefName, labels, author }: MergedNode) => agentBranch(h
|
|
|
84
90
|
// first (a pull request is updated when merged, or after), on from what was
|
|
85
91
|
// `kept`: till one updated before the year, or before the newest read before
|
|
86
92
|
// was, as many queries an answer as MERGED_QUERIES, a read not finished
|
|
87
|
-
// going on from where it was left by the next
|
|
88
|
-
//
|
|
93
|
+
// going on from where it was left by the next, or once those merged since
|
|
94
|
+
// `need` are all read; one read again whose label no longer names an agent,
|
|
95
|
+
// what it was read to bring forgotten.
|
|
89
96
|
export async function agentMerges(fullName: string, token: string, since: number, kept: AgentMerges | null,
|
|
90
|
-
fetchImpl: typeof fetch = globalThis.fetch): Promise<AgentMerges> {
|
|
97
|
+
fetchImpl: typeof fetch = globalThis.fetch, need = since): Promise<AgentMerges> {
|
|
91
98
|
const [owner, name] = fullName.split('/') as [string, string]
|
|
92
99
|
const merges = new Map((kept?.merges ?? []).filter(([, at]) => at >= since))
|
|
93
100
|
const rebases = new Map((kept?.rebases ?? []).filter(([, at]) => at >= since).map(([sha, at, subjects, more = 0]) => [sha, [at, subjects, more] as const]))
|
|
94
101
|
const pulls = new Map((kept?.pulls ?? []).filter(([, at]) => at >= since))
|
|
95
102
|
const pullRebases = new Map((kept?.pullRebases ?? []).filter(([, at]) => at >= since).map(([sha, at, subjects, more = 0]) => [sha, [at, subjects, more] as const]))
|
|
96
|
-
|
|
103
|
+
// Those read reaching back no further than they needed, and `need`
|
|
104
|
+
// further: read again from the newest on, as far back as it needs.
|
|
105
|
+
const further = !kept?.next && kept?.reach !== undefined && kept.reach >= need
|
|
106
|
+
const stop = kept?.next?.stop ?? (further ? since : Math.max(since, kept?.through ?? since))
|
|
97
107
|
const listed = (map: typeof rebases) => [...map].map(([sha, [at, subjects, more]]): Rebase => more > 0 ? [sha, at, subjects, more] : [sha, at, subjects])
|
|
98
|
-
const result = (through: number | null, next: AgentMerges['next']): AgentMerges => ({ merges: [...merges], rebases: listed(rebases), through,
|
|
99
|
-
pulls: [...pulls], pullRebases: listed(pullRebases) })
|
|
100
|
-
let { after = null, from = null, read = 0 }: { after?: string | null; from?: number | null; read?: number } = kept?.next ?? {}
|
|
108
|
+
const result = (through: number | null, next: AgentMerges['next'], reach?: number): AgentMerges => ({ merges: [...merges], rebases: listed(rebases), through,
|
|
109
|
+
...reach === undefined ? {} : { reach }, next, pulls: [...pulls], pullRebases: listed(pullRebases) })
|
|
110
|
+
let { after = null, from = null, read = 0, to = null }: { after?: string | null; from?: number | null; read?: number; to?: number | null } = kept?.next ?? {}
|
|
101
111
|
for (let queries = 0; queries < MERGED_QUERIES; queries++) {
|
|
102
112
|
const page = (await githubGraphql(MERGED_QUERY, { owner, name, after }, token, fetchImpl) as MergedPage | null)?.repository?.pullRequests
|
|
103
113
|
let done = page?.pageInfo?.hasNextPage !== true || typeof page.pageInfo.endCursor !== 'string'
|
|
@@ -106,6 +116,7 @@ export async function agentMerges(fullName: string, token: string, since: number
|
|
|
106
116
|
if (updated === null) continue
|
|
107
117
|
if (updated < stop) { done = true; break }
|
|
108
118
|
from ??= updated
|
|
119
|
+
to = updated
|
|
109
120
|
read++
|
|
110
121
|
const [merged, merge] = [timeOf(node?.mergedAt), node?.mergeCommit?.oid]
|
|
111
122
|
if (!node || merged === null || merged < since || typeof merge !== 'string' || !SHA.test(merge)) continue
|
|
@@ -129,10 +140,15 @@ export async function agentMerges(fullName: string, token: string, since: number
|
|
|
129
140
|
for (const oid of brought) merges.set(oid, merged)
|
|
130
141
|
if (subjects) rebases.set(merge, [merged, subjects, more])
|
|
131
142
|
}
|
|
132
|
-
|
|
143
|
+
// Read as far back as it reads to: the year's start, every one since
|
|
144
|
+
// read; else as far as those read before reached.
|
|
145
|
+
if (done) return result(from ?? stop, null, stop > since ? kept?.reach : undefined)
|
|
133
146
|
after = page!.pageInfo!.endCursor as string
|
|
147
|
+
// Read as far back as `need`: finished, so far back, those updated
|
|
148
|
+
// since read by the next as ever.
|
|
149
|
+
if (need > since && to !== null && to < need) return result(Math.max(from ?? stop, kept?.through ?? Number.NEGATIVE_INFINITY), null, to)
|
|
134
150
|
}
|
|
135
|
-
return result(kept?.through ?? null, { after: after!, from: from ?? stop, stop, read })
|
|
151
|
+
return result(kept?.through ?? null, { after: after!, from: from ?? stop, stop, read, ...to === null ? {} : { to } }, kept?.reach)
|
|
136
152
|
}
|
|
137
153
|
|
|
138
154
|
// Whether two reads of the pull requests merged (agentMerges) bring the
|
|
@@ -152,7 +168,7 @@ const headed = (subject: string, headline: string) => headline.endsWith('…') ?
|
|
|
152
168
|
// (`read`, agentMerges): those it names, and of one rebased, its commits'
|
|
153
169
|
// copies, the last and those below it on their first parents, as long as
|
|
154
170
|
// each one's subject is that of one of its commits older than the last
|
|
155
|
-
// matched, as Commits tells a rebase (repository-
|
|
171
|
+
// matched, as Commits tells a rebase (repository-squashes.ts squashedPulls),
|
|
156
172
|
// past those of its commits the rebase left none of (an empty one GitHub
|
|
157
173
|
// counted no files of), and, past them all, as many more as it had before
|
|
158
174
|
// its latest read; a squash merge's one commit alone, its subject the pull
|
|
@@ -6,14 +6,12 @@ import { GITHUB_AGENTS, type GithubAgent } from '../common/managed/github-agents
|
|
|
6
6
|
|
|
7
7
|
// The agents known by their GitHub Apps or accounts, beside Claude and
|
|
8
8
|
// Codex: those of GITHUB_AGENTS with branches of their own, in its order
|
|
9
|
-
// (common/managed/github-agents.ts, where each is told
|
|
10
|
-
// name, its avatar (its mark, served by
|
|
11
|
-
// App's, `app-<id>`), the fingerprint it
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.flatMap(([id, { name, avatar, accounts: [account], prefix, fingerprint }]) => prefix ? [{ id, name, prefix, ...avatar ? { avatar } : {},
|
|
16
|
-
...account?.id === undefined ? {} : { account: { id: account.id, logins: account.logins } }, ...fingerprint ? { fingerprint } : {} }] : [])
|
|
9
|
+
// (common/managed/github-agents.ts, where each is told, its accounts among
|
|
10
|
+
// it), each its id and name, its avatar (its mark, served by
|
|
11
|
+
// /api/avatar/<avatar>, a GitHub App's, `app-<id>`), the fingerprint it
|
|
12
|
+
// leaves, and its branches' prefix.
|
|
13
|
+
export const APP_AGENTS: readonly (AppAgent & AppAgentMarks)[] = Object.entries<GithubAgent>(GITHUB_AGENTS)
|
|
14
|
+
.flatMap(([id, { name, avatar, prefix, fingerprint }]) => prefix ? [{ id, name, prefix, ...avatar ? { avatar } : {}, ...fingerprint ? { fingerprint } : {} }] : [])
|
|
17
15
|
// The prefixes of the branches agents work on: Claude Code's and Codex's,
|
|
18
16
|
// and the app agents'.
|
|
19
17
|
export const AGENT_BRANCH_PREFIXES = ['claude/', 'codex/', ...APP_AGENTS.map(agent => agent.prefix)]
|
|
@@ -5,12 +5,13 @@ export class CacheMissError extends Error {
|
|
|
5
5
|
constructor() { super('Managed cache unavailable') }
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
// Keys are internal relative paths; delete removes a whole directory of
|
|
8
|
+
// Keys are internal relative paths; delete removes a whole directory of
|
|
9
|
+
// variants, but for one directory within it where `keep` names one.
|
|
9
10
|
export interface CacheStorage {
|
|
10
11
|
exists(key: string): Promise<boolean>
|
|
11
12
|
put(key: string, bytes: Buffer): Promise<void>
|
|
12
13
|
open(key: string): Promise<OpenedBlob>
|
|
13
|
-
delete(prefix: string): Promise<void>
|
|
14
|
+
delete(prefix: string, keep?: string): Promise<void>
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export function validateCacheKey(key: string): string {
|
|
@@ -10,7 +10,7 @@ export const MAX_COAUTHORS = 3
|
|
|
10
10
|
// The rules co-authors are read by, the agents' addresses and names among
|
|
11
11
|
// them: what was kept read by others (history pages, squashed pull requests)
|
|
12
12
|
// read again by these, once raised.
|
|
13
|
-
export const COAUTHORS_READ =
|
|
13
|
+
export const COAUTHORS_READ = 4
|
|
14
14
|
const MAX_NAME = 256
|
|
15
15
|
const textOf = (value: unknown, max: number) => typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : null
|
|
16
16
|
|
|
@@ -30,6 +30,11 @@ const FINGERPRINTED = APP_AGENTS.filter(agent => agent.fingerprint)
|
|
|
30
30
|
// commit gives (where it is read), and account.
|
|
31
31
|
export type Maker = { name: unknown; email?: unknown; account: NamedAccount }
|
|
32
32
|
|
|
33
|
+
// Those a commit's message names in its `Co-authored-by:` trailers
|
|
34
|
+
// (agent-tasks.ts trailerBlock), each by name and address.
|
|
35
|
+
export const coauthorTrailers = (message: unknown) => typeof message === 'string'
|
|
36
|
+
? [...trailerBlock(message.replaceAll('\r\n', '\n')).matchAll(COAUTHOR_TRAILER)].map(([, name, email]) => ({ name: name!, email: email! })) : []
|
|
37
|
+
|
|
33
38
|
// The agent one who made a commit is: by their account, else the address
|
|
34
39
|
// their commit names them by; else, GitHub naming no account for them, by
|
|
35
40
|
// their name.
|
|
@@ -50,7 +55,7 @@ const makerAgent = ({ name, email, account }: Maker): GithubAgentId | null => gi
|
|
|
50
55
|
// two people of one name with accounts or addresses of their own two.
|
|
51
56
|
export function coauthorsOf(message: unknown, author: Maker, listed: Maker[] | null = null): Coauthor[] {
|
|
52
57
|
const text = typeof message === 'string' ? message.replaceAll('\r\n', '\n') : ''
|
|
53
|
-
const named = listed ??
|
|
58
|
+
const named = listed ?? coauthorTrailers(text).map(({ name, email }) => ({ name, email, account: commitAccount({ email }) }))
|
|
54
59
|
const signals = agentSignals([text], FINGERPRINTED)
|
|
55
60
|
const marked = [...signals.claude || signals.sessions.length > 0 ? ['claude'] : [], ...signals.tasks.length > 0 || signals.codex ? ['codex'] : [],
|
|
56
61
|
...CURSOR_AGENT.test(text) ? ['cursor'] : [], ...signals.agents]
|
|
@@ -1,51 +1,52 @@
|
|
|
1
1
|
// The versions a repository's lockfiles of npm's ecosystem lock at a commit,
|
|
2
2
|
// as their review reads them (pull-dependency-reviews.ts): every lockfile
|
|
3
3
|
// there (findRepoLockfiles) a review holds to (`held`, lockfile-scope.ts),
|
|
4
|
-
// read as a pull request's are (pull-dependencies.ts readLockfile, kept
|
|
5
|
-
// this instance
|
|
6
|
-
// each's versions (`lockfiles`), and those its reader
|
|
7
|
-
// past MAX_LOCKFILES (`unread`), as many read at once
|
|
8
|
-
// those of some of them (lockedIn), each version once by
|
|
9
|
-
// are held to (lockedWith), dev only where it is wherever it
|
|
10
|
-
// GitHub having none of a lockfile it lists there fails the
|
|
11
|
-
// would for any file.
|
|
4
|
+
// read as a pull request's are (pull-dependencies.ts readLockfile), but kept
|
|
5
|
+
// by its blob, in this instance and beyond it (`kept`), as many as
|
|
6
|
+
// MAX_LOCKFILES of those, each's versions (`lockfiles`), and those its reader
|
|
7
|
+
// refuses, of Bun's, or past MAX_LOCKFILES (`unread`), as many read at once
|
|
8
|
+
// as READ_AT_ONCE; and those of some of them (lockedIn), each version once by
|
|
9
|
+
// what its reviews are held to (lockedWith), dev only where it is wherever it
|
|
10
|
+
// is locked. GitHub having none of a lockfile it lists there fails the
|
|
11
|
+
// whole, as it would for any file.
|
|
12
12
|
import { createClient } from '@preventive/upstream/github.js'
|
|
13
13
|
import { upstreamGithub } from './github-errors.ts'
|
|
14
14
|
import { GithubApiError } from './github-app.ts'
|
|
15
15
|
import type { LockedPackage } from './locked-packages.ts'
|
|
16
16
|
import { type Lru, lru } from './lru.ts'
|
|
17
|
-
import { eachOf, lockedWith, lockfileFormat,
|
|
17
|
+
import { type GithubAsked, type ListedLockfile, type LockfileStore, eachOf, lockedWith, lockfileFormat, readLockfile } from './pull-dependencies.ts'
|
|
18
18
|
|
|
19
19
|
export const MAX_LOCKFILES = 100
|
|
20
20
|
const READ_AT_ONCE = 4
|
|
21
21
|
|
|
22
22
|
export interface CommitLockfiles { lockfiles: { path: string; packages: LockedPackage[] }[]; unread: string[] }
|
|
23
|
-
// Set where GitHub was asked for what a read reads (`github`), not all of it
|
|
24
|
-
// held in this instance.
|
|
25
|
-
export interface GithubAsked { github: boolean }
|
|
26
23
|
|
|
27
24
|
// The lockfiles at commit `sha` its review reads, by their paths, each with
|
|
28
|
-
// its format (pull-dependencies.ts lockfileFormat), kept by
|
|
29
|
-
// `asked` told where
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
// its format (pull-dependencies.ts lockfileFormat) and its blob's id, kept by
|
|
26
|
+
// the commit, in this instance and beyond it (`kept`); `asked` told where
|
|
27
|
+
// GitHub was.
|
|
28
|
+
const listings: Lru<ListedLockfile[]> = lru(20_000, listed => listed.length + 1)
|
|
29
|
+
export async function lockfilesAt(fullName: string, token: string, sha: string, kept: LockfileStore | null, asked?: GithubAsked) {
|
|
32
30
|
const key = `${fullName}\0${sha}`
|
|
33
31
|
const known = listings.get(key)
|
|
34
32
|
if (known) return known
|
|
33
|
+
const stored = await kept?.listings.read(sha)
|
|
34
|
+
if (stored) { listings.set(key, stored); return stored }
|
|
35
35
|
if (asked) asked.github = true
|
|
36
36
|
const { lockfiles } = await upstreamGithub(() => createClient({ token, userAgent: 'deepview-triage' }).findRepoLockfiles({ repo: fullName, sha }))
|
|
37
|
-
const listed = lockfiles.flatMap(({ path }) => { const format = lockfileFormat(path); return format ? [{ path, format }] : [] })
|
|
37
|
+
const listed = lockfiles.flatMap(({ path, sha: blob }) => { const format = lockfileFormat(path); return format ? [{ path, format, sha: blob }] : [] })
|
|
38
38
|
listings.set(key, listed)
|
|
39
|
+
await kept?.listings.write(sha, listed)
|
|
39
40
|
return listed
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
export async function lockedAt(fullName: string, token: string, sha: string, held: (path: string) => boolean = () => true,
|
|
43
|
-
|
|
43
|
+
export async function lockedAt(fullName: string, token: string, sha: string, kept: LockfileStore | null, held: (path: string) => boolean = () => true,
|
|
44
|
+
asked?: GithubAsked): Promise<CommitLockfiles> {
|
|
45
|
+
const listed = (await lockfilesAt(fullName, token, sha, kept, asked)).filter(({ path }) => held(path))
|
|
44
46
|
const unread = listed.slice(MAX_LOCKFILES).map(({ path }) => path)
|
|
45
47
|
const lockfiles: CommitLockfiles['lockfiles'] = []
|
|
46
48
|
const reading = listed.slice(0, MAX_LOCKFILES)
|
|
47
|
-
|
|
48
|
-
const reads = await eachOf(reading, READ_AT_ONCE, lockfile => readLockfile(fullName, token, sha, lockfile))
|
|
49
|
+
const reads = await eachOf(reading, READ_AT_ONCE, lockfile => readLockfile(fullName, token, sha, lockfile, kept, asked))
|
|
49
50
|
for (const [at, lockfile] of reading.entries()) {
|
|
50
51
|
const read = reads[at]!
|
|
51
52
|
if ('error' in read && read.error === 'github-not-found') throw new GithubApiError(404, 'github-not-found')
|
|
@@ -494,7 +494,7 @@ export interface ManagedDb extends DependencyReviewStore, RepositoryDependencyRe
|
|
|
494
494
|
getBundleByIntegrity(integrity: string): Promise<ManagedBundle | null>
|
|
495
495
|
resolveBundleUpload(integrity: string): Promise<ManagedBundle | null>
|
|
496
496
|
getBundle(id: string): Promise<ManagedBundle | null>
|
|
497
|
-
// One
|
|
497
|
+
// One statement reads the live session, bundle and grants. Passing a team
|
|
498
498
|
// (or null for any team) also checks advisory security access; omission skips it.
|
|
499
499
|
getBundleAccessSnapshot(sessionId: string, now: number, id: string, advisoriesTeamId?: string | null): Promise<{
|
|
500
500
|
bundle: ManagedBundle | null; canReadAdvisories: boolean
|
|
@@ -697,13 +697,6 @@ function prepareStatements(db: ManagedSql) {
|
|
|
697
697
|
insertSessionStmt: db.prepare(
|
|
698
698
|
`INSERT INTO managed_session (id, user_id, csrf_token, created_at, expires_at) VALUES (?, ?, ?, ?, ?)`,
|
|
699
699
|
),
|
|
700
|
-
selectSessionStmt: db.prepare(
|
|
701
|
-
`SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp, s.upload_key AS uploadKey,
|
|
702
|
-
u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
|
|
703
|
-
FROM managed_session s
|
|
704
|
-
JOIN managed_user u ON u.id = s.user_id
|
|
705
|
-
WHERE s.id = ? AND s.expires_at > ?`,
|
|
706
|
-
),
|
|
707
700
|
selectOwnSessionStmt: db.prepare(
|
|
708
701
|
`SELECT s.id AS id, s.csrf_token AS csrf, s.expires_at AS exp, s.upload_key AS uploadKey,
|
|
709
702
|
u.id AS uid, u.login AS login, u.name AS name, u.avatar_url AS avatar, u.role AS role
|
|
@@ -994,15 +987,18 @@ function prepareStatements(db: ManagedSql) {
|
|
|
994
987
|
),
|
|
995
988
|
selectTriageFixesStmt: db.prepare(`SELECT DISTINCT fix FROM managed_finding_triage
|
|
996
989
|
WHERE finding_id IN (SELECT value FROM json_each(?)) AND fix IS NOT NULL ORDER BY fix`),
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
990
|
+
// Each finding's latest triage batch, comment versions and linked issues,
|
|
991
|
+
// in one statement.
|
|
992
|
+
annotationRevisionStmt: db.prepare(
|
|
993
|
+
`WITH requested AS (SELECT value AS id FROM json_each(?))
|
|
994
|
+
SELECT (SELECT json_group_array(json_object('id', t.finding_id, 'revision', (SELECT e.batch_id FROM managed_finding_triage_event e
|
|
995
|
+
WHERE e.finding_id = t.finding_id ORDER BY e.seq DESC LIMIT 1)) ORDER BY t.finding_id)
|
|
996
|
+
FROM managed_finding_triage t WHERE t.finding_id IN (SELECT id FROM requested)) AS triage,
|
|
997
|
+
(SELECT json_group_array(json_object('id', c.id, 'version', c.version) ORDER BY c.id)
|
|
998
|
+
FROM managed_finding_comment c WHERE c.finding_id IN (SELECT id FROM requested)) AS comments,
|
|
999
|
+
(SELECT json_group_array(json_object('findingId', i.finding_id, 'issueUrl', i.issue_url, 'autoFixUrl', i.auto_fix_url) ORDER BY i.finding_id)
|
|
1000
|
+
FROM managed_finding_issue i WHERE i.finding_id IN (SELECT id FROM requested) AND i.issue_url IS NOT NULL) AS issues`,
|
|
1001
|
+
),
|
|
1006
1002
|
insertBundleStmt: db.prepare(
|
|
1007
1003
|
`WITH candidate(id, slug) AS (VALUES (?, ?))
|
|
1008
1004
|
INSERT INTO managed_bundle (id, slug, integrity, filename, kind, byte_size, uploaded_by, uploaded_by_login, repo_id, repo_directory, visible, uploaded_at, data_key, storage_encrypted, provenance, build_conditions)
|
|
@@ -1157,6 +1153,32 @@ function prepareStatements(db: ManagedSql) {
|
|
|
1157
1153
|
WHERE b.id = ? AND tu.user_id = ? AND tu.view_security = 1
|
|
1158
1154
|
AND (? IS NULL OR tr.team_id = ?) LIMIT 1`,
|
|
1159
1155
|
),
|
|
1156
|
+
// A bundle access snapshot's reads in one statement, so one snapshot of
|
|
1157
|
+
// the database answers them all: the session's user, view sessions
|
|
1158
|
+
// included; the bundle, NULL where there is none; whether one of the
|
|
1159
|
+
// user's team scopes contains it, the grant both selectBundleReadableStmt
|
|
1160
|
+
// (a manager's, beside their own uploads) and selectRepoPathReadableStmt
|
|
1161
|
+
// (a reader's, of its repository directory) ask for; and, where the flag
|
|
1162
|
+
// asks, whether one in the team (any, where NULL) shows them its
|
|
1163
|
+
// advisories (selectBundleSecurityStmt).
|
|
1164
|
+
selectBundleAccessStmt: db.prepare(
|
|
1165
|
+
`WITH viewer AS (
|
|
1166
|
+
SELECT u.id AS uid, u.role AS role
|
|
1167
|
+
FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
1168
|
+
WHERE s.id = ? AND s.expires_at > ?
|
|
1169
|
+
)
|
|
1170
|
+
SELECT v.uid AS uid, v.role AS role, b.id AS id, b.slug AS slug, b.integrity AS integrity, b.filename AS filename, b.kind AS kind,
|
|
1171
|
+
b.byte_size AS byteSize, b.uploaded_by AS uploadedBy, b.repo_id AS repoId, b.repo_directory AS repoDirectory, b.visible AS visible,
|
|
1172
|
+
b.uploaded_at AS uploadedAt, b.provenance AS provenance, b.build_conditions AS buildConditions,
|
|
1173
|
+
EXISTS(SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
1174
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
1175
|
+
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = v.uid) AS granted,
|
|
1176
|
+
(? = 1 AND EXISTS(SELECT 1 FROM managed_team_repo tr JOIN managed_team_user tu ON tu.team_id = tr.team_id
|
|
1177
|
+
JOIN managed_team t ON t.id = tu.team_id AND t.hidden = 0
|
|
1178
|
+
WHERE tr.repo_id = b.repo_id AND ${BUNDLE_IN_TEAM_PATH_SQL} AND tu.user_id = v.uid AND tu.view_security = 1
|
|
1179
|
+
AND (? IS NULL OR tr.team_id = ?))) AS security
|
|
1180
|
+
FROM viewer v LEFT JOIN managed_bundle b ON b.id = ?`,
|
|
1181
|
+
),
|
|
1160
1182
|
// The viewer's effective visibility permissions for a report: OR'd (MAX over
|
|
1161
1183
|
// 0/1) across memberships whose repository path contains the report. NULLs
|
|
1162
1184
|
// (no such membership) read as 0 = no permission.
|
|
@@ -1439,15 +1461,10 @@ function triageMethods(stmts: ReturnType<typeof prepareStatements>, historyLimit
|
|
|
1439
1461
|
return {
|
|
1440
1462
|
async getAnnotationRevision(findingIds: readonly string[]): Promise<string> {
|
|
1441
1463
|
if (findingIds.length === 0) return '[]'
|
|
1442
|
-
const ids = JSON.stringify(findingIds)
|
|
1443
1464
|
// Batch UUIDs survive history trimming and distinguish same-timestamp
|
|
1444
1465
|
// writes, even if SQLite reuses a sequence after an annotation purge.
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
await stmts.annotationCommentRevisionStmt.all(ids),
|
|
1448
|
-
await db.prepare(`SELECT finding_id, issue_url, auto_fix_url FROM managed_finding_issue
|
|
1449
|
-
WHERE finding_id IN (SELECT value FROM json_each(?)) AND issue_url IS NOT NULL ORDER BY finding_id`).all(ids),
|
|
1450
|
-
])
|
|
1466
|
+
const row = await stmts.annotationRevisionStmt.get(JSON.stringify(findingIds)) as { triage: unknown; comments: unknown; issues: unknown }
|
|
1467
|
+
return JSON.stringify([jsonRows(row.triage), jsonRows(row.comments), jsonRows(row.issues)])
|
|
1451
1468
|
},
|
|
1452
1469
|
async deleteTriage(findingIds: readonly string[]): Promise<number> {
|
|
1453
1470
|
if (findingIds.length === 0) return 0
|
|
@@ -1579,22 +1596,19 @@ function bundleMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedS
|
|
|
1579
1596
|
return row == null ? null : mapBundle(row)
|
|
1580
1597
|
},
|
|
1581
1598
|
async getBundleAccessSnapshot(sessionId: string, now: number, id: string, advisoriesTeamId?: string | null) {
|
|
1582
|
-
|
|
1583
|
-
|
|
1599
|
+
// One statement, shared by SQLite and Postgres; the advisories grant is
|
|
1600
|
+
// read only where a team (or null, any) is passed.
|
|
1601
|
+
const team = advisoriesTeamId ?? null
|
|
1602
|
+
const row = await stmts.selectBundleAccessStmt.get(sessionId, now, advisoriesTeamId === undefined ? 0 : 1, team, team, id) as
|
|
1603
|
+
{ uid: string; role: Role; granted: unknown; security: unknown } & (BundleRow | { id: null }) | undefined
|
|
1604
|
+
if (!row) return null
|
|
1584
1605
|
const denied = { bundle: null, canReadAdvisories: false }
|
|
1585
|
-
if (!roleAtLeast(
|
|
1586
|
-
const
|
|
1587
|
-
if (!row) return denied
|
|
1588
|
-
const manager = roleAtLeast(session.role, 'manage')
|
|
1606
|
+
if (!roleAtLeast(row.role, 'view') || row.id === null) return denied
|
|
1607
|
+
const manager = roleAtLeast(row.role, 'manage')
|
|
1589
1608
|
if (!manager && row.visible !== 1) return denied
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
if (!allowed) return denied
|
|
1594
|
-
}
|
|
1595
|
-
const canReadAdvisories = manager || (advisoriesTeamId !== undefined
|
|
1596
|
-
&& await stmts.selectBundleSecurityStmt.get(id, session.uid, advisoriesTeamId, advisoriesTeamId) != null)
|
|
1597
|
-
return { bundle: mapBundle(row), canReadAdvisories }
|
|
1609
|
+
// Admins read every bundle, managers their own uploads too.
|
|
1610
|
+
if (row.role !== 'admin' && !(row.granted || (manager && row.uploadedBy === row.uid))) return denied
|
|
1611
|
+
return { bundle: mapBundle(row), canReadAdvisories: manager || Boolean(row.security) }
|
|
1598
1612
|
},
|
|
1599
1613
|
async listBundles(userId?: string): Promise<AdminBundle[]> {
|
|
1600
1614
|
const rows = (await selectBundlesStmt.all(userId ?? null, userId ?? null, userId ?? null)) as BundleListRow[]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
|
|
1
|
+
import { DEPENDENCY_REVIEW_COLUMNS, DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
|
|
2
2
|
import { LINK_REPORT_SCHEMA } from './link-reports.ts'
|
|
3
3
|
import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
|
|
4
4
|
import { TEAM_NPM_SCOPE_SCHEMA } from './team-npm-scopes.ts'
|
|
@@ -17,6 +17,7 @@ import { UPSTREAM_CACHE_SCHEMA } from './upstream-cache.ts'
|
|
|
17
17
|
import { COMMENT_SCHEMA } from './comments.ts'
|
|
18
18
|
import { ACTIVITY_SCHEMA, uploadAction } from './activity.ts'
|
|
19
19
|
import { type ManagedSqlDriver, scopeManagedMethods } from './sql.ts'
|
|
20
|
+
import { countStatement, timed } from './request-timing.ts'
|
|
20
21
|
import { postgresSchema, postgresSql } from './sql-postgres.ts'
|
|
21
22
|
import { managedTableRenames } from './db-table-names.ts'
|
|
22
23
|
import { WORKSPACE_SHARE_SCHEMA } from './workspace-shares.ts'
|
|
@@ -38,7 +39,7 @@ const LOCK = 'SELECT pg_advisory_xact_lock(1937006964, 1835101793)'
|
|
|
38
39
|
// Whether every version up to the latest is recorded, in one round trip at
|
|
39
40
|
// a cold start: a database with no versions table yet (undefined_table) has
|
|
40
41
|
// none. Outside a transaction, so the failed read aborts nothing.
|
|
41
|
-
const SCHEMA_VERSIONS =
|
|
42
|
+
const SCHEMA_VERSIONS = 32
|
|
42
43
|
async function currentSchema(db: PgConnection): Promise<boolean> {
|
|
43
44
|
try {
|
|
44
45
|
const { rows } = await db.query(`SELECT count(*) AS n FROM managed_schema_version WHERE version BETWEEN 1 AND ${SCHEMA_VERSIONS}`)
|
|
@@ -183,6 +184,14 @@ async function migrateRepoUploadIndexes(db: PgConnection): Promise<void> {
|
|
|
183
184
|
await db.query('INSERT INTO managed_schema_version VALUES (31)')
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
// The shasums of the tarballs a dependency review reads, none known of
|
|
188
|
+
// those written before.
|
|
189
|
+
async function migrateDependencyReviewShasums(db: PgConnection): Promise<void> {
|
|
190
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 32')).rows.length > 0) return
|
|
191
|
+
await db.query(`ALTER TABLE managed_dependency_review ${DEPENDENCY_REVIEW_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
192
|
+
await db.query('INSERT INTO managed_schema_version VALUES (32)')
|
|
193
|
+
}
|
|
194
|
+
|
|
186
195
|
async function initialize(db: PgConnection): Promise<void> {
|
|
187
196
|
if (await currentSchema(db)) return
|
|
188
197
|
await db.query('BEGIN')
|
|
@@ -257,7 +266,7 @@ async function initialize(db: PgConnection): Promise<void> {
|
|
|
257
266
|
await db.query(postgresSchema(revisionSchema(true)) + '; INSERT INTO managed_schema_version VALUES (11)')
|
|
258
267
|
}
|
|
259
268
|
for (const migrate of [migrateRepositoryDefaultCache, migrateBundleBuildLeases, migrateBundleVisibility, migrateLinkReports, migrateRepositoryAliases, migrateHiddenTeams, migrateIssueFixes, migrateBundleProvenance, migrateUpstreamCache, migrateBundleBuildConditions, migrateSessionUploadKeys, migrateViewSessions, migrateGithubCommits, migrateTeamNpmScopes, migrateDependencyReviews, migrateGithubLinks, migrateRepositoryDependencyReviews,
|
|
260
|
-
migrateRepositoryDependencyReviewLockfiles, migrateRepoUploadIndexes]) await migrate(db)
|
|
269
|
+
migrateRepositoryDependencyReviewLockfiles, migrateRepoUploadIndexes, migrateDependencyReviewShasums]) await migrate(db)
|
|
261
270
|
await db.query('COMMIT')
|
|
262
271
|
} catch (err) {
|
|
263
272
|
await db.query('ROLLBACK')
|
|
@@ -325,7 +334,12 @@ function requestConnections(connect: PgConnect) {
|
|
|
325
334
|
}
|
|
326
335
|
|
|
327
336
|
// Exposed for parity tests against real PostgreSQL semantics via PGlite.
|
|
328
|
-
export async function openPostgresManagedDb(
|
|
337
|
+
export async function openPostgresManagedDb(leased: PgConnect, options: ManagedDbOptions = {}, reuseConnections = false): Promise<ManagedDb> {
|
|
338
|
+
// Each statement told in its request's Server-Timing (request-timing.ts).
|
|
339
|
+
const connect: PgConnect = async () => {
|
|
340
|
+
const connection = await leased()
|
|
341
|
+
return { query: (sql, params) => { countStatement(); return connection.query(sql, params) }, release: discard => connection.release(discard) }
|
|
342
|
+
}
|
|
329
343
|
const initial = await connect()
|
|
330
344
|
try { await initialize(initial) } catch (err) { await initial.release(true).catch(() => {}); throw err }
|
|
331
345
|
await initial.release()
|
|
@@ -431,7 +445,9 @@ export async function openNeonManagedDb(url: string, options: ManagedDbOptions =
|
|
|
431
445
|
let db: ManagedDb
|
|
432
446
|
try {
|
|
433
447
|
db = await openPostgresManagedDb(async () => {
|
|
434
|
-
|
|
448
|
+
// A lease's wait told in its request's Server-Timing: a new connection's
|
|
449
|
+
// TLS, WebSocket and Postgres handshake, where the pool had none idle.
|
|
450
|
+
const client = await timed('conn', () => pool.connect())
|
|
435
451
|
if (!watched.has(client)) { watched.add(client); client.on('error', () => { failed.add(client) }) }
|
|
436
452
|
return { query: (sql, params) => client.query(sql, params),
|
|
437
453
|
release: discard => Promise.try(() => client.release(discard === true || failed.has(client))) }
|
package/server-managed/db.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { revisionSchema } from './revisions.ts'
|
|
|
10
10
|
import { MANAGED_SCHEMA, REPO_UPLOAD_INDEXES, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
11
11
|
import { GITHUB_LINK_USER_COLUMNS } from './github-links.ts'
|
|
12
12
|
import { REPOSITORY_DEPENDENCY_REVIEW_COLUMNS } from './repository-dependency-reviews.ts'
|
|
13
|
+
import { DEPENDENCY_REVIEW_COLUMNS } from './dependency-reviews.ts'
|
|
13
14
|
import { STORAGE_SCHEMA } from './storage-db.ts'
|
|
14
15
|
import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
15
16
|
import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
|
|
@@ -124,6 +125,7 @@ export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}
|
|
|
124
125
|
ensureColumn(db, 'managed_finding_issue', 'auto_fix_checked_at', 'INTEGER')
|
|
125
126
|
ensureColumn(db, 'managed_selected_repo', 'active', 'INTEGER NOT NULL DEFAULT 1')
|
|
126
127
|
for (const [column, type] of REPOSITORY_DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_selected_repo', column, type)
|
|
128
|
+
for (const [column, type] of DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_dependency_review', column, type)
|
|
127
129
|
ensureColumn(db, 'managed_finding_triage_event', 'report_id', 'TEXT')
|
|
128
130
|
ensureColumn(db, 'managed_finding_triage_event', 'report', 'TEXT')
|
|
129
131
|
ensureColumn(db, 'managed_finding_triage_event', 'repo', 'TEXT')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// A repository's Dependabot alerts for a repository risk scan, read with the
|
|
2
2
|
// App's installation token where the admin's own access lets them see them on
|
|
3
|
-
// GitHub, as
|
|
3
|
+
// GitHub, as doc/bundles.md's repository risk scan section tells.
|
|
4
4
|
import type { ManagedConfig } from './config.ts'
|
|
5
5
|
import type { SelectedRepo } from './db.ts'
|
|
6
6
|
import { type DependabotAlert, dependabotAlerts } from './dependabot-alerts.ts'
|
|
@@ -18,11 +18,14 @@ const MAX_FORMATTED = 1_000_000
|
|
|
18
18
|
// read after, every one formatted.
|
|
19
19
|
const REVIEWS_AT_ONCE = 200
|
|
20
20
|
|
|
21
|
-
// `review` as the hook is given it
|
|
22
|
-
//
|
|
21
|
+
// `review` as the hook is given it: each version with the integrity and
|
|
22
|
+
// shasum the review keeps of the tarball it read, and the version it is of
|
|
23
|
+
// with the patch `locked`, a pull request's lockfile, locks it with, where it
|
|
24
|
+
// is one's.
|
|
23
25
|
function entryOf(review: DependencyReview, locked?: AddedPackage | undefined): DependencyReviewEntry {
|
|
24
|
-
return { ecosystem: 'npm', old: review.basePackage === null ? null : { name: review.basePackage, version: review.baseVersion ?? '', integrity: review.baseIntegrity ?? ''
|
|
25
|
-
|
|
26
|
+
return { ecosystem: 'npm', old: review.basePackage === null ? null : { name: review.basePackage, version: review.baseVersion ?? '', integrity: review.baseIntegrity ?? '',
|
|
27
|
+
...review.baseShasum ? { shasum: review.baseShasum } : {} },
|
|
28
|
+
new: { name: review.package, version: review.version, integrity: review.integrity, ...review.shasum ? { shasum: review.shasum } : {},
|
|
26
29
|
...locked?.patchHash ? { patchHash: locked.patchHash } : {} },
|
|
27
30
|
status: review.status!, coverage: review.coverage === 'full' ? 'full' : 'partial', comment: review.comment,
|
|
28
31
|
files: review.entries.filter(entry => entry.status !== null || entry.comment.trim() !== '').map(entry => ({ path: entry.file, status: entry.status, comment: entry.comment })),
|