@preventive/triage 1.0.0-alpha.44 → 1.0.0-alpha.45
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/common/bundle-sources.js +20 -1
- package/common/default-scan-models.ts +1 -1
- package/common/dependency-reviews.d.ts +0 -5
- package/common/dependency-reviews.js +0 -17
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/github-agents.ts +92 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/roles.ts +7 -0
- package/common/managed/routes.js +88 -7
- package/common/managed/team-app.d.ts +5 -2
- package/common/managed/team-app.js +2 -2
- package/common/server-info.ts +3 -0
- package/common/workspace-app.js +6 -3
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2XUEZHIP.js +18 -18
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-3PS4KWD2.js +10 -0
- package/out/chunk-5DHL5NG3.js +2 -0
- package/out/chunk-6HNXNIQY.js +52 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-AFLZH7WG.js +1 -0
- package/out/chunk-BC2UCLFJ.js +1 -0
- package/out/chunk-C4Z6ZBXV.js +6 -6
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HMAJDVXN.js +1 -0
- package/out/chunk-I2MDGSQX.js +3 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -1
- package/out/chunk-NPW52BM2.js +17 -0
- package/out/chunk-NRPC74AM.js +1 -1
- package/out/chunk-NV7ES4EW.js +1 -0
- package/out/chunk-OKRLLE5I.js +17 -0
- package/out/chunk-OOJM4CTU.js +1 -1
- package/out/chunk-PNHBZGDD.js +1 -1
- package/out/chunk-PTGDYLNS.js +36 -0
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-Q6MVYGYV.js +3 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-QRD6GIG5.js +229 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +1 -1
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TUII34K2.js +1 -1
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WI7Z5A2T.js +6 -0
- package/out/chunk-WQ74ZBZF.js +7 -0
- package/out/chunk-XVBEOGOS.js +3 -0
- package/out/client-managed-import.js +3 -3
- package/out/client-managed.js +31 -35
- package/out/client-sync.js +11 -11
- package/out/compare.js +35 -0
- package/out/dialogs.js +51 -0
- package/out/github-issue-authorized.html +1 -1
- package/out/github-issue-authorized.js +1 -1
- package/out/graph.js +17 -17
- package/out/index.html +30 -34
- package/out/prism.js +14 -14
- package/out/scan.js +2 -0
- package/out/terminal.js +294 -293
- package/out/view.css +1 -1
- package/out/view.js +16 -107
- package/package.json +51 -6
- package/risk/actions/findings.js +156 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +165 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/hosts.js +6 -6
- package/risk/files/readability.js +23 -17
- package/risk/files/runs-code.js +18 -6
- package/risk/files/script.js +7 -3
- package/risk/files/stamps.js +8 -7
- package/risk/index.d.ts +68 -1
- package/risk/index.js +6 -2
- package/risk/npm/assess.js +1 -1
- package/risk/npm/compare.js +34 -10
- package/risk/npm/files.js +413 -2
- package/risk/npm/tarball.js +56 -73
- package/server-managed/agent-merges.ts +198 -0
- package/server-managed/app-agents.ts +30 -0
- package/server-managed/avatar-store.ts +1 -0
- package/server-managed/commit-coauthors.ts +83 -0
- package/server-managed/commit-lockfiles.ts +62 -0
- package/server-managed/db-methods.ts +29 -10
- package/server-managed/db-neon.ts +30 -2
- package/server-managed/db-schema.ts +9 -2
- package/server-managed/db.ts +7 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +97 -0
- package/server-managed/dependency-reviews.ts +65 -15
- package/server-managed/github-accounts.ts +33 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +23 -9
- package/server-managed/github-errors.ts +2 -0
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-oauth.ts +5 -2
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http.ts +1122 -52
- package/server-managed/index.ts +6 -2
- package/server-managed/locked-packages.ts +37 -16
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +173 -0
- package/server-managed/npm-insights.ts +20 -11
- package/server-managed/npm-kept.ts +51 -4
- package/server-managed/npm-loads.ts +20 -13
- package/server-managed/npm-packages.ts +24 -64
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +280 -0
- package/server-managed/pull-dependency-reviews.ts +318 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/repository-actions.ts +209 -0
- package/server-managed/repository-agents.ts +220 -0
- package/server-managed/repository-browser.ts +54 -13
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +52 -0
- package/server-managed/repository-dependency-reviews.ts +135 -0
- package/server-managed/repository-history.ts +445 -0
- package/server-managed/repository-issues.ts +181 -0
- package/server-managed/repository-pulse.ts +432 -0
- package/server-managed/repository-risk-read.ts +165 -0
- package/server-managed/repository-risk.ts +113 -105
- package/server-managed/reviewed-dependencies.ts +149 -0
- package/server-managed/session.ts +9 -5
- package/server-managed/sql.ts +1 -1
- package/server-managed/storage-stores.ts +5 -0
- package/server-managed/team-app.ts +19 -3
- package/server-managed/workspace-shares.ts +4 -1
- package/server.ts +11 -0
- package/out/chunk-2ABLKCOY.js +0 -46
- package/out/chunk-2KBF5TZ7.js +0 -2
- package/out/chunk-35VE6CDH.js +0 -2
- package/out/chunk-3BZ7JERB.js +0 -1
- package/out/chunk-4XCDFGDP.js +0 -10
- package/out/chunk-5LSXOLUS.js +0 -1
- package/out/chunk-5RCM2J63.js +0 -1
- package/out/chunk-7B6HB4D7.js +0 -1
- package/out/chunk-AJ4GTATZ.js +0 -3
- package/out/chunk-CH4UN6WK.js +0 -225
- package/out/chunk-CMQA4DM6.js +0 -1
- package/out/chunk-CVVJAXVY.js +0 -2
- package/out/chunk-DIJJNTRH.js +0 -10
- package/out/chunk-EDKLBQQN.js +0 -17
- package/out/chunk-F47DXCEV.js +0 -33
- package/out/chunk-FK3DZEKL.js +0 -1
- package/out/chunk-G6AJWENO.js +0 -1
- package/out/chunk-GJ4IUCNS.js +0 -1
- package/out/chunk-GNXOKIGP.js +0 -80
- package/out/chunk-GZ4MS3TE.js +0 -13
- package/out/chunk-GZWM7UQM.js +0 -2
- package/out/chunk-IU3CCINR.js +0 -3
- package/out/chunk-K3X5EJBQ.js +0 -2
- package/out/chunk-KEX2ATEW.js +0 -2
- package/out/chunk-KYCNJVB7.js +0 -1
- package/out/chunk-LPCTJYNA.js +0 -7
- package/out/chunk-O4RJY5TP.js +0 -2
- package/out/chunk-OD4LPN3H.js +0 -1
- package/out/chunk-QWDWTBM7.js +0 -1
- package/out/chunk-QYWKC557.js +0 -1
- package/out/chunk-R75XOKUP.js +0 -1
- package/out/chunk-SWTLADW5.js +0 -1
- package/out/chunk-T7OKH4VW.js +0 -1
- package/out/chunk-TZ5KB2Y5.js +0 -10
- package/out/chunk-UNTRGE26.js +0 -2
- package/out/chunk-UTR2LVME.js +0 -2
- package/out/chunk-UYAXVRVL.js +0 -1
- package/out/chunk-V3ME6FI5.js +0 -2
- package/out/chunk-V5O7M2YT.js +0 -1
- package/out/chunk-VIHGO77Y.js +0 -1
- package/out/chunk-W4J3MGLB.js +0 -1
- package/out/chunk-WGG7KQUH.js +0 -1
- package/out/chunk-XYFXXB2G.js +0 -10
- package/out/chunk-ZESLQCTF.js +0 -6
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// The commits agents' pull requests brought to a repository's default
|
|
2
|
+
// branch, for its Pulse (repository-pulse.ts commitStats): an agent that
|
|
3
|
+
// commits as whoever ran it (Codex's own) leaves its commits no mark, its
|
|
4
|
+
// pull request's branch alone naming it (app-agents.ts
|
|
5
|
+
// AGENT_BRANCH_PREFIXES), or a label one who ran it gave it naming the agent
|
|
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).
|
|
8
|
+
import { labelAgents } from '../common/managed/agent-tasks.ts'
|
|
9
|
+
import { GITHUB_AGENTS, agentOperated } from '../common/managed/github-agents.ts'
|
|
10
|
+
import { AGENT_BRANCH_PREFIXES, APP_AGENTS } from './app-agents.ts'
|
|
11
|
+
import { type HistoryCommit, githubGraphql } from './repository-history.ts'
|
|
12
|
+
|
|
13
|
+
// The merged pull requests read an answer, a page a query, as many queries
|
|
14
|
+
// as MERGED_QUERIES, the rest left for the next to ask; of each, as many of
|
|
15
|
+
// its latest commits as MERGED_COMMITS.
|
|
16
|
+
const MERGED_PAGE = 100
|
|
17
|
+
const MERGED_QUERIES = 5
|
|
18
|
+
const MERGED_COMMITS = 100
|
|
19
|
+
// Of each, as many labels as MERGED_LABELS, GitHub's most a page, a label
|
|
20
|
+
// naming any agent the app knows making it an agent's.
|
|
21
|
+
const MERGED_LABELS = 100
|
|
22
|
+
// How long before or after a pull request rebased onto the branch was merged
|
|
23
|
+
// its copies are committed.
|
|
24
|
+
const REBASE_SKEW_MS = 10 * 60_000
|
|
25
|
+
const LABEL_AGENTS = [...new Set([...Object.keys(GITHUB_AGENTS), ...APP_AGENTS.map(agent => agent.id)])]
|
|
26
|
+
const SHA = /^[a-f\d]{40}$/u
|
|
27
|
+
|
|
28
|
+
const count = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0
|
|
29
|
+
const timeOf = (value: unknown) => {
|
|
30
|
+
const at = typeof value === 'string' ? Date.parse(value) : Number.NaN
|
|
31
|
+
return Number.isSafeInteger(at) ? at : null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The agents' pull requests merged since a year's start, as read so far
|
|
35
|
+
// (agentMerges): the commits each brought, and when it was merged; those
|
|
36
|
+
// rebased onto the branch by their last commit there, when merged, and
|
|
37
|
+
// their commits' subjects, the oldest first (agentCommits); when the newest
|
|
38
|
+
// read was last updated, those updated since read again (`through`, null
|
|
39
|
+
// till all are read once); and a read not finished: where it goes on
|
|
40
|
+
// (`after`), when the newest it began with was updated (`from`), the oldest
|
|
41
|
+
// it reads to (`stop`) and how many it has read (`read`). And of every pull
|
|
42
|
+
// request merged, an agent's or not, its merge commit and when (`pulls`),
|
|
43
|
+
// and one rebased so (`pullRebases`), for the commits pushed to the branch
|
|
44
|
+
// 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[] }
|
|
47
|
+
// A pull request rebased onto the branch: its last commit there, when it was
|
|
48
|
+
// merged, its latest commits' subjects, the oldest first, and how many more
|
|
49
|
+
// it had than those read (`more`).
|
|
50
|
+
type Rebase = [sha: string, at: number, subjects: string[], more?: number]
|
|
51
|
+
|
|
52
|
+
const MERGED_QUERY = `query($owner: String!, $name: String!, $after: String) {
|
|
53
|
+
repository(owner: $owner, name: $name) {
|
|
54
|
+
pullRequests(states: MERGED, first: ${MERGED_PAGE}, after: $after, orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
55
|
+
pageInfo { hasNextPage endCursor }
|
|
56
|
+
nodes { updatedAt mergedAt headRefName author { login ... on User { databaseId } } labels(first: ${MERGED_LABELS}) { nodes { name } }
|
|
57
|
+
mergeCommit { oid parents { totalCount } }
|
|
58
|
+
commits(last: ${MERGED_COMMITS}) { totalCount nodes { commit { oid messageHeadline changedFilesIfAvailable } } } }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}`
|
|
62
|
+
type MergedPage = { repository?: { pullRequests?: { pageInfo?: { hasNextPage?: unknown; endCursor?: unknown }
|
|
63
|
+
nodes?: ({ updatedAt?: unknown; mergedAt?: unknown; headRefName?: unknown; author?: { login?: unknown; databaseId?: unknown } | null
|
|
64
|
+
labels?: { nodes?: ({ name?: unknown } | null)[] } | null
|
|
65
|
+
mergeCommit?: { oid?: unknown; parents?: { totalCount?: unknown } | null } | null
|
|
66
|
+
commits?: { totalCount?: unknown; nodes?: ({ commit?: { oid?: unknown; messageHeadline?: unknown; changedFilesIfAvailable?: unknown } | null } | null)[] } | null
|
|
67
|
+
} | null)[] } | null } | null }
|
|
68
|
+
const agentBranch = (name: unknown) => typeof name === 'string' && AGENT_BRANCH_PREFIXES.some(prefix => name.startsWith(prefix))
|
|
69
|
+
type MergedNode = NonNullable<NonNullable<NonNullable<NonNullable<MergedPage['repository']>['pullRequests']>['nodes']>[number]>
|
|
70
|
+
// Whether a pull request is an agent's: by its branch, a label naming one,
|
|
71
|
+
// or its author, an account agents work as.
|
|
72
|
+
const agentPull = ({ headRefName, labels, author }: MergedNode) => agentBranch(headRefName)
|
|
73
|
+
|| labelAgents((labels?.nodes ?? []).map(label => label?.name), LABEL_AGENTS).length > 0
|
|
74
|
+
|| agentOperated(typeof author?.login === 'string' ? author.login : null, Number.isSafeInteger(author?.databaseId) ? author!.databaseId as number : null)
|
|
75
|
+
|
|
76
|
+
// The commits the pull requests merged since `since` from an agent's branch,
|
|
77
|
+
// labelled for one, or opened by an account agents work as, brought: each its merge commit, a squash merge's one
|
|
78
|
+
// commit; where it was
|
|
79
|
+
// merged with a merge commit, which a year counts none of, its own commits,
|
|
80
|
+
// its latest MERGED_COMMITS; and of one of more commits whose merge commit
|
|
81
|
+
// has one parent, its commits' subjects, for those it was rebased as to be
|
|
82
|
+
// told by (agentCommits), those of its empty commits left out, as a rebase
|
|
83
|
+
// leaves them. Those merged are read the most recently updated
|
|
84
|
+
// first (a pull request is updated when merged, or after), on from what was
|
|
85
|
+
// `kept`: till one updated before the year, or before the newest read before
|
|
86
|
+
// was, as many queries an answer as MERGED_QUERIES, a read not finished
|
|
87
|
+
// going on from where it was left by the next; one read again whose label
|
|
88
|
+
// no longer names an agent, what it was read to bring forgotten.
|
|
89
|
+
export async function agentMerges(fullName: string, token: string, since: number, kept: AgentMerges | null,
|
|
90
|
+
fetchImpl: typeof fetch = globalThis.fetch): Promise<AgentMerges> {
|
|
91
|
+
const [owner, name] = fullName.split('/') as [string, string]
|
|
92
|
+
const merges = new Map((kept?.merges ?? []).filter(([, at]) => at >= since))
|
|
93
|
+
const rebases = new Map((kept?.rebases ?? []).filter(([, at]) => at >= since).map(([sha, at, subjects, more = 0]) => [sha, [at, subjects, more] as const]))
|
|
94
|
+
const pulls = new Map((kept?.pulls ?? []).filter(([, at]) => at >= since))
|
|
95
|
+
const pullRebases = new Map((kept?.pullRebases ?? []).filter(([, at]) => at >= since).map(([sha, at, subjects, more = 0]) => [sha, [at, subjects, more] as const]))
|
|
96
|
+
const stop = kept?.next?.stop ?? Math.max(since, kept?.through ?? since)
|
|
97
|
+
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, next,
|
|
99
|
+
pulls: [...pulls], pullRebases: listed(pullRebases) })
|
|
100
|
+
let { after = null, from = null, read = 0 }: { after?: string | null; from?: number | null; read?: number } = kept?.next ?? {}
|
|
101
|
+
for (let queries = 0; queries < MERGED_QUERIES; queries++) {
|
|
102
|
+
const page = (await githubGraphql(MERGED_QUERY, { owner, name, after }, token, fetchImpl) as MergedPage | null)?.repository?.pullRequests
|
|
103
|
+
let done = page?.pageInfo?.hasNextPage !== true || typeof page.pageInfo.endCursor !== 'string'
|
|
104
|
+
for (const node of page?.nodes ?? []) {
|
|
105
|
+
const updated = timeOf(node?.updatedAt)
|
|
106
|
+
if (updated === null) continue
|
|
107
|
+
if (updated < stop) { done = true; break }
|
|
108
|
+
from ??= updated
|
|
109
|
+
read++
|
|
110
|
+
const [merged, merge] = [timeOf(node?.mergedAt), node?.mergeCommit?.oid]
|
|
111
|
+
if (!node || merged === null || merged < since || typeof merge !== 'string' || !SHA.test(merge)) continue
|
|
112
|
+
const own = (node.commits?.nodes ?? []).map(item => item?.commit)
|
|
113
|
+
const merging = count(node.mergeCommit?.parents?.totalCount) > 1
|
|
114
|
+
const brought = [merge, ...merging ? own.map(commit => commit?.oid) : []].filter((oid): oid is string => typeof oid === 'string' && SHA.test(oid))
|
|
115
|
+
const copied = own.filter(commit => commit?.changedFilesIfAvailable !== 0)
|
|
116
|
+
// Of one of more commits than its latest read, how many more it had.
|
|
117
|
+
const more = Math.max(0, count(node.commits?.totalCount) - own.length)
|
|
118
|
+
const subjects = !merging && (copied.length > 1 || more > 0) && copied.length > 0 && copied.every(commit => typeof commit?.messageHeadline === 'string')
|
|
119
|
+
? copied.map(commit => commit!.messageHeadline as string) : null
|
|
120
|
+
pulls.set(merge, merged)
|
|
121
|
+
if (subjects) pullRebases.set(merge, [merged, subjects, more])
|
|
122
|
+
// One an agent's no more, its label taken off (which updates it), what
|
|
123
|
+
// it was read to bring none of an agent's.
|
|
124
|
+
if (!agentPull(node)) {
|
|
125
|
+
for (const oid of brought.filter(sha => merges.get(sha) === merged)) merges.delete(oid)
|
|
126
|
+
if (rebases.get(merge)?.[0] === merged) rebases.delete(merge)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
for (const oid of brought) merges.set(oid, merged)
|
|
130
|
+
if (subjects) rebases.set(merge, [merged, subjects, more])
|
|
131
|
+
}
|
|
132
|
+
if (done) return result(from ?? stop, null)
|
|
133
|
+
after = page!.pageInfo!.endCursor as string
|
|
134
|
+
}
|
|
135
|
+
return result(kept?.through ?? null, { after: after!, from: from ?? stop, stop, read })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Whether two reads of the pull requests merged (agentMerges) bring the
|
|
139
|
+
// same commits, the agents' and every one's (`pulls`, `pullRebases`, which
|
|
140
|
+
// tell those pushed directly), whatever order each read them in.
|
|
141
|
+
export function sameMerges(a: AgentMerges, b: AgentMerges) {
|
|
142
|
+
const sorted = (entries: unknown[] = []) => entries.map(entry => JSON.stringify(entry)).toSorted()
|
|
143
|
+
const brought = (read: AgentMerges) => JSON.stringify([read.merges, read.rebases, read.pulls, read.pullRebases].map(sorted))
|
|
144
|
+
return brought(a) === brought(b)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// A commit's subject as GitHub heads its message: whole, or, past what it
|
|
148
|
+
// shows, cut short before an ellipsis.
|
|
149
|
+
const headed = (subject: string, headline: string) => headline.endsWith('…') ? subject.startsWith(headline.slice(0, -1)) : subject === headline
|
|
150
|
+
|
|
151
|
+
// The commits of `commits` (a year's) the agents' pull requests brought
|
|
152
|
+
// (`read`, agentMerges): those it names, and of one rebased, its commits'
|
|
153
|
+
// copies, the last and those below it on their first parents, as long as
|
|
154
|
+
// each one's subject is that of one of its commits older than the last
|
|
155
|
+
// matched, as Commits tells a rebase (repository-history.ts squashedPulls),
|
|
156
|
+
// past those of its commits the rebase left none of (an empty one GitHub
|
|
157
|
+
// counted no files of), and, past them all, as many more as it had before
|
|
158
|
+
// its latest read; a squash merge's one commit alone, its subject the pull
|
|
159
|
+
// request's own.
|
|
160
|
+
export function agentCommits(read: AgentMerges, commits: HistoryCommit[]): Set<string> {
|
|
161
|
+
const shas = new Set(read.merges.map(([sha]) => sha))
|
|
162
|
+
const bySha = new Map(commits.map(commit => [commit.sha, commit]))
|
|
163
|
+
for (const [last, merged, subjects, more = 0] of read.rebases) {
|
|
164
|
+
let commit = bySha.get(last)
|
|
165
|
+
let at = subjects.length - 1
|
|
166
|
+
for (; commit && at >= 0; commit = bySha.get(commit.parents[0] ?? '')) {
|
|
167
|
+
const [copied, upTo] = [commit, at]
|
|
168
|
+
const found = subjects.findLastIndex((subject, index) => index <= upTo && headed(copied.subject, subject))
|
|
169
|
+
if (found < 0) break
|
|
170
|
+
shas.add(commit.sha)
|
|
171
|
+
at = found - 1
|
|
172
|
+
}
|
|
173
|
+
// Each of its latest commits read copied, as many as it had before them
|
|
174
|
+
// the commits below, on their first parents, as long as each was
|
|
175
|
+
// committed as it was merged, as a rebase commits its copies: not past
|
|
176
|
+
// its first, its empty ones, which a rebase leaves out, counted too.
|
|
177
|
+
const copiedAsMerged = (when: number | null) => when !== null && Math.abs(when - merged) <= REBASE_SKEW_MS
|
|
178
|
+
for (let rest = at < 0 ? more : 0; commit && rest > 0 && copiedAsMerged(commit.committedAt); rest--, commit = bySha.get(commit.parents[0] ?? '')) {
|
|
179
|
+
shas.add(commit.sha)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return shas
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The commits of `commits` (a year's, from the branch's `head`) pushed to
|
|
186
|
+
// it directly: on its first parents from the head, none a merge, none a
|
|
187
|
+
// pull request brought (`read.pulls`: its merge commit, a squash's one
|
|
188
|
+
// commit, or the copies of one rebased onto it, as agentCommits tells
|
|
189
|
+
// them); those a merge brought from its other parents, not.
|
|
190
|
+
export function directCommits(read: AgentMerges, commits: HistoryCommit[], head: string): Set<string> {
|
|
191
|
+
const pulled = agentCommits({ ...read, merges: read.pulls ?? [], rebases: read.pullRebases ?? [] }, commits)
|
|
192
|
+
const bySha = new Map(commits.map(commit => [commit.sha, commit]))
|
|
193
|
+
const direct = new Set<string>()
|
|
194
|
+
for (let commit = bySha.get(head); commit; commit = bySha.get(commit.parents[0] ?? '')) {
|
|
195
|
+
if (commit.parents.length <= 1 && !pulled.has(commit.sha)) direct.add(commit.sha)
|
|
196
|
+
}
|
|
197
|
+
return direct
|
|
198
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The agents the server knows by their GitHub Apps or accounts (the Agents
|
|
2
|
+
// tab's, server-managed/repository-agents.ts, whose marks the Commits tab
|
|
3
|
+
// reads on commits too, repository-history.ts).
|
|
4
|
+
import type { AppAgent, AppAgentMarks } from '../common/managed/agent-tasks.ts'
|
|
5
|
+
import { GITHUB_AGENTS } from '../common/managed/github-agents.ts'
|
|
6
|
+
|
|
7
|
+
// The agents known by their GitHub Apps or accounts, beside Claude and
|
|
8
|
+
// Codex: each its id and name, its avatar (its mark, served by
|
|
9
|
+
// /api/avatar/<avatar>, for these alone: a GitHub App's, `app-<id>`, or
|
|
10
|
+
// account's, `user-<id>`), the fingerprint it leaves as a line of its pull
|
|
11
|
+
// requests' bodies and its commits' messages, the account its work is
|
|
12
|
+
// authored by (its id and the logins GitHub names it by, told by both), and
|
|
13
|
+
// its branches' prefix. Their work may carry Claude's or
|
|
14
|
+
// Codex's marks too, those its tools. Copilot's coding agent opens its pull
|
|
15
|
+
// requests and authors its commits as its account, Copilot (198982749), on
|
|
16
|
+
// `copilot/` branches; Dependabot, its GitHub App (29110), opens its pull
|
|
17
|
+
// requests as its account, `dependabot` (49699333, `dependabot[bot]` to
|
|
18
|
+
// GitHub's REST API and in its address), on `dependabot/<ecosystem>/…`
|
|
19
|
+
// branches. Their accounts and marks are those the app draws their work by
|
|
20
|
+
// wherever it shows it (common/managed/github-agents.ts): Copilot's its own,
|
|
21
|
+
// the app's to draw, no avatar.
|
|
22
|
+
const { copilot, dependabot } = GITHUB_AGENTS
|
|
23
|
+
export const APP_AGENTS: readonly (AppAgent & AppAgentMarks)[] = [
|
|
24
|
+
{ id: 'neo', name: 'Neo', avatar: 'app-3115942', fingerprint: '🤖 Generated with [Neo](', prefix: 'neo/' },
|
|
25
|
+
{ id: 'copilot', name: copilot.name, account: copilot.accounts[0], prefix: 'copilot/' },
|
|
26
|
+
{ id: 'dependabot', name: dependabot.name, avatar: dependabot.avatar, account: dependabot.accounts[0], prefix: 'dependabot/' },
|
|
27
|
+
]
|
|
28
|
+
// The prefixes of the branches agents work on: Claude Code's and Codex's,
|
|
29
|
+
// and the app agents'.
|
|
30
|
+
export const AGENT_BRANCH_PREFIXES = ['claude/', 'codex/', ...APP_AGENTS.map(agent => agent.prefix)]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Who made a commit beside its author, as the Commits tab shows them
|
|
2
|
+
// (repository-history.ts): its co-authors, as its `Co-authored-by:` trailers
|
|
3
|
+
// name them, and the agents its message marks.
|
|
4
|
+
import { agentSignals, trailerBlock } from '../common/managed/agent-tasks.ts'
|
|
5
|
+
import { GITHUB_AGENTS, type GithubAgentId, emailAgent, githubAgent } from '../common/managed/github-agents.ts'
|
|
6
|
+
import { APP_AGENTS } from './app-agents.ts'
|
|
7
|
+
import { type NamedAccount, commitAccount } from './github-accounts.ts'
|
|
8
|
+
|
|
9
|
+
export const MAX_COAUTHORS = 3
|
|
10
|
+
// The rules co-authors are read by, the agents' addresses and names among
|
|
11
|
+
// them: what was kept read by others (history pages, squashed pull requests)
|
|
12
|
+
// read again by these, once raised.
|
|
13
|
+
export const COAUTHORS_READ = 2
|
|
14
|
+
const MAX_NAME = 256
|
|
15
|
+
const textOf = (value: unknown, max: number) => typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : null
|
|
16
|
+
|
|
17
|
+
// One who made a commit beside its author: by name and account, as its
|
|
18
|
+
// author is, and the agent it is where known, one GitHub knows by its account
|
|
19
|
+
// or its commits' address or name (common/managed/github-agents.ts) or one
|
|
20
|
+
// the server knows by its app (app-agents.ts), with its avatar.
|
|
21
|
+
export interface Coauthor { name: string | null; login: string | null; id: number | null; agent?: string; avatar?: string }
|
|
22
|
+
|
|
23
|
+
// `Co-authored-by: <name> <address>` trailers, GitHub matching the key
|
|
24
|
+
// regardless of case; Cursor's agents' links, as their work carries them;
|
|
25
|
+
// the app agents known by the fingerprints they leave.
|
|
26
|
+
const COAUTHOR_TRAILER = /^co-authored-by:[ \t]*([^<\n]*?)[ \t]*<([^<>\s]+)>[ \t]*$/gimu
|
|
27
|
+
const CURSOR_AGENT = /https:\/\/cursor\.com\/(?:agents\?id=|background-agent\?bcId=)bc-/u
|
|
28
|
+
const FINGERPRINTED = APP_AGENTS.filter(agent => agent.fingerprint)
|
|
29
|
+
|
|
30
|
+
// One named as a commit's author or co-author: by name, the address its
|
|
31
|
+
// commit gives (where it is read), and account.
|
|
32
|
+
export type Maker = { name: unknown; email?: unknown; account: NamedAccount }
|
|
33
|
+
|
|
34
|
+
// The agent one who made a commit is: by their account, else the address
|
|
35
|
+
// their commit names them by; else, GitHub naming no account for them, by
|
|
36
|
+
// their name.
|
|
37
|
+
const makerAgent = ({ name, email, account }: Maker): GithubAgentId | null => githubAgent(account.login, account.id) ?? emailAgent(email)
|
|
38
|
+
?? (account.login ? null : githubAgent(null, null, typeof name === 'string' ? name : null))
|
|
39
|
+
|
|
40
|
+
// Who made a commit, past its author (`author`), as its message names them:
|
|
41
|
+
// its co-authors, those GitHub's GraphQL API lists past the author (`listed`,
|
|
42
|
+
// the addresses its `Co-authored-by:` trailers give, by the accounts GitHub
|
|
43
|
+
// knows them by), else its trailers (agent-tasks.ts trailerBlock), an account's no-reply
|
|
44
|
+
// address naming it;
|
|
45
|
+
// then the agents it marks, Claude's and Codex's as the Agents tab reads them
|
|
46
|
+
// (agent-tasks.ts agentSignals: their sessions' and tasks' links, Claude's
|
|
47
|
+
// co-author and "Generated with" lines), the app agents' by their
|
|
48
|
+
// fingerprints, Cursor's by its agents' links. Each the agent it is where
|
|
49
|
+
// known, and once, the author none of them, as many as MAX_COAUTHORS: two
|
|
50
|
+
// are one by the first of their agent, account, address and name both name,
|
|
51
|
+
// two people of one name with accounts or addresses of their own two.
|
|
52
|
+
export function coauthorsOf(message: unknown, author: Maker, listed: Maker[] | null = null): Coauthor[] {
|
|
53
|
+
const text = typeof message === 'string' ? message.replaceAll('\r\n', '\n') : ''
|
|
54
|
+
const named = listed ?? [...trailerBlock(text).matchAll(COAUTHOR_TRAILER)].map(([, name, email]) => ({ name, email, account: commitAccount({ email }) }))
|
|
55
|
+
const signals = agentSignals([text], FINGERPRINTED)
|
|
56
|
+
const marked = [...signals.claude || signals.sessions.length > 0 ? ['claude'] : [], ...signals.tasks.length > 0 || signals.codex ? ['codex'] : [],
|
|
57
|
+
...CURSOR_AGENT.test(text) ? ['cursor'] : [], ...signals.agents]
|
|
58
|
+
type Identity = { agent: string | null; login: string | null; email: string | null; name: string | null }
|
|
59
|
+
const identity = (agent: string | null, login: string | null, name: string | null, email: unknown = null): Identity => ({ agent, login: login?.toLowerCase() ?? null,
|
|
60
|
+
email: typeof email === 'string' && email.trim() ? email.trim().toLowerCase() : null, name: name?.toLowerCase() ?? null })
|
|
61
|
+
const same = (a: Identity, b: Identity) => a.agent && b.agent ? a.agent === b.agent : a.login && b.login ? a.login === b.login
|
|
62
|
+
: a.email && b.email ? a.email === b.email : a.name !== null && a.name === b.name
|
|
63
|
+
const seen = [identity(makerAgent(author), author.account.login, textOf(author.name, MAX_NAME), author.email)]
|
|
64
|
+
const coauthors: Coauthor[] = []
|
|
65
|
+
const add = (coauthor: Coauthor, email: unknown = null) => {
|
|
66
|
+
const who = identity(coauthor.agent ?? null, coauthor.login, coauthor.name, email)
|
|
67
|
+
if (seen.some(other => same(other, who)) || coauthors.length >= MAX_COAUTHORS || (!coauthor.login && !coauthor.name && !coauthor.agent)) return
|
|
68
|
+
seen.push(who)
|
|
69
|
+
coauthors.push(coauthor)
|
|
70
|
+
}
|
|
71
|
+
for (const maker of named) {
|
|
72
|
+
const agent = makerAgent(maker)
|
|
73
|
+
// An agent known by its address where GitHub names someone else's
|
|
74
|
+
// account for it (Codex's), as itself, not that account.
|
|
75
|
+
const { login, id } = agent && maker.account.login && githubAgent(maker.account.login, maker.account.id) !== agent ? { login: null, id: null } : maker.account
|
|
76
|
+
add({ name: textOf(maker.name, MAX_NAME), login, id, ...agent ? { agent } : {} }, maker.email)
|
|
77
|
+
}
|
|
78
|
+
for (const agent of marked) {
|
|
79
|
+
const app = APP_AGENTS.find(known => known.id === agent && !Object.hasOwn(GITHUB_AGENTS, known.id))
|
|
80
|
+
add({ name: app?.name ?? GITHUB_AGENTS[agent as GithubAgentId].name, login: null, id: null, agent, ...app?.avatar ? { avatar: app.avatar } : {} })
|
|
81
|
+
}
|
|
82
|
+
return coauthors
|
|
83
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The versions a repository's lockfiles of npm's ecosystem lock at a commit,
|
|
2
|
+
// as their review reads them (pull-dependency-reviews.ts): every lockfile
|
|
3
|
+
// there (findRepoLockfiles) a review holds to (`held`, lockfile-scope.ts),
|
|
4
|
+
// read as a pull request's are (pull-dependencies.ts readLockfile, kept in
|
|
5
|
+
// this instance by the commit and path), as many as MAX_LOCKFILES of those,
|
|
6
|
+
// each's versions (`lockfiles`), and those its reader refuses, of Bun's, or
|
|
7
|
+
// past MAX_LOCKFILES (`unread`); and those of some of them (lockedIn), each
|
|
8
|
+
// version once by what its reviews are held to (lockedWith), dev only where
|
|
9
|
+
// it is wherever it is locked. GitHub having none of a lockfile it lists
|
|
10
|
+
// there fails the whole, as it would for any file.
|
|
11
|
+
import { createClient } from '@preventive/upstream/github.js'
|
|
12
|
+
import { upstreamGithub } from './github-errors.ts'
|
|
13
|
+
import { GithubApiError } from './github-app.ts'
|
|
14
|
+
import type { LockedPackage } from './locked-packages.ts'
|
|
15
|
+
import { type Lru, lru } from './lru.ts'
|
|
16
|
+
import { lockedWith, lockfileFormat, readLockfile } from './pull-dependencies.ts'
|
|
17
|
+
|
|
18
|
+
export const MAX_LOCKFILES = 100
|
|
19
|
+
|
|
20
|
+
export interface CommitLockfiles { lockfiles: { path: string; packages: LockedPackage[] }[]; unread: string[] }
|
|
21
|
+
|
|
22
|
+
// The lockfiles at commit `sha` its review reads, by their paths, each with
|
|
23
|
+
// its format (pull-dependencies.ts lockfileFormat), kept by the commit.
|
|
24
|
+
const listings: Lru<{ path: string; format: NonNullable<ReturnType<typeof lockfileFormat>> }[]> = lru(20_000, listed => listed.length + 1)
|
|
25
|
+
export async function lockfilesAt(fullName: string, token: string, sha: string) {
|
|
26
|
+
const key = `${fullName}\0${sha}`
|
|
27
|
+
const known = listings.get(key)
|
|
28
|
+
if (known) return known
|
|
29
|
+
const { lockfiles } = await upstreamGithub(() => createClient({ token, userAgent: 'deepview-triage' }).findRepoLockfiles({ repo: fullName, sha }))
|
|
30
|
+
const listed = lockfiles.flatMap(({ path }) => { const format = lockfileFormat(path); return format ? [{ path, format }] : [] })
|
|
31
|
+
listings.set(key, listed)
|
|
32
|
+
return listed
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function lockedAt(fullName: string, token: string, sha: string, held: (path: string) => boolean = () => true): Promise<CommitLockfiles> {
|
|
36
|
+
const listed = (await lockfilesAt(fullName, token, sha)).filter(({ path }) => held(path))
|
|
37
|
+
const unread = listed.slice(MAX_LOCKFILES).map(({ path }) => path)
|
|
38
|
+
const lockfiles: CommitLockfiles['lockfiles'] = []
|
|
39
|
+
for (const lockfile of listed.slice(0, MAX_LOCKFILES)) {
|
|
40
|
+
const read = await readLockfile(fullName, token, sha, lockfile)
|
|
41
|
+
if ('error' in read && read.error === 'github-not-found') throw new GithubApiError(404, 'github-not-found')
|
|
42
|
+
if ('error' in read) unread.push(lockfile.path)
|
|
43
|
+
else lockfiles.push({ path: lockfile.path, packages: read.packages })
|
|
44
|
+
}
|
|
45
|
+
return { lockfiles, unread: unread.toSorted() }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The versions those of `read`'s lockfiles `held` holds to lock, each once,
|
|
49
|
+
// and their paths.
|
|
50
|
+
export function lockedIn(read: CommitLockfiles, held: (path: string) => boolean = () => true) {
|
|
51
|
+
const packages = new Map<string, LockedPackage>()
|
|
52
|
+
const paths: string[] = []
|
|
53
|
+
for (const lockfile of read.lockfiles.filter(({ path }) => held(path))) {
|
|
54
|
+
paths.push(lockfile.path)
|
|
55
|
+
for (const pkg of lockfile.packages) {
|
|
56
|
+
const kept = packages.get(lockedWith(pkg))
|
|
57
|
+
if (!kept) packages.set(lockedWith(pkg), { ...pkg })
|
|
58
|
+
else if (kept.dev && !pkg.dev) delete kept.dev
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { packages: [...packages.values()], paths }
|
|
62
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type LinkReportStore, linkReportMethods, linkRevision } from './link-reports.ts'
|
|
2
2
|
import { type RepositoryAliasStore, repositoryAliasMethods } from './repository-aliases.ts'
|
|
3
3
|
import { type DependencyReviewStore, dependencyReviewMethods } from './dependency-reviews.ts'
|
|
4
|
+
import { type RepositoryDependencyReviewStore, repositoryDependencyReviewMethods } from './repository-dependency-reviews.ts'
|
|
4
5
|
import { type TeamNpmScopeStore, teamNpmScopeMethods } from './team-npm-scopes.ts'
|
|
6
|
+
import { type GithubLinkStore, githubLinkMethods } from './github-links.ts'
|
|
5
7
|
import { createHash, randomUUID } from 'node:crypto'
|
|
6
8
|
import { teamCatalogRevision } from './team-catalog.ts'
|
|
7
9
|
import { type Role, roleAtLeast } from '../common/managed/roles.ts'
|
|
@@ -358,21 +360,26 @@ export interface UserTeam {
|
|
|
358
360
|
dependencyReview?: boolean
|
|
359
361
|
permissions: TeamUserPermissions
|
|
360
362
|
cacheKey?: string
|
|
363
|
+
// The team's repository links, by name: its Repositories, whether or not
|
|
364
|
+
// anything is attached to them yet.
|
|
365
|
+
repos: TeamRepoLink[]
|
|
361
366
|
reports: UserTeamReport[]
|
|
362
367
|
bundles: UserTeamBundle[]
|
|
363
368
|
}
|
|
364
369
|
|
|
365
370
|
// Backend-agnostic store surface (SQLite + PostgreSQL implementations).
|
|
366
|
-
export interface ManagedDb extends DependencyReviewStore, ActivityStore, CommentStore, GithubMetadataStore, GithubCommitStore, ManagedIssueStore, BundleBuildLeaseStore, UpstreamCacheStore, WorkspaceShareStore, ImportTriageStore, ManagementStore, ManagementCatalogStore, StorageDb, LinkReportStore, RepositoryAliasStore, TeamNpmScopeStore {
|
|
371
|
+
export interface ManagedDb extends DependencyReviewStore, RepositoryDependencyReviewStore, ActivityStore, CommentStore, GithubMetadataStore, GithubCommitStore, ManagedIssueStore, BundleBuildLeaseStore, UpstreamCacheStore, WorkspaceShareStore, ImportTriageStore, ManagementStore, ManagementCatalogStore, StorageDb, LinkReportStore, RepositoryAliasStore, TeamNpmScopeStore, GithubLinkStore {
|
|
367
372
|
claimMaintenanceLease(owner: string, now: number, until: number, migration?: boolean): Promise<boolean>
|
|
368
373
|
finishMaintenanceLease(owner: string, until: number): Promise<void>
|
|
369
374
|
getFeedState(sessionId: string, now: number): Promise<{ user: Pick<StoredUser, 'id' | 'role'>; catalog: number; annotations: number } | null>
|
|
370
375
|
// Reuse connections within a finite request or one feed polling iteration.
|
|
371
376
|
withRequest?<T>(work: () => Promise<T>): Promise<T>
|
|
372
|
-
// Upsert the identity; returns the user's opaque id (stable across logins)
|
|
377
|
+
// Upsert the identity; returns the user's opaque id (stable across logins),
|
|
378
|
+
// or null for an account linked to a user (github-links.ts), which neither
|
|
379
|
+
// signs in nor registers. A name chosen from a linked account is kept.
|
|
373
380
|
// Initial-admin approval comes only from trusted login configuration. It
|
|
374
381
|
// promotes a matching No access identity only while it is the sole user.
|
|
375
|
-
upsertUser(user: ManagedUser, now: number, initialAdminGithubId?: number | null): Promise<string>
|
|
382
|
+
upsertUser(user: ManagedUser, now: number, initialAdminGithubId?: number | null): Promise<string | null>
|
|
376
383
|
createSession(session: ManagedSession, now: number): Promise<void>
|
|
377
384
|
// A sign-in session; never a view session, so a view token in the session
|
|
378
385
|
// cookie authenticates nothing.
|
|
@@ -563,12 +570,14 @@ function prepareStatements(db: ManagedSql) {
|
|
|
563
570
|
// Identity refreshes preserve roles. Login can separately bootstrap the
|
|
564
571
|
// configured sole No access user within this same writer transaction.
|
|
565
572
|
upsertUserStmt: db.prepare(
|
|
566
|
-
`INSERT INTO managed_user (id, github_user_id, login, name, avatar_url, role, created_at, updated_at)
|
|
567
|
-
VALUES (?, ?, ?, ?, ?, 'none', ?, ?)
|
|
573
|
+
`INSERT INTO managed_user (id, github_user_id, login, name, github_name, avatar_url, role, created_at, updated_at)
|
|
574
|
+
VALUES (?, ?, ?, ?, ?, ?, 'none', ?, ?)
|
|
568
575
|
ON CONFLICT(github_user_id) DO UPDATE SET
|
|
569
|
-
login = excluded.login,
|
|
576
|
+
login = excluded.login, github_name = excluded.github_name,
|
|
577
|
+
name = CASE WHEN managed_user.name_link IS NULL THEN excluded.name ELSE managed_user.name END,
|
|
570
578
|
avatar_url = excluded.avatar_url, updated_at = excluded.updated_at`,
|
|
571
579
|
),
|
|
580
|
+
selectLinkedStmt: db.prepare(`SELECT 1 AS linked FROM managed_github_link WHERE github_user_id = ?`),
|
|
572
581
|
selectUserIdStmt: db.prepare(`SELECT id, role FROM managed_user WHERE github_user_id = ?`),
|
|
573
582
|
promoteInitialAdminStmt: db.prepare(
|
|
574
583
|
`UPDATE managed_user SET role = 'admin' WHERE id = ? AND role = 'none'
|
|
@@ -910,10 +919,15 @@ function prepareStatements(db: ManagedSql) {
|
|
|
910
919
|
renameTeamStmt: db.prepare(`UPDATE managed_team SET name = ?, updated_at = ? WHERE id = ?`),
|
|
911
920
|
setTeamHiddenStmt: db.prepare(`UPDATE managed_team SET hidden = ?, updated_at = ? WHERE id = ?`),
|
|
912
921
|
selectTeamsStmt: db.prepare(`SELECT id, slug, name, hidden FROM managed_team ORDER BY name ASC`),
|
|
922
|
+
// `dependencyReview` where the user reviews dependencies in the team
|
|
923
|
+
// (dependency-reviews.ts reviewsIn): one that reviews them, and a member
|
|
924
|
+
// who manages, or triages and may see its dependencies; an admin in any.
|
|
913
925
|
selectTeamsForUserStmt: db.prepare(
|
|
914
|
-
`SELECT t.id AS id, t.slug AS slug, t.name AS name,
|
|
926
|
+
`SELECT t.id AS id, t.slug AS slug, t.name AS name,
|
|
927
|
+
CASE WHEN t.dependency_review = 1 AND (u.role IN ('admin', 'manage') OR (u.role = 'triage' AND tu.view_dependencies = 1))
|
|
928
|
+
THEN 1 ELSE 0 END AS dependencyReview,
|
|
915
929
|
tu.view_dependencies AS dependencies, tu.view_security AS security
|
|
916
|
-
FROM managed_team_user tu JOIN managed_team t ON t.id = tu.team_id
|
|
930
|
+
FROM managed_team_user tu JOIN managed_team t ON t.id = tu.team_id JOIN managed_user u ON u.id = tu.user_id
|
|
917
931
|
WHERE tu.user_id = ? AND t.hidden = 0 ORDER BY t.name ASC, t.id ASC`,
|
|
918
932
|
),
|
|
919
933
|
selectUserTeamScopesStmt: db.prepare(
|
|
@@ -1513,6 +1527,8 @@ function teamMethods(stmts: ReturnType<typeof prepareStatements>, db: ManagedSql
|
|
|
1513
1527
|
cacheKey: createHash('sha256').update(JSON.stringify([
|
|
1514
1528
|
t.dependencies, t.security, links, scopes.filter(scope => scope.teamId === t.id),
|
|
1515
1529
|
])).digest('base64url'),
|
|
1530
|
+
repos: scopes.filter(scope => scope.teamId === t.id).map(scope => ({ repoId: scope.repoId, fullName: scope.fullName, path: scope.path || null }))
|
|
1531
|
+
.toSorted((a, b) => a.fullName.localeCompare(b.fullName) || (a.path ?? '').localeCompare(b.path ?? '')),
|
|
1516
1532
|
reports: reportsByTeam.get(t.id) ?? [],
|
|
1517
1533
|
bundles: bundlesByTeam.get(t.id) ?? [],
|
|
1518
1534
|
}))
|
|
@@ -1597,7 +1613,7 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1597
1613
|
const reports = reportMethods(stmts, db, key)
|
|
1598
1614
|
const triage = triageMethods(stmts, options.triageHistoryLimit ?? 0, db)
|
|
1599
1615
|
const {
|
|
1600
|
-
upsertUserStmt, selectUserIdStmt, promoteInitialAdminStmt, selectGithubIdStmt, insertSessionStmt, selectOwnSessionStmt, selectUsersStmt,
|
|
1616
|
+
upsertUserStmt, selectLinkedStmt, selectUserIdStmt, promoteInitialAdminStmt, selectGithubIdStmt, insertSessionStmt, selectOwnSessionStmt, selectUsersStmt,
|
|
1601
1617
|
touchUserSeenStmt, updateRoleStmt, updateTokensStmt, selectTokensStmt, ensureUploadKeyStmt, deleteSessionStmt, deleteExpiredStmt,
|
|
1602
1618
|
selectViewSessionStmt, deleteOtherViewsStmt, insertViewSessionStmt, deleteDemotedViewsStmt,
|
|
1603
1619
|
} = stmts
|
|
@@ -1607,7 +1623,9 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1607
1623
|
...linkReportMethods(db, key),
|
|
1608
1624
|
...repositoryAliasMethods(db),
|
|
1609
1625
|
...teamNpmScopeMethods(db),
|
|
1626
|
+
...githubLinkMethods(db),
|
|
1610
1627
|
...dependencyReviewMethods(db),
|
|
1628
|
+
...repositoryDependencyReviewMethods(db),
|
|
1611
1629
|
...bundleBuildLeaseMethods(db),
|
|
1612
1630
|
...upstreamCacheMethods(db),
|
|
1613
1631
|
async claimMaintenanceLease(owner, now, until, migration = false) {
|
|
@@ -1619,9 +1637,10 @@ export function createManagedMethods(db: ManagedSql, options: ManagedDbOptions =
|
|
|
1619
1637
|
await db.prepare('UPDATE managed_maintenance_lease SET expires_at = ? WHERE owner = ?').run(until, owner)
|
|
1620
1638
|
},
|
|
1621
1639
|
async upsertUser(user, now, initialAdminGithubId = null) {
|
|
1640
|
+
if (await selectLinkedStmt.get(user.githubUserId)) return null
|
|
1622
1641
|
// New row → a fresh id; ON CONFLICT(github_user_id) keeps an existing
|
|
1623
1642
|
// user's id (DO UPDATE leaves it untouched), so re-read to return it.
|
|
1624
|
-
await upsertUserStmt.run(randomUUID(), user.githubUserId, user.login, user.name, user.avatarUrl, now, now)
|
|
1643
|
+
await upsertUserStmt.run(randomUUID(), user.githubUserId, user.login, user.name, user.name, user.avatarUrl, now, now)
|
|
1625
1644
|
const row = (await selectUserIdStmt.get(user.githubUserId)) as { id: string; role: Role }
|
|
1626
1645
|
// Avoid the population check unless this login matches the configured
|
|
1627
1646
|
// identity and still needs approval. NOT EXISTS stops at any other user.
|
|
@@ -2,6 +2,8 @@ import { 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'
|
|
5
|
+
import { GITHUB_LINK_SCHEMA, GITHUB_LINK_USER_COLUMNS } from './github-links.ts'
|
|
6
|
+
import { REPOSITORY_DEPENDENCY_REVIEW_COLUMNS } from './repository-dependency-reviews.ts'
|
|
5
7
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
6
8
|
import { WebSocket } from 'ws'
|
|
7
9
|
import { type ManagedDb, type ManagedDbOptions, createManagedMethods } from './db-methods.ts'
|
|
@@ -36,7 +38,7 @@ async function currentSchema(db: PgConnection): Promise<boolean> {
|
|
|
36
38
|
const exists = (await db.query("SELECT to_regclass('managed_schema_version') AS name")).rows[0]?.['name']
|
|
37
39
|
if (!exists) return false
|
|
38
40
|
const versions = new Set((await db.query('SELECT version FROM managed_schema_version')).rows.map(row => Number(row['version'])))
|
|
39
|
-
return Array.from({ length:
|
|
41
|
+
return Array.from({ length: 30 }, (_, i) => i + 1).every(version => versions.has(version))
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
async function migrateRepositoryDefaultCache(db: PgConnection): Promise<void> {
|
|
@@ -142,6 +144,31 @@ async function migrateDependencyReviews(db: PgConnection): Promise<void> {
|
|
|
142
144
|
await db.query('INSERT INTO managed_schema_version VALUES (27)')
|
|
143
145
|
}
|
|
144
146
|
|
|
147
|
+
// Existing names are the sign-in accounts' own: none was chosen yet.
|
|
148
|
+
async function migrateGithubLinks(db: PgConnection): Promise<void> {
|
|
149
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 28')).rows.length > 0) return
|
|
150
|
+
await db.query(postgresSchema(GITHUB_LINK_SCHEMA))
|
|
151
|
+
await db.query(`ALTER TABLE managed_user ${GITHUB_LINK_USER_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
152
|
+
await db.query('UPDATE managed_user SET github_name = name')
|
|
153
|
+
await db.query('INSERT INTO managed_schema_version VALUES (28)')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function migrateRepositoryDependencyReviews(db: PgConnection): Promise<void> {
|
|
157
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 29')).rows.length > 0) return
|
|
158
|
+
await db.query(`ALTER TABLE managed_selected_repo ${REPOSITORY_DEPENDENCY_REVIEW_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
159
|
+
await db.query('INSERT INTO managed_schema_version VALUES (29)')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The lockfiles a repository's dependency review holds to, its columns
|
|
163
|
+
// added since 29 added there too; and a team's published reviews, listed by
|
|
164
|
+
// it, indexed so.
|
|
165
|
+
async function migrateRepositoryDependencyReviewLockfiles(db: PgConnection): Promise<void> {
|
|
166
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 30')).rows.length > 0) return
|
|
167
|
+
await db.query(`ALTER TABLE managed_selected_repo ${REPOSITORY_DEPENDENCY_REVIEW_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
168
|
+
await db.query('CREATE INDEX IF NOT EXISTS managed_dependency_review_team_idx ON managed_dependency_review_team(team_id, review_id)')
|
|
169
|
+
await db.query('INSERT INTO managed_schema_version VALUES (30)')
|
|
170
|
+
}
|
|
171
|
+
|
|
145
172
|
async function initialize(db: PgConnection): Promise<void> {
|
|
146
173
|
if (await currentSchema(db)) return
|
|
147
174
|
await db.query('BEGIN')
|
|
@@ -215,7 +242,8 @@ async function initialize(db: PgConnection): Promise<void> {
|
|
|
215
242
|
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 11')).rows.length === 0) {
|
|
216
243
|
await db.query(postgresSchema(revisionSchema(true)) + '; INSERT INTO managed_schema_version VALUES (11)')
|
|
217
244
|
}
|
|
218
|
-
for (const migrate of [migrateRepositoryDefaultCache, migrateBundleBuildLeases, migrateBundleVisibility, migrateLinkReports, migrateRepositoryAliases, migrateHiddenTeams, migrateIssueFixes, migrateBundleProvenance, migrateUpstreamCache, migrateBundleBuildConditions, migrateSessionUploadKeys, migrateViewSessions, migrateGithubCommits, migrateTeamNpmScopes, migrateDependencyReviews
|
|
245
|
+
for (const migrate of [migrateRepositoryDefaultCache, migrateBundleBuildLeases, migrateBundleVisibility, migrateLinkReports, migrateRepositoryAliases, migrateHiddenTeams, migrateIssueFixes, migrateBundleProvenance, migrateUpstreamCache, migrateBundleBuildConditions, migrateSessionUploadKeys, migrateViewSessions, migrateGithubCommits, migrateTeamNpmScopes, migrateDependencyReviews, migrateGithubLinks, migrateRepositoryDependencyReviews,
|
|
246
|
+
migrateRepositoryDependencyReviewLockfiles]) await migrate(db)
|
|
219
247
|
await db.query('COMMIT')
|
|
220
248
|
} catch (err) {
|
|
221
249
|
await db.query('ROLLBACK')
|
|
@@ -3,6 +3,7 @@ import { LINK_REPORT_SCHEMA } from './link-reports.ts'
|
|
|
3
3
|
import { REPOSITORY_ALIAS_SCHEMA } from './repository-aliases.ts'
|
|
4
4
|
import { WORKSPACE_SHARE_SCHEMA } from './workspace-shares.ts'
|
|
5
5
|
import { TEAM_NPM_SCOPE_SCHEMA } from './team-npm-scopes.ts'
|
|
6
|
+
import { GITHUB_LINK_SCHEMA } from './github-links.ts'
|
|
6
7
|
|
|
7
8
|
// Created after the column migration, which predates it on older databases.
|
|
8
9
|
export const VIEW_SESSION_INDEX = 'CREATE INDEX IF NOT EXISTS managed_session_viewer_idx ON managed_session(viewer_session);'
|
|
@@ -64,7 +65,13 @@ CREATE TABLE IF NOT EXISTS managed_selected_repo (
|
|
|
64
65
|
added_by TEXT REFERENCES managed_user(id) ON DELETE SET NULL,
|
|
65
66
|
added_at INTEGER NOT NULL,
|
|
66
67
|
updated_at INTEGER NOT NULL,
|
|
67
|
-
active INTEGER NOT NULL DEFAULT 1
|
|
68
|
+
active INTEGER NOT NULL DEFAULT 1,
|
|
69
|
+
-- Its pull requests' dependencies reviewed (repository-dependency-reviews.ts):
|
|
70
|
+
-- as many published reviews required of each version, and the commit at
|
|
71
|
+
-- whose lockfiles' versions none is (null for none).
|
|
72
|
+
dependency_review INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
dependency_reviews_required INTEGER NOT NULL DEFAULT 1,
|
|
74
|
+
dependency_review_since TEXT
|
|
68
75
|
) STRICT;
|
|
69
76
|
|
|
70
77
|
CREATE INDEX IF NOT EXISTS selected_repo_full_name_idx ON managed_selected_repo(full_name);
|
|
@@ -224,4 +231,4 @@ CREATE TABLE IF NOT EXISTS managed_team_user (
|
|
|
224
231
|
) STRICT;
|
|
225
232
|
|
|
226
233
|
CREATE INDEX IF NOT EXISTS team_user_user_idx ON managed_team_user(user_id);
|
|
227
|
-
` + WORKSPACE_SHARE_SCHEMA + LINK_REPORT_SCHEMA + REPOSITORY_ALIAS_SCHEMA + TEAM_NPM_SCOPE_SCHEMA + DEPENDENCY_REVIEW_SCHEMA
|
|
234
|
+
` + WORKSPACE_SHARE_SCHEMA + LINK_REPORT_SCHEMA + REPOSITORY_ALIAS_SCHEMA + TEAM_NPM_SCOPE_SCHEMA + DEPENDENCY_REVIEW_SCHEMA + GITHUB_LINK_SCHEMA
|
package/server-managed/db.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { initCommentMethods } from './comments.ts'
|
|
|
8
8
|
import { initActivityMethods } from './activity.ts'
|
|
9
9
|
import { revisionSchema } from './revisions.ts'
|
|
10
10
|
import { MANAGED_SCHEMA, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
11
|
+
import { GITHUB_LINK_USER_COLUMNS } from './github-links.ts'
|
|
12
|
+
import { REPOSITORY_DEPENDENCY_REVIEW_COLUMNS } from './repository-dependency-reviews.ts'
|
|
11
13
|
import { STORAGE_SCHEMA } from './storage-db.ts'
|
|
12
14
|
import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
13
15
|
import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
|
|
@@ -79,6 +81,10 @@ export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}
|
|
|
79
81
|
ensureColumn(db, table, 'storage_encrypted', 'INTEGER NOT NULL DEFAULT 0')
|
|
80
82
|
}
|
|
81
83
|
ensureColumn(db, 'managed_user', 'gh_tokens_encrypted', 'INTEGER NOT NULL DEFAULT 0')
|
|
84
|
+
// Existing names are the sign-in accounts' own: none was chosen yet.
|
|
85
|
+
for (const [column, type] of GITHUB_LINK_USER_COLUMNS) {
|
|
86
|
+
if (ensureColumn(db, 'managed_user', column, type) && column === 'github_name') db.exec('UPDATE managed_user SET github_name = name')
|
|
87
|
+
}
|
|
82
88
|
ensureColumn(db, 'managed_github_metadata', 'state_reason', GITHUB_STATE_REASON_COLUMN)
|
|
83
89
|
ensureColumn(db, 'managed_github_metadata', 'attempted_at', 'INTEGER')
|
|
84
90
|
// Existing public links must opt in too; never preserve an implicit grant.
|
|
@@ -116,6 +122,7 @@ export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}
|
|
|
116
122
|
ensureColumn(db, 'managed_finding_issue', 'auto_fix_url', 'TEXT')
|
|
117
123
|
ensureColumn(db, 'managed_finding_issue', 'auto_fix_checked_at', 'INTEGER')
|
|
118
124
|
ensureColumn(db, 'managed_selected_repo', 'active', 'INTEGER NOT NULL DEFAULT 1')
|
|
125
|
+
for (const [column, type] of REPOSITORY_DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_selected_repo', column, type)
|
|
119
126
|
ensureColumn(db, 'managed_finding_triage_event', 'report_id', 'TEXT')
|
|
120
127
|
ensureColumn(db, 'managed_finding_triage_event', 'report', 'TEXT')
|
|
121
128
|
ensureColumn(db, 'managed_finding_triage_event', 'repo', 'TEXT')
|