@preventive/triage 1.0.0-alpha.50 → 1.0.0-alpha.52
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/managed/finding-detail.d.ts +2 -1
- package/common/managed/finding-detail.js +42 -11
- package/common/managed/finding-values.d.ts +2 -0
- package/common/managed/finding-values.js +44 -0
- package/common/managed/lite-name.d.ts +2 -0
- package/common/managed/lite-name.js +20 -0
- package/common/managed/npm-packages.d.ts +2 -0
- package/common/managed/npm-packages.js +18 -1
- package/common/managed/routes.js +19 -7
- package/common/managed/team-app.d.ts +1 -0
- package/common/managed/team-app.js +17 -1
- package/out/activity.js +4 -4
- package/out/{chunk-75YLHBCM.js → chunk-2K6UN3PC.js} +1 -1
- package/out/{chunk-HTBYHSPG.js → chunk-CHRBUGVN.js} +1 -1
- package/out/{chunk-4XMP7YVH.js → chunk-FMXWXDR4.js} +1 -1
- package/out/chunk-HT2SSA3V.js +1 -0
- package/out/{chunk-2WDZPXVL.js → chunk-IMM274IW.js} +1 -1
- package/out/{chunk-FKBXNUW4.js → chunk-KOGNQDSJ.js} +1 -1
- package/out/chunk-LJ7XRXFU.js +52 -0
- package/out/{chunk-PBZPYIG7.js → chunk-MYGJBYYJ.js} +1 -1
- package/out/chunk-NCN3M5EP.js +3 -0
- package/out/chunk-OGAYMEER.js +232 -0
- package/out/client-managed-import.js +1 -1
- package/out/client-managed.js +26 -26
- package/out/compare.js +14 -14
- package/out/dialogs.js +31 -31
- package/out/graph.js +1 -1
- package/out/index.html +7 -7
- package/out/scan.js +2 -2
- package/out/view.css +1 -1
- package/out/view.js +11 -11
- package/package.json +8 -1
- package/server-managed/agent-merges.ts +1 -1
- package/server-managed/app-agents.ts +4 -2
- package/server-managed/commit-lockfiles.ts +7 -4
- package/server-managed/comparison.ts +13 -2
- package/server-managed/db-neon.ts +13 -3
- package/server-managed/db.ts +2 -1
- package/server-managed/dependency-review-comments.ts +16 -18
- package/server-managed/dependency-reviews.ts +45 -27
- package/server-managed/github-app.ts +3 -42
- package/server-managed/github-requests.ts +127 -0
- package/server-managed/http.ts +433 -129
- package/server-managed/listed-activity.ts +39 -0
- package/server-managed/locked-packages.ts +28 -5
- package/server-managed/npm-kept.ts +18 -4
- package/server-managed/npm-patch.ts +325 -0
- package/server-managed/pull-dependencies.ts +9 -6
- package/server-managed/pull-dependency-reviews.ts +19 -13
- package/server-managed/repository-agents.ts +162 -69
- package/server-managed/repository-dependency-reviews.ts +6 -5
- package/server-managed/repository-pulls.ts +114 -58
- package/server-managed/repository-pulse.ts +5 -3
- package/server-managed/repository-stamp.ts +41 -0
- package/server-managed/reviewed-dependencies.ts +4 -5
- package/server-managed/team-app.ts +3 -1
- package/server-managed/team-reports.ts +29 -10
- package/out/chunk-6LNQTKAV.js +0 -52
- package/out/chunk-6S2U2UYG.js +0 -232
- package/out/chunk-FC65TPUB.js +0 -1
- package/out/chunk-UK7RGJT6.js +0 -3
|
@@ -16,19 +16,17 @@
|
|
|
16
16
|
// an agent, and those alone are answered, never the texts. GraphQL alone
|
|
17
17
|
// gives them, so it needs a token.
|
|
18
18
|
import { type AgentAuthor, type AgentBranch, type AgentCommit, type AgentIssue, type AgentPull, type AgentSignals, type AppAgent, agentSignals } from '../common/managed/agent-tasks.ts'
|
|
19
|
-
import { GITHUB_AGENTS, type GithubAgentAccount, type GithubAgentId } from '../common/managed/github-agents.ts'
|
|
19
|
+
import { GITHUB_AGENTS, type GithubAgentAccount, type GithubAgentId, githubAgent } from '../common/managed/github-agents.ts'
|
|
20
20
|
import { AGENT_BRANCH_PREFIXES, APP_AGENTS } from './app-agents.ts'
|
|
21
|
-
import { GithubApiError } from './github-app.ts'
|
|
22
21
|
import type { GraphqlError } from './github-graphql.ts'
|
|
23
22
|
import { ACTOR, ALL_BRANCHES, type HistoryCommit, type HistoryPages, type NamedAccount, actorOf, commitAccount, commitMarks, githubGraphql, historyFrom, historyMarks,
|
|
24
23
|
inBatches, linkNoreplyUsers, messageMarks, variablesOf } from './repository-history.ts'
|
|
25
|
-
import type { PullList } from './repository-pulls.ts'
|
|
24
|
+
import type { IssueList, PullList } from './repository-pulls.ts'
|
|
26
25
|
|
|
27
26
|
export const AGENT_PULLS_PAGE = 50
|
|
28
27
|
export const AGENT_COMMITS_PAGE = 100
|
|
29
28
|
const AGENT_ISSUES = 100
|
|
30
29
|
const AGENT_BRANCHES = 500
|
|
31
|
-
const MAX_BRANCH_PAGES = 50
|
|
32
30
|
const CLOSES = 5
|
|
33
31
|
// A pull request's labels read, for one naming an agent: GitHub's most a
|
|
34
32
|
// page.
|
|
@@ -46,25 +44,36 @@ const PULLED_PARALLEL = 4
|
|
|
46
44
|
const AGENT_EMAILS = APP_AGENTS.flatMap(({ id }) => GITHUB_AGENTS[id as GithubAgentId].accounts
|
|
47
45
|
.flatMap(({ id: account, logins }: GithubAgentAccount) => account === undefined ? [] : logins.map(login => `${account}+${login}@users.noreply.github.com`)))
|
|
48
46
|
|
|
49
|
-
//
|
|
50
|
-
|
|
47
|
+
// Branches' tip commits, `c<n>` the `n`th's: each's message and author.
|
|
48
|
+
const tipsQuery = (count: number) => `query($owner: String!, $name: String!, ${variablesOf('o', count, 'GitObjectID!')}) {
|
|
49
|
+
repository(owner: $owner, name: $name) {
|
|
50
|
+
${Array.from({ length: count }, (_, at) => `c${at}: object(oid: $o${at}) { ... on Commit { message author { email user { login databaseId } } } }`).join('\n ')}
|
|
51
|
+
}
|
|
52
|
+
}`
|
|
53
|
+
// Tips looked up a query, and queries at once.
|
|
54
|
+
const TIPS = 100
|
|
55
|
+
const TIPS_PARALLEL = 4
|
|
56
|
+
// An agent's branches, a page at a time, as GitHub lists them by name, where
|
|
57
|
+
// the repository's branches as kept stop short of them (`cut`): each its tip
|
|
58
|
+
// and when that was committed; as many pages of each as MAX_BRANCH_PAGES.
|
|
51
59
|
const BRANCHES_QUERY = `query($owner: String!, $name: String!, $prefix: String!, $after: String) {
|
|
52
60
|
repository(owner: $owner, name: $name) {
|
|
53
61
|
refs(refPrefix: $prefix, first: 100, after: $after) {
|
|
54
62
|
pageInfo { hasNextPage endCursor }
|
|
55
|
-
nodes { prefix name target { ... on Commit { committedDate
|
|
63
|
+
nodes { prefix name target { oid ... on Commit { committedDate } } }
|
|
56
64
|
}
|
|
57
65
|
}
|
|
58
66
|
}`
|
|
67
|
+
const MAX_BRANCH_PAGES = 50
|
|
59
68
|
// What a commit of the default branch is read by.
|
|
60
69
|
const COMMIT_FIELDS = 'oid messageHeadline message committedDate additions deletions author { email user { login databaseId } } associatedPullRequests(first: 1) { totalCount }'
|
|
61
|
-
// The newest of the default branch's
|
|
62
|
-
// authored, by their GitHub no-reply addresses, however far back: a
|
|
63
|
-
// commits pushed with no pull request here (as before a repository
|
|
64
|
-
// may lie deeper than pages are read on to.
|
|
65
|
-
const AUTHORED_QUERY = `query($owner: String!, $name: String!, $emails: [String!]!) {
|
|
70
|
+
// The newest of the commits from the default branch's head the app agents'
|
|
71
|
+
// accounts authored, by their GitHub no-reply addresses, however far back: a
|
|
72
|
+
// bot's commits pushed with no pull request here (as before a repository
|
|
73
|
+
// moved) may lie deeper than pages are read on to.
|
|
74
|
+
const AUTHORED_QUERY = `query($owner: String!, $name: String!, $head: GitObjectID!, $emails: [String!]!) {
|
|
66
75
|
repository(owner: $owner, name: $name) {
|
|
67
|
-
|
|
76
|
+
object(oid: $head) { ... on Commit { history(first: ${AGENT_COMMITS_PAGE}, author: { emails: $emails }) { nodes { ${COMMIT_FIELDS} } } } }
|
|
68
77
|
}
|
|
69
78
|
}`
|
|
70
79
|
// What a pull request is read by, for its row (pull): its texts for the
|
|
@@ -74,13 +83,12 @@ const AUTHORED_QUERY = `query($owner: String!, $name: String!, $emails: [String!
|
|
|
74
83
|
export const AGENT_PULL_FIELDS = `number title state isDraft createdAt updatedAt author { __typename ${ACTOR} } body headRefName isCrossRepository
|
|
75
84
|
headRepositoryOwner { login } additions deletions last: commits(last: 1) { nodes { commit { message } } } mergeCommit { message }
|
|
76
85
|
closingIssuesReferences(first: ${CLOSES}) { nodes { number } } labels(first: ${LABELS}) { nodes { name } }`
|
|
77
|
-
//
|
|
78
|
-
const
|
|
86
|
+
// What an issue is read by, for its row (issue).
|
|
87
|
+
export const AGENT_ISSUE_FIELDS = `number title state createdAt updatedAt author { __typename ${ACTOR} }`
|
|
88
|
+
// The latest issues, however long ago they were updated.
|
|
89
|
+
const ISSUES_QUERY = `query($owner: String!, $name: String!) {
|
|
79
90
|
repository(owner: $owner, name: $name) {
|
|
80
|
-
|
|
81
|
-
issues(first: ${AGENT_ISSUES}, orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
82
|
-
nodes { number title state createdAt updatedAt author { ${ACTOR} } }
|
|
83
|
-
}
|
|
91
|
+
issues(first: ${AGENT_ISSUES}, orderBy: { field: UPDATED_AT, direction: DESC }) { nodes { ${AGENT_ISSUE_FIELDS} } }
|
|
84
92
|
}
|
|
85
93
|
}`
|
|
86
94
|
// A page of the pull requests a search finds, as GitHub sorts them, on from
|
|
@@ -99,8 +107,7 @@ export type PullNode = { number?: unknown; title?: unknown; state?: unknown; isD
|
|
|
99
107
|
type GitActor = { email?: unknown; user?: Login }
|
|
100
108
|
type CommitNode = { oid?: unknown; messageHeadline?: unknown; message?: unknown; committedDate?: unknown; additions?: unknown; deletions?: unknown
|
|
101
109
|
author?: GitActor | null; associatedPullRequests?: { totalCount?: unknown } }
|
|
102
|
-
type IssueNode = { number?: unknown; title?: unknown; state?: unknown; createdAt?: unknown; updatedAt?: unknown; author?: Login }
|
|
103
|
-
type RefNode = { prefix?: unknown; name?: unknown; target?: { committedDate?: unknown; message?: unknown; author?: GitActor | null } | null }
|
|
110
|
+
export type IssueNode = { number?: unknown; title?: unknown; state?: unknown; createdAt?: unknown; updatedAt?: unknown; author?: Login }
|
|
104
111
|
type Connection<T> = { pageInfo?: { hasNextPage?: unknown; endCursor?: unknown }; nodes?: (T | null)[] }
|
|
105
112
|
// Whether a pull request brought each commit, `c<n>` the `n`th's.
|
|
106
113
|
const pulledQuery = (count: number) => `query($owner: String!, $name: String!, ${variablesOf('o', count, 'GitObjectID!')}) {
|
|
@@ -109,10 +116,12 @@ const pulledQuery = (count: number) => `query($owner: String!, $name: String!, $
|
|
|
109
116
|
}
|
|
110
117
|
}`
|
|
111
118
|
type Pulled = { repository?: Record<string, { associatedPullRequests?: { totalCount?: unknown } } | null> | null }
|
|
112
|
-
type
|
|
113
|
-
|
|
114
|
-
type
|
|
119
|
+
type Authored = { repository?: { object?: { history?: Connection<CommitNode> } | null } | null }
|
|
120
|
+
type Issues = { repository?: { issues?: Connection<IssueNode> } | null }
|
|
121
|
+
type Tips = { repository?: Record<string, { message?: unknown; author?: GitActor | null } | null> | null }
|
|
122
|
+
type RefNode = { prefix?: unknown; name?: unknown; target?: { oid?: unknown; committedDate?: unknown } | null }
|
|
115
123
|
type RefsPage = { repository?: { refs?: Connection<RefNode> } | null }
|
|
124
|
+
type Found = { search?: { pageInfo?: { endCursor?: unknown }; nodes?: (PullNode | null)[] } | null } | null
|
|
116
125
|
|
|
117
126
|
const count = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0
|
|
118
127
|
const number = (value: unknown) => Number.isSafeInteger(value) && (value as number) > 0 ? value as number : null
|
|
@@ -147,12 +156,17 @@ const accountOf = (actor: Login) => both(actorOf(actor))
|
|
|
147
156
|
// Dependabot's, is told by that alone (repository-history.ts commitAccount).
|
|
148
157
|
const authorOf = (actor: GitActor | null | undefined) => both(commitAccount(actor))
|
|
149
158
|
|
|
159
|
+
// A commit the app agents' accounts authored, as kept: as read, its
|
|
160
|
+
// message's marks (messageMarks) in its place, never the message.
|
|
161
|
+
export type AuthoredCommit = Omit<CommitNode, 'message'> & { marks: AgentSignals | null }
|
|
162
|
+
const authoredOf = ({ message, ...node }: CommitNode): AuthoredCommit => ({ ...node, marks: messageMarks(message) })
|
|
163
|
+
|
|
150
164
|
// A commit to the default branch no pull request merged, where an agent left
|
|
151
165
|
// its marks on it, or Claude authored it (commitMarks).
|
|
152
|
-
function directCommit(node:
|
|
166
|
+
function directCommit(node: AuthoredCommit | null): AgentCommit | null {
|
|
153
167
|
if (!node || typeof node.oid !== 'string' || !SHA.test(node.oid) || count(node.associatedPullRequests?.totalCount) > 0) return null
|
|
154
168
|
const author = authorOf(node.author)
|
|
155
|
-
const signals = commitMarks(
|
|
169
|
+
const signals = commitMarks(node.marks, { name: null, email: node.author?.email, account: commitAccount(node.author) })
|
|
156
170
|
if (!signals) return null
|
|
157
171
|
return { oid: node.oid, title: nameOf(node.messageHeadline, MAX_TITLE) ?? '', at: timeOf(node.committedDate),
|
|
158
172
|
author: author?.login ?? loginOf(node.author?.user), authorId: author?.id ?? null,
|
|
@@ -209,38 +223,96 @@ function frontier(commits: HistoryCommit[], left: string[]) {
|
|
|
209
223
|
return starts.length > 0 ? starts.join(',') : null
|
|
210
224
|
}
|
|
211
225
|
|
|
212
|
-
|
|
226
|
+
// An issue's row: what it is, and who opened it.
|
|
227
|
+
export function issue(node: IssueNode | null): AgentIssue | null {
|
|
213
228
|
const at = number(node?.number)
|
|
214
229
|
if (!node || !at) return null
|
|
215
230
|
return { number: at, title: nameOf(node.title, MAX_TITLE) ?? '', state: node.state === 'OPEN' ? 'open' : 'closed', author: loginOf(node.author),
|
|
216
231
|
authorId: accountOf(node.author)?.id ?? null, createdAt: timeOf(node.createdAt), updatedAt: timeOf(node.updatedAt) }
|
|
217
232
|
}
|
|
218
233
|
|
|
219
|
-
// A branch
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
234
|
+
// A branch's tip, as kept: its message's marks (messageMarks), never the
|
|
235
|
+
// message, and its author as read.
|
|
236
|
+
export interface BranchTip { marks: AgentSignals | null; author: GitActor | null }
|
|
237
|
+
// The tips of branches, kept by their commits, which never change.
|
|
238
|
+
export interface BranchTips {
|
|
239
|
+
read(shas: string[]): Promise<Map<string, BranchTip>>
|
|
240
|
+
write(sha: string, tip: BranchTip): Promise<void>
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// A branch an agent's prefix names (`name`), its tip (`sha`) committed `at`,
|
|
244
|
+
// as the Agents tab shows it: its tip's author, and the marks an agent left
|
|
245
|
+
// on it, its tip's message's, its author's account an agent's, or its name
|
|
246
|
+
// under an agent's prefix, as agent-tasks.ts agentSignals tells them.
|
|
247
|
+
function agentBranch({ name, at }: { name: string; at: number | null }, tip: BranchTip | undefined): AgentBranch {
|
|
248
|
+
const author = authorOf(tip?.author)
|
|
249
|
+
const authored = author ? githubAgent(author.login, author.id) : null
|
|
250
|
+
const marks = tip?.marks
|
|
251
|
+
return { name, at, author: author?.login ?? loginOf(tip?.author?.user), authorId: author?.id ?? null, signals: { sessions: marks?.sessions ?? [],
|
|
252
|
+
tasks: marks?.tasks ?? [], claude: marks?.claude ?? false, codex: marks?.codex ?? false,
|
|
253
|
+
agents: APP_AGENTS.flatMap(({ id, prefix }) => marks?.agents.includes(id) || authored === id || name.startsWith(prefix) ? [id] : []) } }
|
|
227
254
|
}
|
|
228
255
|
|
|
229
|
-
// The
|
|
230
|
-
//
|
|
231
|
-
async function
|
|
232
|
-
const found:
|
|
256
|
+
// The branches under `prefix`, every one, as many as MAX_BRANCH_PAGES of
|
|
257
|
+
// them list, named in full however GitHub splits its name from the prefix.
|
|
258
|
+
async function prefixHeads(owner: string, name: string, prefix: string, token: string, fetchImpl: typeof fetch) {
|
|
259
|
+
const found: Head[] = []
|
|
233
260
|
let after: string | null = null
|
|
234
261
|
for (let page = 0; page < MAX_BRANCH_PAGES; page++) {
|
|
235
262
|
const refs: Connection<RefNode> | undefined = (await githubGraphql(BRANCHES_QUERY, { owner, name, prefix: `refs/heads/${prefix}`, after }, token, fetchImpl) as RefsPage)
|
|
236
263
|
?.repository?.refs
|
|
237
|
-
|
|
238
|
-
|
|
264
|
+
for (const node of refs?.nodes ?? []) {
|
|
265
|
+
const ref = typeof node?.prefix === 'string' && typeof node.name === 'string' ? `${node.prefix}${node.name}` : null
|
|
266
|
+
const branch = ref?.startsWith('refs/heads/') ? nameOf(ref.slice('refs/heads/'.length)) : null
|
|
267
|
+
const sha = node?.target?.oid
|
|
268
|
+
if (branch?.startsWith(prefix) && typeof sha === 'string' && SHA.test(sha)) found.push({ name: branch, sha, at: timeOf(node!.target!.committedDate) })
|
|
269
|
+
}
|
|
239
270
|
const next: Connection<RefNode>['pageInfo'] = refs?.pageInfo
|
|
240
271
|
if (next?.hasNextPage !== true || typeof next.endCursor !== 'string') break
|
|
241
272
|
after = next.endCursor
|
|
242
273
|
}
|
|
243
|
-
return found
|
|
274
|
+
return found
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// The newest of the agents' branches, those each prefix names, as many as
|
|
278
|
+
// AGENT_BRANCHES of each, of the repository's branches as kept (`heads`, as
|
|
279
|
+
// the Commits tab reads them; those it stops short of, `cut`, listed by each
|
|
280
|
+
// prefix), their tips as kept (`tips`), those not looked up, TIPS a query,
|
|
281
|
+
// and kept.
|
|
282
|
+
async function agentBranches(fullName: string, token: string, { heads, cut }: { heads: Head[]; cut: boolean }, tips: BranchTips, fetchImpl: typeof fetch) {
|
|
283
|
+
const [owner, name] = fullName.split('/') as [string, string]
|
|
284
|
+
const all = cut ? (await Promise.all(AGENT_BRANCH_PREFIXES.map(prefix => prefixHeads(owner, name, prefix, token, fetchImpl)))).flat() : heads
|
|
285
|
+
const named = AGENT_BRANCH_PREFIXES.flatMap(prefix => all.filter(head => head.name.startsWith(prefix))
|
|
286
|
+
.toSorted((a, b) => (b.at ?? 0) - (a.at ?? 0)).slice(0, AGENT_BRANCHES))
|
|
287
|
+
const shown = named.filter((head, index) => named.findIndex(other => other.name === head.name) === index)
|
|
288
|
+
const shas = [...new Set(shown.map(head => head.sha))]
|
|
289
|
+
const known = await tips.read(shas)
|
|
290
|
+
await inBatches(shas.filter(sha => !known.has(sha)), TIPS, TIPS_PARALLEL, async batch => {
|
|
291
|
+
const data = await githubGraphql(tipsQuery(batch.length), { owner, name, ...Object.fromEntries(batch.map((sha, at) => [`o${at}`, sha])) }, token, fetchImpl,
|
|
292
|
+
notFound) as Tips
|
|
293
|
+
await linkNoreplyUsers(batch.map((_, at) => data?.repository?.[`c${at}`]?.author), token, fetchImpl)
|
|
294
|
+
await Promise.all(batch.map((sha, at) => {
|
|
295
|
+
const node = data?.repository?.[`c${at}`]
|
|
296
|
+
if (!node) return Promise.resolve()
|
|
297
|
+
known.set(sha, { marks: messageMarks(node.message), author: node.author ?? null })
|
|
298
|
+
return tips.write(sha, known.get(sha)!)
|
|
299
|
+
}))
|
|
300
|
+
})
|
|
301
|
+
return shown.map(head => agentBranch(head, known.get(head.sha)))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// The commits from `head` the app agents' accounts authored (AUTHORED_QUERY),
|
|
305
|
+
// as kept by it (`kept`), else read and kept.
|
|
306
|
+
async function authoredCommits(fullName: string, token: string, head: string, kept: AuthoredCommits, fetchImpl: typeof fetch) {
|
|
307
|
+
const [owner, name] = fullName.split('/') as [string, string]
|
|
308
|
+
const key = `${head}\0${AGENT_EMAILS.join(',')}`
|
|
309
|
+
const known = await kept.read(key)
|
|
310
|
+
if (known) return known
|
|
311
|
+
const data = await githubGraphql(AUTHORED_QUERY, { owner, name, head, emails: AGENT_EMAILS }, token, fetchImpl) as Authored
|
|
312
|
+
const nodes = (data?.repository?.object?.history?.nodes ?? []).flatMap(node => node ? [authoredOf(node)] : [])
|
|
313
|
+
await linkNoreplyUsers(nodes.map(node => node.author), token, fetchImpl)
|
|
314
|
+
await kept.write(key, nodes)
|
|
315
|
+
return nodes
|
|
244
316
|
}
|
|
245
317
|
|
|
246
318
|
// What its answers are kept by, with their cursors (http.ts
|
|
@@ -269,17 +341,31 @@ const positionOf = (item: AgentPull) => `${item.updatedAt ?? 0}.${item.number}`
|
|
|
269
341
|
// where it names none.
|
|
270
342
|
const updatedBy = (at: number) => Number.isFinite(at) ? ` updated:<=${new Date(at).toISOString().replace(/\.\d{3}Z$/u, '+00:00')}` : ''
|
|
271
343
|
|
|
272
|
-
//
|
|
344
|
+
// A branch: its name, its tip, and when that was committed.
|
|
345
|
+
type Head = { name: string; sha: string; at: number | null }
|
|
346
|
+
// The commits the app agents' accounts authored from a head (AUTHORED_QUERY),
|
|
347
|
+
// kept by it and the addresses they were asked by (`key`).
|
|
348
|
+
export interface AuthoredCommits {
|
|
349
|
+
read(key: string): Promise<AuthoredCommit[] | null>
|
|
350
|
+
write(key: string, commits: AuthoredCommit[]): Promise<void>
|
|
351
|
+
}
|
|
352
|
+
// What the repository's tabs read and keep together (http.ts): its pull
|
|
353
|
+
// requests and its issues (repository-pulls.ts PullList, IssueList), each
|
|
273
354
|
// read as far back as their first page where none are, and those updated
|
|
274
|
-
// since read as they are (
|
|
275
|
-
|
|
355
|
+
// since read as they are (keptList), its default branch's head (defaultTip),
|
|
356
|
+
// its branches, as the Commits tab reads them (`heads`, whether they stop
|
|
357
|
+
// short of some, `cut`), their tips (`tips`), and the commits its app
|
|
358
|
+
// agents' accounts authored (`authored`).
|
|
359
|
+
export interface Kept { pulls(): Promise<PullList>; issues(): Promise<IssueList>; head(): Promise<string | null>; heads(): Promise<{ heads: Head[]; cut: boolean }>
|
|
360
|
+
tips: BranchTips
|
|
361
|
+
authored: AuthoredCommits }
|
|
276
362
|
|
|
277
363
|
// A page of the repository's pull requests, the most recently updated
|
|
278
364
|
// first, below the one `cursor` names (none: from the newest): of those kept
|
|
279
365
|
// (`kept`), as far back as every one is read; past them, as GitHub's search
|
|
280
366
|
// finds those updated before, a page a query. With where it reads on from,
|
|
281
367
|
// null where none are left: past every one kept where all are.
|
|
282
|
-
async function agentPulls(fullName: string, token: string, cursor: string | null, kept:
|
|
368
|
+
async function agentPulls(fullName: string, token: string, cursor: string | null, kept: Kept['pulls'], fetchImpl: typeof fetch) {
|
|
283
369
|
const at = cursor === null ? null : POSITION.exec(cursor)
|
|
284
370
|
const [time, past] = at ? [Number(at[1]), Number(at[2])] : [Number.POSITIVE_INFINITY, 0]
|
|
285
371
|
const below = (item: AgentPull) => (item.updatedAt ?? 0) < time || ((item.updatedAt ?? 0) === time && item.number < past)
|
|
@@ -290,7 +376,7 @@ async function agentPulls(fullName: string, token: string, cursor: string | null
|
|
|
290
376
|
const read = list.next ? list.next.to ?? Number.POSITIVE_INFINITY : list.reach ?? Number.NEGATIVE_INFINITY
|
|
291
377
|
// As far back as every one is read: the year's start, where all since are.
|
|
292
378
|
const bottom = Math.max(read, list.since)
|
|
293
|
-
const listed = list.
|
|
379
|
+
const listed = list.rows.filter(item => below(item) && (item.updatedAt ?? 0) >= bottom)
|
|
294
380
|
const whole = list.all === true && read === Number.NEGATIVE_INFINITY
|
|
295
381
|
if (listed.length > 0) {
|
|
296
382
|
const page = listed.slice(0, AGENT_PULLS_PAGE)
|
|
@@ -311,43 +397,50 @@ async function agentPulls(fullName: string, token: string, cursor: string | null
|
|
|
311
397
|
return { pulls: rows.filter(item => item.updatedAt !== time || item.number !== past), after: after === cursor ? null : after }
|
|
312
398
|
}
|
|
313
399
|
|
|
400
|
+
// The repository's latest issues, as many as AGENT_ISSUES: of those kept
|
|
401
|
+
// (`kept`), where they hold as many, or every one the repository has; else
|
|
402
|
+
// read, as those kept hold none updated before the year they are read for.
|
|
403
|
+
async function latestIssues(fullName: string, token: string, kept: Kept['issues'], fetchImpl: typeof fetch): Promise<AgentIssue[]> {
|
|
404
|
+
const list = await kept()
|
|
405
|
+
if (list.rows.length >= AGENT_ISSUES || list.all) return list.rows.slice(0, AGENT_ISSUES).map(({ bot: _bot, closedAt: _closedAt, reason: _reason, ...row }) => row)
|
|
406
|
+
const [owner, name] = fullName.split('/') as [string, string]
|
|
407
|
+
const data = await githubGraphql(ISSUES_QUERY, { owner, name }, token, fetchImpl) as Issues
|
|
408
|
+
return (data?.repository?.issues?.nodes ?? []).flatMap(node => issue(node) ?? [])
|
|
409
|
+
}
|
|
410
|
+
|
|
314
411
|
// A page of what the repository's agents left: its pull requests below the
|
|
315
412
|
// one the cursor `pulls` names, as kept (`kept`, agentPulls), its default
|
|
316
413
|
// branch's commits on from those `commits` names, as its history is kept
|
|
317
|
-
// (`pages`, historyFrom: the first page from its head), whether a
|
|
318
|
-
// request brought those marked kept (`pulled`) (the first page where
|
|
319
|
-
// neither is named, with its issues, its
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
414
|
+
// (`pages`, historyFrom: the first page from its head, as kept), whether a
|
|
415
|
+
// pull request brought those marked kept (`pulled`) (the first page where
|
|
416
|
+
// neither is named, with its latest issues, as kept, its agents' branches
|
|
417
|
+
// and the commits the app agents' accounts authored, and the app agents it
|
|
418
|
+
// may name), and the cursors reading on from each, null where either is
|
|
419
|
+
// read whole.
|
|
323
420
|
export async function repositoryAgents(fullName: string, token: string, cursors: { pulls: string | null; commits: string | null }, pages: HistoryPages,
|
|
324
|
-
pulled: CommitPulls, kept:
|
|
325
|
-
const [owner, name] = fullName.split('/') as [string, string]
|
|
421
|
+
pulled: CommitPulls, kept: Kept, fetchImpl: typeof fetch = globalThis.fetch) {
|
|
326
422
|
const first = cursors.pulls === null && cursors.commits === null
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
first ?
|
|
331
|
-
first
|
|
423
|
+
const tip = first ? kept.head() : Promise.resolve(null)
|
|
424
|
+
const [head, issues, branches, nodes, pulls] = await Promise.all([
|
|
425
|
+
tip,
|
|
426
|
+
first ? latestIssues(fullName, token, kept.issues, fetchImpl) : [],
|
|
427
|
+
first ? kept.heads().then(heads => agentBranches(fullName, token, heads, kept.tips, fetchImpl)) : [],
|
|
428
|
+
tip.then(sha => typeof sha === 'string' && SHA.test(sha) ? authoredCommits(fullName, token, sha, kept.authored, fetchImpl) : []),
|
|
429
|
+
first || cursors.pulls !== null ? agentPulls(fullName, token, cursors.pulls, kept.pulls, fetchImpl) : { pulls: [], after: null },
|
|
332
430
|
])
|
|
333
|
-
const repository = data?.repository
|
|
334
|
-
if (data !== null && !repository) throw new GithubApiError(404, 'github-not-found')
|
|
335
|
-
const head = repository?.defaultBranchRef?.target?.oid
|
|
336
431
|
const named = cursors.commits === null ? first && typeof head === 'string' && SHA.test(head) ? [head] : [] : startsOf(cursors.commits)
|
|
337
432
|
const starts = named.slice(0, ALL_BRANCHES)
|
|
338
433
|
const history = starts.length > 0 ? (await historyFrom(fullName, token, starts, pages, fetchImpl)).commits : []
|
|
339
434
|
const marked = history.flatMap(commit => { const signals = historyMarks(commit); return signals ? [{ commit, signals }] : [] })
|
|
340
|
-
const
|
|
341
|
-
const [brought] = await Promise.all([pulledCommits(fullName, token, marked.map(({ commit }) => commit.sha), pulled, fetchImpl),
|
|
342
|
-
linkNoreplyUsers(nodes.map(node => node?.author), token, fetchImpl)])
|
|
435
|
+
const brought = await pulledCommits(fullName, token, marked.map(({ commit }) => commit.sha), pulled, fetchImpl)
|
|
343
436
|
const commits = [...marked.flatMap(({ commit, signals }) => brought.has(commit.sha) ? [] : [keptCommit(commit, signals)]), ...nodes.flatMap(node => directCommit(node) ?? [])]
|
|
344
437
|
const after = { pulls: pulls.after, commits: frontier(history, named.slice(ALL_BRANCHES)) }
|
|
345
438
|
return {
|
|
346
|
-
pulls: pulls.pulls.map(({ merged: _merged, ...row }: AgentPull & { merged?: unknown }) => row),
|
|
439
|
+
pulls: pulls.pulls.map(({ bot: _bot, merged: _merged, ...row }: AgentPull & { bot?: unknown; merged?: unknown }) => row),
|
|
347
440
|
commits: commits.filter((commit, index) => commits.findIndex(other => other.oid === commit.oid) === index),
|
|
348
441
|
...first ? {
|
|
349
|
-
issues
|
|
350
|
-
branches
|
|
442
|
+
issues,
|
|
443
|
+
branches,
|
|
351
444
|
agents: APP_AGENTS.map(({ id, name: agent, avatar }): AppAgent => ({ id, name: agent, ...avatar ? { avatar } : {} })),
|
|
352
445
|
} : {},
|
|
353
446
|
more: after.pulls !== null || after.commits !== null, after,
|
|
@@ -23,8 +23,8 @@ export const SINCE_COMMIT = /^[a-f\d]{40}$/u
|
|
|
23
23
|
|
|
24
24
|
export interface RepoDependencyReview { enabled: boolean; required: number; since: string | null; lockfiles: LockfileScope }
|
|
25
25
|
// A review published of the version it is of: its package, version and
|
|
26
|
-
// npm's integrity of it, the patch it is of (
|
|
27
|
-
//
|
|
26
|
+
// npm's integrity of it, the patch it is of as patched (its hash, as a
|
|
27
|
+
// lockfile names it), the version it was compared with (none for a whole package's),
|
|
28
28
|
// who wrote it (`reviewer`, their id, else their login then), their login
|
|
29
29
|
// as it was then (`login`), its verdict, whether it checked every file
|
|
30
30
|
// (`full`), and when it was published.
|
|
@@ -81,14 +81,14 @@ async function publishedReviews(db: ManagedSql, repoId: number, versions: Review
|
|
|
81
81
|
const asked = versions.slice(at, at + VERSIONS_AT_ONCE)
|
|
82
82
|
const keys = new Set(asked.map(({ name, version }) => `${name}@${version}`))
|
|
83
83
|
const [names, numbers] = [[...new Set(asked.map(({ name }) => name))], [...new Set(asked.map(({ version }) => version))]]
|
|
84
|
-
const rows = (await db.prepare(`SELECT r.id, r.user_id AS userId, r.user_login AS userLogin, r.package, r.version, r.integrity,
|
|
84
|
+
const rows = (await db.prepare(`SELECT r.id, r.user_id AS userId, r.user_login AS userLogin, r.package, r.version, r.integrity, r.patch_hash AS patchHash,
|
|
85
85
|
NULLIF(r.base_package, '') AS basePackage, NULLIF(r.base_version, '') AS baseVersion, r.files, r.status, r.published_at AS publishedAt
|
|
86
86
|
FROM managed_dependency_review r
|
|
87
87
|
WHERE r.ecosystem = 'npm' AND r.published_at IS NOT NULL AND r.revoked_at IS NULL AND r.status IS NOT NULL
|
|
88
88
|
AND r.package IN (${names.map(() => '?').join(',')}) AND r.version IN (${numbers.map(() => '?').join(',')})
|
|
89
89
|
AND EXISTS (SELECT 1 FROM managed_dependency_review_team rt JOIN managed_team t ON t.id = rt.team_id
|
|
90
90
|
WHERE rt.review_id = r.id AND ${COUNTING_TEAM})`).all(...names, ...numbers, repoId) as
|
|
91
|
-
{ id: string; userId: string | null; userLogin: string; package: string; version: string; integrity: string
|
|
91
|
+
{ id: string; userId: string | null; userLogin: string; package: string; version: string; integrity: string; patchHash: string
|
|
92
92
|
basePackage: string | null; baseVersion: string | null; files: string | null; status: ReviewVerdict; publishedAt: number }[])
|
|
93
93
|
.filter(row => keys.has(`${row.package}@${row.version}`))
|
|
94
94
|
if (rows.length === 0) continue
|
|
@@ -105,7 +105,8 @@ async function publishedReviews(db: ManagedSql, repoId: number, versions: Review
|
|
|
105
105
|
const files = Array.isArray(snapshot) ? snapshot : snapshot?.paths ?? null
|
|
106
106
|
const nonText = Array.isArray(snapshot) ? null : snapshot?.nonTextFiles ?? []
|
|
107
107
|
const full = summarizeReview(entries.get(row.id) ?? [], files, nonText).coverage === 'full'
|
|
108
|
-
found.push({ id: row.id, package: row.package, version: row.version, integrity: row.integrity,
|
|
108
|
+
found.push({ id: row.id, package: row.package, version: row.version, integrity: row.integrity, ...row.patchHash ? { patchHash: row.patchHash } : {},
|
|
109
|
+
basePackage: row.basePackage, baseVersion: row.baseVersion,
|
|
109
110
|
reviewer: row.userId ?? `login:${row.userLogin.toLowerCase()}`, login: row.userLogin, status: row.status, full, time: Number(row.publishedAt) })
|
|
110
111
|
}
|
|
111
112
|
}
|