@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
|
@@ -1,34 +1,43 @@
|
|
|
1
|
-
// A repository's pull requests, the most recently updated
|
|
2
|
-
// once for
|
|
3
|
-
// telling whose its year's commits are by
|
|
4
|
-
// agentMerges)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// it
|
|
1
|
+
// A repository's pull requests and its issues, the most recently updated
|
|
2
|
+
// first, each kept once for all of those that read them so (http.ts
|
|
3
|
+
// keptList): the Pulse, telling whose its year's commits are by the pull
|
|
4
|
+
// requests merged (agent-merges.ts agentMerges) and counting those of a
|
|
5
|
+
// period (repository-pulse.ts listedActivity), and the Agents tab, its pages
|
|
6
|
+
// of pull requests (repository-agents.ts agentPulls) and its latest issues.
|
|
7
|
+
// Those updated since the Pulse's year's start, a few queries an answer, the
|
|
8
|
+
// rest read by the next, and those updated since read as they are. An issue
|
|
9
|
+
// kept as both show it; a pull request as the Agents tab shows it
|
|
10
|
+
// (repository-agents.ts pull: the marks an agent left in its texts, never
|
|
11
|
+
// the texts) and, one merged, what it brought the default branch, as the
|
|
12
|
+
// Pulse tells it: an agent that
|
|
10
13
|
// commits as whoever ran it (Codex's own) leaves its commits no mark, its
|
|
11
14
|
// pull request's branch alone naming it (app-agents.ts
|
|
12
15
|
// AGENT_BRANCH_PREFIXES), or a label one who ran it gave it naming the agent
|
|
13
16
|
// (agent-tasks.ts labelAgents), as the Agents tab tells an agent's work; or
|
|
14
17
|
// its author, an account agents work as (github-agents.ts AGENT_OPERATED),
|
|
15
18
|
// or any GitHub App's bot.
|
|
16
|
-
import { type AgentPull, labelAgents } from '../common/managed/agent-tasks.ts'
|
|
19
|
+
import { type AgentIssue, type AgentPull, labelAgents } from '../common/managed/agent-tasks.ts'
|
|
17
20
|
import { GITHUB_AGENTS, agentOperated } from '../common/managed/github-agents.ts'
|
|
18
21
|
import { AGENT_BRANCH_PREFIXES } from './app-agents.ts'
|
|
19
|
-
import { AGENT_PULL_FIELDS, type PullNode, pull } from './repository-agents.ts'
|
|
22
|
+
import { AGENT_ISSUE_FIELDS, AGENT_PULL_FIELDS, type IssueNode, type PullNode, issue, pull } from './repository-agents.ts'
|
|
20
23
|
import { githubGraphql } from './repository-history.ts'
|
|
21
24
|
|
|
22
25
|
// What the pull requests kept are read by, its version: bumped as what a
|
|
23
26
|
// page reads, or how one is told, changes, its pull requests read again
|
|
24
|
-
// (http.ts
|
|
25
|
-
// PULSE_YEAR_VERSION too, as either changes how it tells an agent's)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
// (http.ts keptList keys them by AGENTS_READ_VERSION and the Pulse's
|
|
28
|
+
// PULSE_YEAR_VERSION too, as either changes how it tells an agent's); and
|
|
29
|
+
// the issues kept.
|
|
30
|
+
export const PULLS_READ_VERSION = 2
|
|
31
|
+
export const ISSUES_LIST_READ_VERSION = 1
|
|
32
|
+
// The pull requests or issues read an answer, a page a query, as many
|
|
33
|
+
// queries as LIST_QUERIES, the rest left for the next to ask; of each pull
|
|
34
|
+
// request merged, as many of its latest commits as MERGED_COMMITS. Those
|
|
35
|
+
// updated since the newest read, few as they mostly are, read TOP_UP_PAGE
|
|
36
|
+
// first, the rest LIST_PAGE a page (GitHub reading on from a page's end
|
|
37
|
+
// whatever its size).
|
|
38
|
+
const LIST_PAGE = 100
|
|
39
|
+
const TOP_UP_PAGE = 10
|
|
40
|
+
const LIST_QUERIES = 5
|
|
32
41
|
const MERGED_COMMITS = 100
|
|
33
42
|
const LABEL_AGENTS = Object.keys(GITHUB_AGENTS)
|
|
34
43
|
const SHA = /^[a-f\d]{40}$/u
|
|
@@ -39,9 +48,10 @@ const timeOf = (value: unknown) => {
|
|
|
39
48
|
return Number.isSafeInteger(at) ? at : null
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
// A pull request as read: its row on the Agents tab,
|
|
43
|
-
// it
|
|
44
|
-
|
|
51
|
+
// A pull request as read: its row on the Agents tab, whether a GitHub App's
|
|
52
|
+
// bot opened it (`bot`, GitHub's GraphQL API naming one by its app's login
|
|
53
|
+
// alone), and one merged, what it brought the default branch (`merged`).
|
|
54
|
+
export type KeptPull = AgentPull & { bot?: true; merged?: Merged }
|
|
45
55
|
// What a merged pull request brought: its merge commit (`sha`), when it was
|
|
46
56
|
// merged (`at`) and whether it was an agent's (agentPull); an agent's merged
|
|
47
57
|
// with a merge commit, which a year counts none of, its own commits too
|
|
@@ -51,25 +61,33 @@ export type KeptPull = AgentPull & { merged?: Merged }
|
|
|
51
61
|
// its empty commits left out, as a rebase leaves them, and how many more it
|
|
52
62
|
// had than those read (`more`).
|
|
53
63
|
export interface Merged { sha: string; at: number; agent: boolean; own?: string[]; subjects?: string[]; more?: number }
|
|
54
|
-
//
|
|
55
|
-
// (
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
+
// An issue as read: as the Agents tab shows it, whether a bot opened it
|
|
65
|
+
// (`bot`, as a pull request's), when it was closed, and why (`reason`, as
|
|
66
|
+
// GitHub tells it: completed, not planned or a duplicate).
|
|
67
|
+
export type KeptIssue = AgentIssue & { bot?: true; closedAt: number | null; reason: IssueReason | null }
|
|
68
|
+
export type IssueReason = 'completed' | 'not-planned' | 'duplicate'
|
|
69
|
+
export const ISSUE_REASONS: Record<string, IssueReason> = { COMPLETED: 'completed', NOT_PLANNED: 'not-planned', DUPLICATE: 'duplicate' }
|
|
70
|
+
// The pull requests or issues updated since a year's start (`since`) as
|
|
71
|
+
// read so far (readList), the most recently updated first (`rows`); when the
|
|
72
|
+
// newest read was last updated, those updated since read again (`through`,
|
|
73
|
+
// null till all are read once), and where those read reach back no further
|
|
74
|
+
// than was needed, when the oldest of them was updated (`reach`); whether
|
|
75
|
+
// every one the repository has is read (`all`); and a read not finished:
|
|
76
|
+
// where it goes on (`after`), when the newest it began with was updated
|
|
77
|
+
// (`from`), the oldest it reads to (`stop`), how many it has read (`read`)
|
|
78
|
+
// and when the oldest of them was updated (`to`).
|
|
79
|
+
export interface Listed<Row> { rows: Row[]; since: number; through: number | null; reach?: number; all?: true
|
|
64
80
|
next: { after: string; from: number; stop: number; read: number; to?: number } | null }
|
|
81
|
+
export type PullList = Listed<KeptPull>
|
|
82
|
+
export type IssueList = Listed<KeptIssue>
|
|
65
83
|
|
|
66
|
-
// Since when every
|
|
67
|
-
//
|
|
68
|
-
export const pullsReadTo = (list: Pick<
|
|
84
|
+
// Since when every one updated is read: all of them since the year's
|
|
85
|
+
// start, -∞; none yet, ∞.
|
|
86
|
+
export const pullsReadTo = (list: Pick<Listed<unknown>, 'next' | 'reach'>) => list.next ? list.next.to ?? Number.POSITIVE_INFINITY : list.reach ?? Number.NEGATIVE_INFINITY
|
|
69
87
|
|
|
70
|
-
const PULLS_QUERY = `query($owner: String!, $name: String!, $after: String) {
|
|
88
|
+
const PULLS_QUERY = `query($owner: String!, $name: String!, $after: String, $first: Int!) {
|
|
71
89
|
repository(owner: $owner, name: $name) {
|
|
72
|
-
pullRequests(first: $
|
|
90
|
+
pullRequests(first: $first, after: $after, orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
73
91
|
pageInfo { hasNextPage endCursor }
|
|
74
92
|
nodes { ${AGENT_PULL_FIELDS} mergedAt mergeCommit { oid parents { totalCount } }
|
|
75
93
|
commits(last: ${MERGED_COMMITS}) { totalCount nodes { commit { oid messageHeadline changedFilesIfAvailable } } } }
|
|
@@ -79,7 +97,17 @@ const PULLS_QUERY = `query($owner: String!, $name: String!, $after: String) {
|
|
|
79
97
|
type MergedNode = PullNode & { mergedAt?: unknown; mergeCommit?: { oid?: unknown; message?: unknown; parents?: { totalCount?: unknown } | null } | null
|
|
80
98
|
author?: PullNode['author'] & { __typename?: unknown }
|
|
81
99
|
commits?: { totalCount?: unknown; nodes?: ({ commit?: { oid?: unknown; messageHeadline?: unknown; changedFilesIfAvailable?: unknown } | null } | null)[] } | null }
|
|
82
|
-
|
|
100
|
+
const ISSUES_QUERY = `query($owner: String!, $name: String!, $after: String, $first: Int!) {
|
|
101
|
+
repository(owner: $owner, name: $name) {
|
|
102
|
+
issues(first: $first, after: $after, orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
103
|
+
pageInfo { hasNextPage endCursor }
|
|
104
|
+
nodes { ${AGENT_ISSUE_FIELDS} stateReason closedAt }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}`
|
|
108
|
+
type ClosedNode = IssueNode & { stateReason?: unknown; closedAt?: unknown; author?: IssueNode['author'] & { __typename?: unknown } }
|
|
109
|
+
const botOf = (author: { __typename?: unknown } | null | undefined) => author?.__typename === 'Bot' ? { bot: true as const } : {}
|
|
110
|
+
type ListPage<Node> = { repository?: Record<string, { pageInfo?: { hasNextPage?: unknown; endCursor?: unknown }; nodes?: (Node | null)[] } | null> | null }
|
|
83
111
|
|
|
84
112
|
const agentBranch = (name: unknown) => typeof name === 'string' && AGENT_BRANCH_PREFIXES.some(prefix => name.startsWith(prefix))
|
|
85
113
|
// Whether a pull request is an agent's: by its branch, a label naming one,
|
|
@@ -105,31 +133,60 @@ function mergedOf(node: MergedNode): Merged | null {
|
|
|
105
133
|
return { sha, at, agent, ...own.length > 0 ? { own } : {}, ...subjects ? { subjects, ...more > 0 ? { more } : {} } : {} }
|
|
106
134
|
}
|
|
107
135
|
|
|
136
|
+
// What an issue closed tells beside its row (KeptIssue).
|
|
137
|
+
function closedOf(node: ClosedNode): KeptIssue | null {
|
|
138
|
+
const row = issue(node)
|
|
139
|
+
return row ? { ...row, ...botOf(node.author), closedAt: timeOf(node.closedAt), reason: ISSUE_REASONS[String(node.stateReason)] ?? null } : null
|
|
140
|
+
}
|
|
141
|
+
|
|
108
142
|
// The most recently updated first, the higher number first of those
|
|
109
143
|
// updated together.
|
|
110
|
-
const newestFirst = (a:
|
|
144
|
+
const newestFirst = (a: { number: number; updatedAt: number | null }, b: { number: number; updatedAt: number | null }) =>
|
|
145
|
+
(b.updatedAt ?? 0) - (a.updatedAt ?? 0) || b.number - a.number
|
|
146
|
+
|
|
147
|
+
// How a list is read: the query of its pages, the repository's connection
|
|
148
|
+
// they are of, and what each of them is kept as.
|
|
149
|
+
interface ListSpec<Node, Row> { query: string; connection: 'pullRequests' | 'issues'; row(node: Node): Row | null }
|
|
150
|
+
const PULLS: ListSpec<MergedNode, KeptPull> = { query: PULLS_QUERY, connection: 'pullRequests', row: node => {
|
|
151
|
+
const row = pull(node)
|
|
152
|
+
const merged = mergedOf(node)
|
|
153
|
+
return row ? { ...row, ...botOf(node.author), ...merged ? { merged } : {} } : null
|
|
154
|
+
} }
|
|
155
|
+
const ISSUES: ListSpec<ClosedNode, KeptIssue> = { query: ISSUES_QUERY, connection: 'issues', row: closedOf }
|
|
156
|
+
|
|
157
|
+
// The pull requests updated since `since` (readList), and what each merged
|
|
158
|
+
// brought.
|
|
159
|
+
export const readPulls = (fullName: string, token: string, since: number, kept: PullList | null, fetchImpl: typeof fetch = globalThis.fetch, need = since) =>
|
|
160
|
+
readList(PULLS, fullName, token, since, kept, fetchImpl, need)
|
|
161
|
+
// The issues updated since `since` (readList), and when and why each closed was.
|
|
162
|
+
export const readIssues = (fullName: string, token: string, since: number, kept: IssueList | null, fetchImpl: typeof fetch = globalThis.fetch, need = since) =>
|
|
163
|
+
readList(ISSUES, fullName, token, since, kept, fetchImpl, need)
|
|
111
164
|
|
|
112
|
-
//
|
|
113
|
-
// first (a pull request is updated when merged, or after
|
|
114
|
-
// `kept`: till one updated before the year,
|
|
115
|
-
// was, as many queries an answer as
|
|
116
|
-
// on from where it was left by the
|
|
117
|
-
// are all read; where those read
|
|
118
|
-
//
|
|
119
|
-
// needs. Each kept as last read,
|
|
120
|
-
|
|
121
|
-
|
|
165
|
+
// Those of a list (`spec`) updated since `since`, read the most recently
|
|
166
|
+
// updated first (a pull request is updated when merged, or after, an issue
|
|
167
|
+
// when closed), on from what was `kept`: till one updated before the year,
|
|
168
|
+
// or before the newest read before was, as many queries an answer as
|
|
169
|
+
// LIST_QUERIES, a read not finished going on from where it was left by the
|
|
170
|
+
// next, or once those updated since `need` are all read; where those read
|
|
171
|
+
// reached no further back than they needed, and `need` is further, read
|
|
172
|
+
// again from the newest on, as far back as it needs. Each kept as last read,
|
|
173
|
+
// one updated before the year forgotten.
|
|
174
|
+
async function readList<Node extends { updatedAt?: unknown }, Row extends { number: number; updatedAt: number | null }>(spec: ListSpec<Node, Row>,
|
|
175
|
+
fullName: string, token: string, since: number, kept: Listed<Row> | null, fetchImpl: typeof fetch, need: number): Promise<Listed<Row>> {
|
|
122
176
|
const [owner, name] = fullName.split('/') as [string, string]
|
|
123
|
-
const
|
|
177
|
+
const rows = new Map((kept?.rows ?? []).filter(item => (item.updatedAt ?? 0) >= since).map(item => [item.number, item]))
|
|
124
178
|
// Every one read, as long as none was forgotten, the year begun since.
|
|
125
|
-
const whole = kept?.all === true &&
|
|
179
|
+
const whole = kept?.all === true && rows.size === kept.rows.length
|
|
126
180
|
const further = !kept?.next && kept?.reach !== undefined && kept.reach > need
|
|
127
181
|
const stop = kept?.next?.stop ?? (further ? since : Math.max(since, kept?.through ?? since))
|
|
128
|
-
const result = (through: number | null, next:
|
|
129
|
-
|
|
182
|
+
const result = (through: number | null, next: Listed<Row>['next'], reach: number | undefined, all = whole): Listed<Row> => ({
|
|
183
|
+
rows: [...rows.values()].toSorted(newestFirst), since, through, ...reach === undefined ? {} : { reach }, ...all ? { all: true as const } : {}, next })
|
|
184
|
+
// A read of those updated since the newest read alone.
|
|
185
|
+
const topping = !kept?.next && !further && kept?.through !== null && kept?.through !== undefined
|
|
130
186
|
let { after = null, from = null, read = 0, to = null }: { after?: string | null; from?: number | null; read?: number; to?: number | null } = kept?.next ?? {}
|
|
131
|
-
for (let queries = 0; queries <
|
|
132
|
-
const
|
|
187
|
+
for (let queries = 0; queries < LIST_QUERIES; queries++) {
|
|
188
|
+
const first = topping && queries === 0 ? TOP_UP_PAGE : LIST_PAGE
|
|
189
|
+
const page = (await githubGraphql(spec.query, { owner, name, after, first }, token, fetchImpl) as ListPage<Node> | null)?.repository?.[spec.connection]
|
|
133
190
|
const end = page?.pageInfo?.hasNextPage !== true || typeof page.pageInfo.endCursor !== 'string'
|
|
134
191
|
let crossed = false
|
|
135
192
|
for (const node of page?.nodes ?? []) {
|
|
@@ -139,9 +196,8 @@ export async function readPulls(fullName: string, token: string, since: number,
|
|
|
139
196
|
from ??= updated
|
|
140
197
|
to = updated
|
|
141
198
|
read++
|
|
142
|
-
const row =
|
|
143
|
-
|
|
144
|
-
if (row) pulls.set(row.number, merged ? { ...row, merged } : row)
|
|
199
|
+
const row = spec.row(node)
|
|
200
|
+
if (row) rows.set(row.number, row)
|
|
145
201
|
}
|
|
146
202
|
// Read as far back as it reads to: the year's start, every one since
|
|
147
203
|
// read; else as far as those read before reached. Read to the last
|
|
@@ -26,7 +26,7 @@ const WEEK_MS = 7 * 24 * 60 * 60_000
|
|
|
26
26
|
const DAY_MS = 24 * 60 * 60_000
|
|
27
27
|
const MAX_CONTRIBUTORS = 100
|
|
28
28
|
const DIRECT_LISTED = 500
|
|
29
|
-
const LISTED = 10
|
|
29
|
+
export const LISTED = 10
|
|
30
30
|
// Merged pull requests and closed issues are searched by when each was last
|
|
31
31
|
// updated, GitHub sorting by no merge or close: as many as this are read, and
|
|
32
32
|
// the latest merged or closed of them listed.
|
|
@@ -211,9 +211,10 @@ export async function defaultHead(fullName: string, token: string, fetchImpl: ty
|
|
|
211
211
|
// before it, a clock behind, the branch's commits since beneath them. Read
|
|
212
212
|
// no further than `need` asks, those since it read whole (`complete`, `from`
|
|
213
213
|
// the newest of those read whose parents are not, every commit after it
|
|
214
|
-
// read; null, the year read whole).
|
|
214
|
+
// read; null, the year read whole). How many are read so far `told` as they
|
|
215
|
+
// are.
|
|
215
216
|
export async function yearCommits(fullName: string, token: string, head: string, since: number, pages: HistoryPages,
|
|
216
|
-
fetchImpl: typeof fetch = globalThis.fetch, need = since): Promise<YearCommits> {
|
|
217
|
+
fetchImpl: typeof fetch = globalThis.fetch, need = since, told: (read: number) => void = () => {}): Promise<YearCommits> {
|
|
217
218
|
const found = new Map<string, HistoryCommit>()
|
|
218
219
|
const tried = new Set<string>()
|
|
219
220
|
const within = (commit: HistoryCommit) => commit.committedAt === null || commit.committedAt >= since - YEAR_SKEW_MS
|
|
@@ -236,6 +237,7 @@ export async function yearCommits(fullName: string, token: string, head: string,
|
|
|
236
237
|
const got = await historyFrom(fullName, token, batch, pages, fetchImpl)
|
|
237
238
|
if (got.read > 0) queries++
|
|
238
239
|
for (const commit of got.commits) if (!found.has(commit.sha)) found.set(commit.sha, commit)
|
|
240
|
+
told(read())
|
|
239
241
|
if (read() > YEAR_COMMITS) return { complete: false, read: read(), tooMany: true }
|
|
240
242
|
starts = [...new Set([...found.values()].filter(within).flatMap(commit => commit.parents))].filter(sha => !found.has(sha) && !tried.has(sha))
|
|
241
243
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// A repository's change stamp: what moves as anything its tabs read of it
|
|
2
|
+
// changes, but the checks its pull requests run, which move no pull
|
|
3
|
+
// request's update time, read in one cheap GraphQL query: when it was last
|
|
4
|
+
// pushed to, which branch is its default, how many branches and tags it
|
|
5
|
+
// has, and how many pull requests and issues it has and when the one
|
|
6
|
+
// updated last was. An answer kept of it
|
|
7
|
+
// is read again behind its readers only where its stamp moved since the
|
|
8
|
+
// answer was read (http.ts keptAnswer).
|
|
9
|
+
import { GithubApiError } from './github-errors.ts'
|
|
10
|
+
import { githubGraphql } from './github-graphql.ts'
|
|
11
|
+
|
|
12
|
+
// The rules a stamp is read by: its key's, so that a stamp read by others is
|
|
13
|
+
// none's (http.ts stampOf).
|
|
14
|
+
export const STAMP_READ = 2
|
|
15
|
+
|
|
16
|
+
const STAMP_QUERY = `query($owner: String!, $name: String!) {
|
|
17
|
+
repository(owner: $owner, name: $name) {
|
|
18
|
+
pushedAt
|
|
19
|
+
defaultBranchRef { name }
|
|
20
|
+
heads: refs(refPrefix: "refs/heads/", first: 1) { totalCount }
|
|
21
|
+
tags: refs(refPrefix: "refs/tags/", first: 1) { totalCount }
|
|
22
|
+
pullRequests(first: 1, orderBy: { field: UPDATED_AT, direction: DESC }) { totalCount nodes { updatedAt } }
|
|
23
|
+
issues(first: 1, orderBy: { field: UPDATED_AT, direction: DESC }) { totalCount nodes { updatedAt } }
|
|
24
|
+
}
|
|
25
|
+
}`
|
|
26
|
+
type Counted = { totalCount?: unknown; nodes?: ({ updatedAt?: unknown } | null)[] | null } | null
|
|
27
|
+
type Stamped = { repository?: { pushedAt?: unknown; defaultBranchRef?: { name?: unknown } | null; heads?: Counted; tags?: Counted; pullRequests?: Counted; issues?: Counted } | null }
|
|
28
|
+
| null
|
|
29
|
+
|
|
30
|
+
// Repository `fullName`'s stamp, read with `token`, as text to compare.
|
|
31
|
+
export async function repositoryStamp(fullName: string, token: string, fetchImpl: typeof fetch = globalThis.fetch): Promise<{ stamp: string }> {
|
|
32
|
+
const [owner, name] = fullName.split('/') as [string, string]
|
|
33
|
+
const repository = (await githubGraphql(STAMP_QUERY, { owner, name }, token, fetchImpl) as Stamped)?.repository
|
|
34
|
+
if (!repository) throw new GithubApiError(404, 'github-not-found')
|
|
35
|
+
// One missing a count tells nothing: none, never one that would move with nothing.
|
|
36
|
+
const { heads, tags, pullRequests, issues } = repository
|
|
37
|
+
if (![heads, tags, pullRequests, issues].every(connection => Number.isSafeInteger(connection?.totalCount))) throw new GithubApiError(502, 'github-malformed')
|
|
38
|
+
const latest = (connection: Counted | undefined) => [connection?.totalCount, connection?.nodes?.[0]?.updatedAt ?? null]
|
|
39
|
+
return { stamp: JSON.stringify([repository.pushedAt ?? null, repository.defaultBranchRef?.name ?? null, heads?.totalCount, tags?.totalCount, ...latest(pullRequests),
|
|
40
|
+
...latest(issues)]) }
|
|
41
|
+
}
|
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
// is reviewed since (pull-dependency-reviews.ts): a version passes by the
|
|
10
10
|
// reviews its teams publish, and those its pull request's comments make, as
|
|
11
11
|
// many as the repository requires, or by `lookup`'s saying so, and is
|
|
12
|
-
// otherwise unreviewed. A patched version (pnpm's patchedDependencies)
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// those `lookup` tells of.
|
|
12
|
+
// otherwise unreviewed. A patched version's (pnpm's patchedDependencies)
|
|
13
|
+
// reviews are of its patch, its teams' and its comments' alike. Those
|
|
14
|
+
// comments' reviews count for the pull request alone, as it is open: the
|
|
15
|
+
// host's to keep, as it is merged, among those `lookup` tells of.
|
|
17
16
|
//
|
|
18
17
|
// Each version is asked of `lookup` once a request, as many at once as
|
|
19
18
|
// ASK_AT_ONCE, its answer kept in this instance KEPT_MS; one it fails to
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
// findings, and only expanded does it show that they fail to combine.
|
|
18
18
|
import { reportEntries } from '@preventive/report'
|
|
19
19
|
import { type TeamAppMetadata, managedTeamAppMetadata } from '../common/managed/team-app.js'
|
|
20
|
+
import { expandFindingValues } from '../common/managed/finding-values.js'
|
|
20
21
|
import type { BlobStore } from './blob-store.ts'
|
|
21
22
|
import type { ManagedDb, TeamReportAccessSnapshot, UserTeam } from './db.ts'
|
|
22
23
|
import type { WorkspaceShareSnapshot } from './workspace-shares.ts'
|
|
@@ -86,8 +87,9 @@ async function classify(db: ManagedDb, store: BlobStore, key: string, team: User
|
|
|
86
87
|
// A catalog read before a concurrent publication names another report set.
|
|
87
88
|
if (!access?.teamId || access.reports.length !== ids.size || access.reports.some(report => !ids.has(report.id))) return null
|
|
88
89
|
const body = JSON.parse((await loadTeamReportsResponse(db, store, access)).toString('utf8')) as {
|
|
89
|
-
reports: { id: string; filename: string; data: unknown }[]; links?: string[][]
|
|
90
|
+
reports: { id: string; filename: string; data: unknown }[]; links?: string[][]; values?: unknown[]
|
|
90
91
|
}
|
|
92
|
+
for (const report of body.reports) expandFindingValues(report.data, body.values)
|
|
91
93
|
const restricted = access.reports.some(report => !report.permissions.dependencies || !report.permissions.security)
|
|
92
94
|
const empty = restricted ? body.reports.filter(report => !hasContent(report.data)).map(report => report.id) : []
|
|
93
95
|
const links = body.links ?? []
|
|
@@ -8,7 +8,8 @@ import { managedFindingSourcePaths, readManagedReport } from '../common/managed/
|
|
|
8
8
|
import { filterReportData, projectFinding } from '../common/managed/report-filter.ts'
|
|
9
9
|
import { revalidationLinks, securityLinks } from '../common/security-links.js'
|
|
10
10
|
import { treeDependencyDirectory } from '../client/dependency-paths.js'
|
|
11
|
-
import {
|
|
11
|
+
import { liteWorkspace } from '../common/managed/finding-detail.js'
|
|
12
|
+
import { expandFindingValues, findingValueIndex } from '../common/managed/finding-values.js'
|
|
12
13
|
import type { BlobStore } from './blob-store.ts'
|
|
13
14
|
import type { ManagedDb, TeamReportAccessSnapshot } from './db.ts'
|
|
14
15
|
import { triageWireEntry } from './triage-response.ts'
|
|
@@ -158,13 +159,23 @@ async function buildTeamWorkspace(db: ManagedDb, store: BlobStore, snapshot: Tea
|
|
|
158
159
|
for (let i = 0; i < reports.length; i++) {
|
|
159
160
|
const access = snapshot.reports[i]!, report = reports[i]!
|
|
160
161
|
report.data = filterReportData(report.data, access.permissions, access.repo, securityIds)
|
|
162
|
+
if (report.data && typeof report.data === 'object' && ('think' in report.data || 'reportRepo' in report.data)) {
|
|
163
|
+
const { think: _think, reportRepo: _reportRepo, ...data } = report.data as Finding
|
|
164
|
+
report.data = data
|
|
165
|
+
}
|
|
161
166
|
const ids = new Set<string>()
|
|
162
167
|
for (const finding of groupsOf(report.data).flat()) {
|
|
163
168
|
// Preserve linked security classification for the UI lens, including a
|
|
164
169
|
// linked component that a dependency permission will hide from the UI.
|
|
165
170
|
finding['isSecurity'] = securityIds.has(String(finding['id']))
|
|
166
|
-
// The hash of its file's tree,
|
|
171
|
+
// The hash of its file's tree, the fingerprint its ID was derived from,
|
|
172
|
+
// whether its run thought and the repository its report named, which
|
|
173
|
+
// nothing a team view reads, nor an export (@preventive/report
|
|
174
|
+
// write-md-finding.js) writes.
|
|
167
175
|
delete finding['treeHash']
|
|
176
|
+
delete finding['_idBasis']
|
|
177
|
+
delete finding['think']
|
|
178
|
+
delete finding['reportRepo']
|
|
168
179
|
if (typeof finding['id'] === 'string') { ids.add(finding['id']); allIds.add(finding['id']) }
|
|
169
180
|
}
|
|
170
181
|
visible.set(report.id, { ids, sourcePaths: managedFindingSourcePaths(groupsOf(report.data).flat()) })
|
|
@@ -177,11 +188,13 @@ async function buildTeamWorkspace(db: ManagedDb, store: BlobStore, snapshot: Tea
|
|
|
177
188
|
}
|
|
178
189
|
const trees = Buffer.from(`{"trees":{${reports.map(withoutTree).filter(tree => tree !== null).join(',')}}}`)
|
|
179
190
|
const links = mergeLinkGroups(globalLinks, allIds)
|
|
191
|
+
// The board's: its findings without their detail, and in App mode its rows
|
|
192
|
+
// as the App view shows them (common/managed/finding-detail.js).
|
|
193
|
+
const lite = liteWorkspace(reports, links)
|
|
180
194
|
const encoded = {
|
|
181
195
|
// Bounded together, as when the trees were sent with their reports.
|
|
182
196
|
reports: encodeReports(reports, links, trees.length),
|
|
183
|
-
|
|
184
|
-
lite: encodeReports(reports.map(report => ({ ...report, data: liteReportData(report.data) })), links, 0),
|
|
197
|
+
lite: encodeReports(lite.reports, links, 0, lite.folded),
|
|
185
198
|
trees,
|
|
186
199
|
}
|
|
187
200
|
retainVisibility(db, workspaceKey(snapshot), visible)
|
|
@@ -189,18 +202,23 @@ async function buildTeamWorkspace(db: ManagedDb, store: BlobStore, snapshot: Tea
|
|
|
189
202
|
return { ...encoded, visible }
|
|
190
203
|
}
|
|
191
204
|
// `{ reports, links }` as a response sends them, `spent` bytes already
|
|
192
|
-
// counted against the bound
|
|
193
|
-
|
|
205
|
+
// counted against the bound; `folded` where the board's copy left out rows
|
|
206
|
+
// App mode does not show (common/managed/finding-detail.js). Its findings'
|
|
207
|
+
// packages and repositories are sent once each, in `values`
|
|
208
|
+
// (common/managed/finding-values.js).
|
|
209
|
+
function encodeReports(reports: readonly TeamReport[], links: readonly string[][], spent: number, folded = false): Buffer {
|
|
194
210
|
let outputBytes = spent + 14
|
|
195
211
|
const parts = [Buffer.from('{"reports":[')]
|
|
212
|
+
const values = findingValueIndex()
|
|
196
213
|
for (const [index, report] of reports.entries()) {
|
|
197
|
-
const bytes = Buffer.from(JSON.stringify(report))
|
|
214
|
+
const bytes = Buffer.from(JSON.stringify({ ...report, data: values.index(report.data) }))
|
|
198
215
|
outputBytes += bytes.length + 1
|
|
199
216
|
if (outputBytes > MAX_REPORT_QUERY_BYTES) throw new TeamReportsError(413, 'batch-too-large')
|
|
200
217
|
if (index) parts.push(Buffer.from(','))
|
|
201
218
|
parts.push(bytes)
|
|
202
219
|
}
|
|
203
|
-
const
|
|
220
|
+
const tail = JSON.stringify({ ...links.length > 0 ? { links } : {}, ...folded ? { folded } : {}, ...values.values.length > 0 ? { values: values.values } : {} })
|
|
221
|
+
const suffix = Buffer.from(tail === '{}' ? ']}' : `],${tail.slice(1)}`)
|
|
204
222
|
if (outputBytes + suffix.length > MAX_REPORT_QUERY_BYTES) throw new TeamReportsError(413, 'batch-too-large')
|
|
205
223
|
parts.push(suffix)
|
|
206
224
|
return Buffer.concat(parts)
|
|
@@ -227,8 +245,9 @@ export async function loadTeamReportsResponse(db: ManagedDb, store: BlobStore, s
|
|
|
227
245
|
}
|
|
228
246
|
export async function loadTeamReports(db: ManagedDb, store: BlobStore, snapshot: TeamReportAccessSnapshot): Promise<TeamReport[]> {
|
|
229
247
|
// Object consumers get their own copy, including when sharing a cold load.
|
|
230
|
-
const body = await loadTeamReportsResponse(db, store, snapshot)
|
|
231
|
-
|
|
248
|
+
const body = JSON.parse((await loadTeamReportsResponse(db, store, snapshot)).toString('utf8')) as { reports: TeamReport[]; values?: unknown[] }
|
|
249
|
+
for (const report of body.reports) expandFindingValues(report.data, body.values)
|
|
250
|
+
return body.reports
|
|
232
251
|
}
|
|
233
252
|
async function teamVisibility(db: ManagedDb, store: BlobStore, snapshot: TeamReportAccessSnapshot): Promise<Visibility> {
|
|
234
253
|
let visible = caches.get(db)?.get(workspaceKey(snapshot))
|