@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preventive/triage",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.52",
|
|
4
4
|
"description": "Client & relay server for triaging of automated reports",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"author": {
|
|
@@ -166,6 +166,7 @@
|
|
|
166
166
|
"server-managed/lockfile-scope.ts",
|
|
167
167
|
"server-managed/npm-manifest.ts",
|
|
168
168
|
"server-managed/npm-packages.ts",
|
|
169
|
+
"server-managed/npm-patch.ts",
|
|
169
170
|
"server-managed/package-entry-points.ts",
|
|
170
171
|
"server-managed/pretty-print.ts",
|
|
171
172
|
"server-managed/pull-reviews.ts",
|
|
@@ -196,6 +197,8 @@
|
|
|
196
197
|
"server-managed/repository-issues.ts",
|
|
197
198
|
"server-managed/repository-pulse.ts",
|
|
198
199
|
"server-managed/repository-pulls.ts",
|
|
200
|
+
"server-managed/listed-activity.ts",
|
|
201
|
+
"server-managed/repository-stamp.ts",
|
|
199
202
|
"server-managed/repository-aliases.ts",
|
|
200
203
|
"server-managed/repository-discovery.ts",
|
|
201
204
|
"server-managed/repository-policy.ts",
|
|
@@ -283,6 +286,10 @@
|
|
|
283
286
|
"common/managed/comments.ts",
|
|
284
287
|
"common/managed/finding-detail.d.ts",
|
|
285
288
|
"common/managed/finding-detail.js",
|
|
289
|
+
"common/managed/finding-values.d.ts",
|
|
290
|
+
"common/managed/finding-values.js",
|
|
291
|
+
"common/managed/lite-name.d.ts",
|
|
292
|
+
"common/managed/lite-name.js",
|
|
286
293
|
"common/managed/github-agents.ts",
|
|
287
294
|
"common/managed/github-logins.ts",
|
|
288
295
|
"common/managed/npm-packages.d.ts",
|
|
@@ -33,7 +33,7 @@ type Rebase = [sha: string, at: number, subjects: string[], more?: number]
|
|
|
33
33
|
export function agentMerges(list: PullList, since: number): AgentMerges {
|
|
34
34
|
const merges = new Map<string, number>()
|
|
35
35
|
const [rebases, pulls, pullRebases]: [Rebase[], [string, number, number][], Rebase[]] = [[], [], []]
|
|
36
|
-
for (const { number, merged } of list.
|
|
36
|
+
for (const { number, merged } of list.rows) {
|
|
37
37
|
if (!merged || merged.at < since) continue
|
|
38
38
|
const { sha, at, subjects, more = 0 } = merged
|
|
39
39
|
const rebase: Rebase | null = subjects ? more > 0 ? [sha, at, subjects, more] : [sha, at, subjects] : null
|
|
@@ -16,6 +16,8 @@ export const APP_AGENTS: readonly (AppAgent & AppAgentMarks)[] = Object.entries<
|
|
|
16
16
|
// and the app agents'.
|
|
17
17
|
export const AGENT_BRANCH_PREFIXES = ['claude/', 'codex/', ...APP_AGENTS.map(agent => agent.prefix)]
|
|
18
18
|
// What the marks agents leave on commits are read by, as the history keeps
|
|
19
|
-
// them (repository-history.ts commitMarks)
|
|
20
|
-
//
|
|
19
|
+
// them (repository-history.ts commitMarks), and branches' tips and the
|
|
20
|
+
// commits the app agents' accounts authored (repository-agents.ts): bumped
|
|
21
|
+
// as how they are told changes, so that the Agents tab reads again those
|
|
22
|
+
// kept by older rules.
|
|
21
23
|
export const AGENT_MARKS_READ = 1
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { createClient } from '@preventive/upstream/github.js'
|
|
13
13
|
import { upstreamGithub } from './github-errors.ts'
|
|
14
14
|
import { GithubApiError } from './github-app.ts'
|
|
15
|
-
import type
|
|
15
|
+
import { type LockedPackage, patchedIn } from './locked-packages.ts'
|
|
16
16
|
import { type Lru, lru } from './lru.ts'
|
|
17
17
|
import { type GithubAsked, type ListedLockfile, type LockfileStore, eachOf, lockedWith, lockfileFormat, readLockfile } from './pull-dependencies.ts'
|
|
18
18
|
|
|
@@ -57,16 +57,19 @@ export async function lockedAt(fullName: string, token: string, sha: string, kep
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// The versions those of `read`'s lockfiles `held` holds to lock, each once,
|
|
60
|
-
//
|
|
60
|
+
// each patch's file from the repository's root (patchedIn), the first of
|
|
61
|
+
// them that names one there, and their paths.
|
|
61
62
|
export function lockedIn(read: CommitLockfiles, held: (path: string) => boolean = () => true) {
|
|
62
63
|
const packages = new Map<string, LockedPackage>()
|
|
63
64
|
const paths: string[] = []
|
|
64
65
|
for (const lockfile of read.lockfiles.filter(({ path }) => held(path))) {
|
|
65
66
|
paths.push(lockfile.path)
|
|
66
67
|
for (const pkg of lockfile.packages) {
|
|
68
|
+
const found = patchedIn(lockfile.path, { ...pkg })
|
|
67
69
|
const kept = packages.get(lockedWith(pkg))
|
|
68
|
-
if (!kept) packages.set(lockedWith(pkg),
|
|
69
|
-
|
|
70
|
+
if (!kept) { packages.set(lockedWith(pkg), found); continue }
|
|
71
|
+
if (kept.dev && !pkg.dev) delete kept.dev
|
|
72
|
+
if (kept.patchPath === undefined && found.patchPath !== undefined) kept.patchPath = found.patchPath
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
return { packages: [...packages.values()], paths }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Compare's comparison of two managed bundles or two versions of an npm
|
|
2
|
-
// package
|
|
2
|
+
// package, the one shown as a patch changes it where Review asks
|
|
3
|
+
// (GET /api/bundles/:id/comparison?other=, GET /api/npm/comparison):
|
|
3
4
|
// their files paired, at one path or renamed, with the sizes, resolutions,
|
|
4
5
|
// dependency versions and line counts its Overview and Code read beside them,
|
|
5
6
|
// as common/bundle-comparison.js prepareComparison works them out. The viewer
|
|
@@ -14,7 +15,8 @@ import { prepareComparison } from '../common/bundle-comparison.js'
|
|
|
14
15
|
import type { BundleDetails } from '../common/bundle-metadata.js'
|
|
15
16
|
import type { OpenedBlob } from './blob-store.ts'
|
|
16
17
|
import { encodeBrotli } from './brotli.ts'
|
|
17
|
-
import { type NpmPackageFile, npmFileRows } from './npm-packages.ts'
|
|
18
|
+
import { type NpmPackageFile, type NpmVersionDocument, npmFileRows } from './npm-packages.ts'
|
|
19
|
+
import type { NpmPatch } from './npm-patch.ts'
|
|
18
20
|
|
|
19
21
|
// A side with no files, as Review compares a package's first version with:
|
|
20
22
|
// one for each comparison, as working one out keeps its line counts on it.
|
|
@@ -39,3 +41,12 @@ export async function comparisonBody(base: BundleDetails, other: BundleDetails,
|
|
|
39
41
|
const body = await encodeBrotli(Buffer.from(json))
|
|
40
42
|
return { size: body.byteLength, stream: Readable.from([body]) }
|
|
41
43
|
}
|
|
44
|
+
|
|
45
|
+
// A version's `files` as `patch` changes them (npm-patch.ts patchedFiles), as
|
|
46
|
+
// the viewer reads a version's (npm-loads.ts loadNpmPackageBody), with the
|
|
47
|
+
// patch, as sent: `{ name, version, private, integrity, patch, files }`.
|
|
48
|
+
export async function patchedVersionBody(doc: NpmVersionDocument, patch: NpmPatch, files: NpmPackageFile[]): Promise<OpenedBlob> {
|
|
49
|
+
const json = JSON.stringify({ name: doc.name, version: doc.version, private: doc.private, integrity: doc.dist.integrity, patch, files: npmFileRows(files) })
|
|
50
|
+
const body = await encodeBrotli(Buffer.from(json))
|
|
51
|
+
return { size: body.byteLength, stream: Readable.from([body]) }
|
|
52
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEPENDENCY_REVIEW_COLUMNS, DEPENDENCY_REVIEW_SCHEMA } from './dependency-reviews.ts'
|
|
1
|
+
import { DEPENDENCY_REVIEW_COLUMNS, DEPENDENCY_REVIEW_INDEXES, 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'
|
|
@@ -39,7 +39,7 @@ const LOCK = 'SELECT pg_advisory_xact_lock(1937006964, 1835101793)'
|
|
|
39
39
|
// Whether every version up to the latest is recorded, in one round trip at
|
|
40
40
|
// a cold start: a database with no versions table yet (undefined_table) has
|
|
41
41
|
// none. Outside a transaction, so the failed read aborts nothing.
|
|
42
|
-
const SCHEMA_VERSIONS =
|
|
42
|
+
const SCHEMA_VERSIONS = 33
|
|
43
43
|
async function currentSchema(db: PgConnection): Promise<boolean> {
|
|
44
44
|
try {
|
|
45
45
|
const { rows } = await db.query(`SELECT count(*) AS n FROM managed_schema_version WHERE version BETWEEN 1 AND ${SCHEMA_VERSIONS}`)
|
|
@@ -192,6 +192,16 @@ async function migrateDependencyReviewShasums(db: PgConnection): Promise<void> {
|
|
|
192
192
|
await db.query('INSERT INTO managed_schema_version VALUES (32)')
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
// The patch a dependency review is of, and where it was read, none of
|
|
196
|
+
// those written before; each author's one draft of a comparison held to its
|
|
197
|
+
// patch too.
|
|
198
|
+
async function migrateDependencyReviewPatches(db: PgConnection): Promise<void> {
|
|
199
|
+
if ((await db.query('SELECT version FROM managed_schema_version WHERE version = 33')).rows.length > 0) return
|
|
200
|
+
await db.query(`ALTER TABLE managed_dependency_review ${DEPENDENCY_REVIEW_COLUMNS.map(([column, type]) => `ADD COLUMN IF NOT EXISTS ${column} ${type}`).join(', ')}`)
|
|
201
|
+
await db.query(DEPENDENCY_REVIEW_INDEXES)
|
|
202
|
+
await db.query('INSERT INTO managed_schema_version VALUES (33)')
|
|
203
|
+
}
|
|
204
|
+
|
|
195
205
|
async function initialize(db: PgConnection): Promise<void> {
|
|
196
206
|
if (await currentSchema(db)) return
|
|
197
207
|
await db.query('BEGIN')
|
|
@@ -266,7 +276,7 @@ async function initialize(db: PgConnection): Promise<void> {
|
|
|
266
276
|
await db.query(postgresSchema(revisionSchema(true)) + '; INSERT INTO managed_schema_version VALUES (11)')
|
|
267
277
|
}
|
|
268
278
|
for (const migrate of [migrateRepositoryDefaultCache, migrateBundleBuildLeases, migrateBundleVisibility, migrateLinkReports, migrateRepositoryAliases, migrateHiddenTeams, migrateIssueFixes, migrateBundleProvenance, migrateUpstreamCache, migrateBundleBuildConditions, migrateSessionUploadKeys, migrateViewSessions, migrateGithubCommits, migrateTeamNpmScopes, migrateDependencyReviews, migrateGithubLinks, migrateRepositoryDependencyReviews,
|
|
269
|
-
migrateRepositoryDependencyReviewLockfiles, migrateRepoUploadIndexes, migrateDependencyReviewShasums]) await migrate(db)
|
|
279
|
+
migrateRepositoryDependencyReviewLockfiles, migrateRepoUploadIndexes, migrateDependencyReviewShasums, migrateDependencyReviewPatches]) await migrate(db)
|
|
270
280
|
await db.query('COMMIT')
|
|
271
281
|
} catch (err) {
|
|
272
282
|
await db.query('ROLLBACK')
|
package/server-managed/db.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { revisionSchema } from './revisions.ts'
|
|
|
10
10
|
import { MANAGED_SCHEMA, REPO_UPLOAD_INDEXES, VIEW_SESSION_INDEX } from './db-schema.ts'
|
|
11
11
|
import { GITHUB_LINK_USER_COLUMNS } from './github-links.ts'
|
|
12
12
|
import { REPOSITORY_DEPENDENCY_REVIEW_COLUMNS } from './repository-dependency-reviews.ts'
|
|
13
|
-
import { DEPENDENCY_REVIEW_COLUMNS } from './dependency-reviews.ts'
|
|
13
|
+
import { DEPENDENCY_REVIEW_COLUMNS, DEPENDENCY_REVIEW_INDEXES } from './dependency-reviews.ts'
|
|
14
14
|
import { STORAGE_SCHEMA } from './storage-db.ts'
|
|
15
15
|
import { GITHUB_COMMIT_SCHEMA, GITHUB_METADATA_SCHEMA, GITHUB_STATE_REASON_COLUMN } from './github-metadata.ts'
|
|
16
16
|
import { MANAGED_ISSUE_SCHEMA } from './managed-issues.ts'
|
|
@@ -126,6 +126,7 @@ export function openSqliteManagedDb(path: string, options: ManagedDbOptions = {}
|
|
|
126
126
|
ensureColumn(db, 'managed_selected_repo', 'active', 'INTEGER NOT NULL DEFAULT 1')
|
|
127
127
|
for (const [column, type] of REPOSITORY_DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_selected_repo', column, type)
|
|
128
128
|
for (const [column, type] of DEPENDENCY_REVIEW_COLUMNS) ensureColumn(db, 'managed_dependency_review', column, type)
|
|
129
|
+
db.exec(DEPENDENCY_REVIEW_INDEXES)
|
|
129
130
|
ensureColumn(db, 'managed_finding_triage_event', 'report_id', 'TEXT')
|
|
130
131
|
ensureColumn(db, 'managed_finding_triage_event', 'report', 'TEXT')
|
|
131
132
|
ensureColumn(db, 'managed_finding_triage_event', 'repo', 'TEXT')
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// only caller of the hook); and is told of none where it answers nothing, or
|
|
8
8
|
// fails, which is logged.
|
|
9
9
|
import type { DependencyReview, DependencyReviewStore } from './dependency-reviews.ts'
|
|
10
|
-
import { type
|
|
10
|
+
import { type PullDependencies, eachOf } from './pull-dependencies.ts'
|
|
11
11
|
import type { PublishedReview, RepoReviewState } from './repository-dependency-reviews.ts'
|
|
12
12
|
import type { DependencyReviewEntry, ReviewedDependencies } from './reviewed-dependencies.ts'
|
|
13
13
|
|
|
@@ -20,13 +20,12 @@ const REVIEWS_AT_ONCE = 200
|
|
|
20
20
|
|
|
21
21
|
// `review` as the hook is given it: each version with the integrity and
|
|
22
22
|
// shasum the review keeps of the tarball it read, and the version it is of
|
|
23
|
-
// with the patch
|
|
24
|
-
|
|
25
|
-
function entryOf(review: DependencyReview, locked?: AddedPackage | undefined): DependencyReviewEntry {
|
|
23
|
+
// with the patch it is of, where it is a patched one's.
|
|
24
|
+
function entryOf(review: DependencyReview): DependencyReviewEntry {
|
|
26
25
|
return { ecosystem: 'npm', old: review.basePackage === null ? null : { name: review.basePackage, version: review.baseVersion ?? '', integrity: review.baseIntegrity ?? '',
|
|
27
26
|
...review.baseShasum ? { shasum: review.baseShasum } : {} },
|
|
28
27
|
new: { name: review.package, version: review.version, integrity: review.integrity, ...review.shasum ? { shasum: review.shasum } : {},
|
|
29
|
-
...
|
|
28
|
+
...review.patchHash ? { patchHash: review.patchHash } : {} },
|
|
30
29
|
status: review.status!, coverage: review.coverage === 'full' ? 'full' : 'partial', comment: review.comment,
|
|
31
30
|
files: review.entries.filter(entry => entry.status !== null || entry.comment.trim() !== '').map(entry => ({ path: entry.file, status: entry.status, comment: entry.comment })),
|
|
32
31
|
reviewer: review.userLogin, publishedAt: review.publishedAt! }
|
|
@@ -38,13 +37,13 @@ const ownPublished = (review: DependencyReview, user: string) => review.user ===
|
|
|
38
37
|
// What the host formats of `reviews`, of them `user`'s own published ones
|
|
39
38
|
// alone, and the pull request they are of the versions of, where it is
|
|
40
39
|
// one's; none where it formats none of them.
|
|
41
|
-
async function formatOwn(reviewed: ReviewedDependencies | null, user: string, reviews:
|
|
40
|
+
async function formatOwn(reviewed: ReviewedDependencies | null, user: string, reviews: DependencyReview[],
|
|
42
41
|
pullRequest?: { repository: string; number: number }): Promise<string | null> {
|
|
43
42
|
const format = reviewed?.formatDependencyReviews
|
|
44
|
-
const own = reviews.filter(
|
|
43
|
+
const own = reviews.filter(review => ownPublished(review, user))
|
|
45
44
|
if (!format || own.length === 0) return null
|
|
46
45
|
try {
|
|
47
|
-
const text = await format({ reviews: own.map(
|
|
46
|
+
const text = await format({ reviews: own.map(entryOf), ...pullRequest ? { pullRequest } : {} })
|
|
48
47
|
return typeof text === 'string' && text.trim() !== '' && text.length <= MAX_FORMATTED ? text : null
|
|
49
48
|
} catch (error) {
|
|
50
49
|
console.warn('reviewed dependencies: failed to format dependency reviews', error instanceof Error ? error.message : error)
|
|
@@ -57,16 +56,15 @@ async function formatOwn(reviewed: ReviewedDependencies | null, user: string, re
|
|
|
57
56
|
export function withFormatted<T extends DependencyReview>(reviewed: ReviewedDependencies | null, user: string, reviews: T[]): Promise<(T & { formatted?: string })[]> {
|
|
58
57
|
if (!reviewed?.formatDependencyReviews) return Promise.resolve(reviews)
|
|
59
58
|
return eachOf(reviews, FORMAT_AT_ONCE, async review => {
|
|
60
|
-
const formatted = ownPublished(review, user) ? await formatOwn(reviewed, user, [
|
|
59
|
+
const formatted = ownPublished(review, user) ? await formatOwn(reviewed, user, [review]) : null
|
|
61
60
|
return formatted ? { ...review, formatted } : review
|
|
62
61
|
})
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
function ownOfPull(told: PullDependencies, reviews: PublishedReview[], user: string): Map<string, AddedPackage> {
|
|
64
|
+
// The ids of those of `reviews` `user` published of the versions `told`
|
|
65
|
+
// adds, as its lockfiles lock each: of its version, of an integrity it is
|
|
66
|
+
// locked with and of its patch, whole or compared with a version it was.
|
|
67
|
+
function ownOfPull(told: PullDependencies, reviews: PublishedReview[], user: string): Set<string> {
|
|
70
68
|
// Theirs by the version they are of, each version probed once.
|
|
71
69
|
const theirs = new Map<string, PublishedReview[]>()
|
|
72
70
|
for (const review of reviews) {
|
|
@@ -74,13 +72,13 @@ function ownOfPull(told: PullDependencies, reviews: PublishedReview[], user: str
|
|
|
74
72
|
const key = `${review.package}@${review.version}`
|
|
75
73
|
theirs.set(key, [...theirs.get(key) ?? [], review])
|
|
76
74
|
}
|
|
77
|
-
const mine = new
|
|
75
|
+
const mine = new Set<string>()
|
|
78
76
|
if (theirs.size === 0) return mine
|
|
79
77
|
for (const pkg of told.lockfiles.flatMap(dir => 'added' in dir ? dir.added : [])) {
|
|
80
78
|
const hashes = new Set((pkg.integrity ?? '').split(/\s+/u).filter(Boolean))
|
|
81
79
|
for (const review of theirs.get(`${pkg.name}@${pkg.version}`) ?? []) {
|
|
82
80
|
if (!hashes.has(review.integrity) || (review.patchHash ?? '') !== (pkg.patchHash ?? '')) continue
|
|
83
|
-
if (review.basePackage === null || (review.basePackage === pkg.name && (pkg.was ?? []).includes(review.baseVersion ?? ''))) mine.
|
|
81
|
+
if (review.basePackage === null || (review.basePackage === pkg.name && (pkg.was ?? []).includes(review.baseVersion ?? ''))) mine.add(review.id!)
|
|
84
82
|
}
|
|
85
83
|
}
|
|
86
84
|
return mine
|
|
@@ -94,7 +92,7 @@ export async function formattedPull(reviewed: ReviewedDependencies | null, db: P
|
|
|
94
92
|
if (!reviewed?.formatDependencyReviews || !state) return null
|
|
95
93
|
const mine = ownOfPull(told, state.reviews, user)
|
|
96
94
|
if (mine.size === 0) return null
|
|
97
|
-
const ids = [...mine
|
|
95
|
+
const ids = [...mine], reviews: DependencyReview[] = []
|
|
98
96
|
for (let at = 0; at < ids.length; at += REVIEWS_AT_ONCE) reviews.push(...await db.listOwnDependencyReviewsById(user, ids.slice(at, at + REVIEWS_AT_ONCE)))
|
|
99
|
-
return formatOwn(reviewed, user, reviews
|
|
97
|
+
return formatOwn(reviewed, user, reviews, pullRequest)
|
|
100
98
|
}
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
import { ManagedMutationError } from './management.ts'
|
|
3
|
+
import type { PatchSource } from './npm-patch.ts'
|
|
3
4
|
import type { ManagedSql } from './sql.ts'
|
|
4
5
|
import { REVIEW_VERDICTS, type ReviewSummary, type ReviewVerdict, allowedReviewVerdicts, needsPackageJsonReview, summarizeReview } from '../common/dependency-reviews.js'
|
|
5
6
|
|
|
7
|
+
// A review is of a version, as npm's registry has it, by its integrity; of
|
|
8
|
+
// it as pnpm patches it (`patchHash`, the patch's hash as a lockfile names
|
|
9
|
+
// it), where it is patched; and compared with a version (its base), or none.
|
|
6
10
|
export interface ReviewContext {
|
|
7
|
-
ecosystem: 'npm'; package: string; version: string; integrity: string
|
|
11
|
+
ecosystem: 'npm'; package: string; version: string; integrity: string; patchHash: string | null
|
|
8
12
|
basePackage: string | null; baseVersion: string | null; baseIntegrity: string | null
|
|
9
13
|
}
|
|
10
14
|
// The shasums (sha1s, in hex) of the tarballs a review reads, its version's
|
|
11
15
|
// and its base's, where npm tells them: each held, with its integrity, to the
|
|
12
|
-
// tarball its reviewer was shown, as a review is written (http.ts)
|
|
13
|
-
|
|
16
|
+
// tarball its reviewer was shown, as a review is written (http.ts); and where
|
|
17
|
+
// its patch was read (`patch`, npm-patch.ts), a patched version's.
|
|
18
|
+
export interface ReviewedTarballs { shasum: string | null; baseShasum: string | null; patch?: PatchSource | null }
|
|
14
19
|
export interface ReviewInput { file: string; comment: string; status: ReviewVerdict | null }
|
|
15
20
|
export interface ReviewEntry extends ReviewInput { id: string; time: number }
|
|
16
21
|
export interface ReviewTeam { id: string; name: string }
|
|
17
|
-
export interface DependencyReview extends ReviewContext, ReviewedTarballs, ReviewSummary {
|
|
22
|
+
export interface DependencyReview extends ReviewContext, Omit<ReviewedTarballs, 'patch'>, ReviewSummary {
|
|
23
|
+
// Where its patch was read, as it was written last: whence it is read again.
|
|
24
|
+
patchSource: PatchSource | null
|
|
18
25
|
id: string; user: string | null; userLogin: string; userName: string | null; time: number; revision: number
|
|
19
26
|
publishedAt: number | null; revokedAt: number | null; entries: ReviewEntry[]; teams: ReviewTeam[]; files: string[] | null
|
|
20
27
|
nonTextFiles: string[] | null
|
|
@@ -27,7 +34,7 @@ export type ReviewChange = { action: 'save'; reviewId: string | null; revision:
|
|
|
27
34
|
| { action: 'revoke'; reviewId: string; revision: number }
|
|
28
35
|
export interface DependencyReviewStore {
|
|
29
36
|
listDependencyReviews(context: ReviewContext, user: string, allBases?: boolean): Promise<DependencyReview[]>
|
|
30
|
-
getLatestOwnDependencyReview(user: string, target: Pick<ReviewContext, 'ecosystem' | 'package' | 'version' | 'integrity'>): Promise<DependencyReview | null>
|
|
37
|
+
getLatestOwnDependencyReview(user: string, target: Pick<ReviewContext, 'ecosystem' | 'package' | 'version' | 'integrity' | 'patchHash'>): Promise<DependencyReview | null>
|
|
31
38
|
listOwnDependencyReviews(user: string, before: { time: number; id: string } | null): Promise<{ reviews: DependencyReview[]; next: { time: number; id: string } | null }>
|
|
32
39
|
// `user`'s own reviews of `ids`, none another's.
|
|
33
40
|
listOwnDependencyReviewsById(user: string, ids: string[]): Promise<DependencyReview[]>
|
|
@@ -40,14 +47,17 @@ export interface DependencyReviewStore {
|
|
|
40
47
|
listDependencyReviewEnabledTeams(): Promise<string[]>
|
|
41
48
|
setTeamDependencyReview(sessionId: string, teamId: string, enabled: boolean): Promise<boolean>
|
|
42
49
|
discardDependencyReview(sessionId: string, reviewId: string, revision: number): Promise<void>
|
|
43
|
-
// Written with the shasums of the tarballs read (`tarballs`),
|
|
44
|
-
// draft keeps as it is begun, saved and published; none
|
|
50
|
+
// Written with the shasums of the tarballs read (`tarballs`), and where its
|
|
51
|
+
// patch was, which a draft keeps as it is begun, saved and published; none
|
|
52
|
+
// where not told.
|
|
45
53
|
changeDependencyReview(context: ReviewContext, sessionId: string, change: ReviewChange, now: number, tarballs?: ReviewedTarballs): Promise<DependencyReview>
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
// An unpublished review belongs only to its author. Publication freezes all
|
|
49
57
|
// entries together and attaches that snapshot to its selected teams. Continuing
|
|
50
58
|
// makes a new review; published entries and their timestamps never change.
|
|
59
|
+
// Each author's one draft of a comparison is held so by
|
|
60
|
+
// DEPENDENCY_REVIEW_INDEXES, once its columns are.
|
|
51
61
|
export const DEPENDENCY_REVIEW_SCHEMA = `
|
|
52
62
|
CREATE TABLE IF NOT EXISTS managed_dependency_review (
|
|
53
63
|
id TEXT PRIMARY KEY,
|
|
@@ -57,13 +67,10 @@ CREATE TABLE IF NOT EXISTS managed_dependency_review (
|
|
|
57
67
|
base_package TEXT NOT NULL, base_version TEXT NOT NULL, base_integrity TEXT NOT NULL,
|
|
58
68
|
time INTEGER NOT NULL, revision INTEGER NOT NULL, published_at INTEGER, revoked_at INTEGER, files TEXT,
|
|
59
69
|
status TEXT CHECK (status IN ('accept', 'accept-with-nits', 'request-changes', 'reject')), comment TEXT NOT NULL DEFAULT '',
|
|
60
|
-
shasum TEXT NOT NULL DEFAULT '', base_shasum TEXT NOT NULL DEFAULT ''
|
|
70
|
+
shasum TEXT NOT NULL DEFAULT '', base_shasum TEXT NOT NULL DEFAULT '', patch_hash TEXT NOT NULL DEFAULT '', patch_source TEXT
|
|
61
71
|
) STRICT;
|
|
62
72
|
CREATE INDEX IF NOT EXISTS managed_dependency_review_context_idx
|
|
63
73
|
ON managed_dependency_review(ecosystem, package, version, integrity, base_package, base_version, base_integrity, time);
|
|
64
|
-
CREATE UNIQUE INDEX IF NOT EXISTS managed_dependency_review_unpublished_idx
|
|
65
|
-
ON managed_dependency_review(user_id, ecosystem, package, version, integrity, base_package, base_version, base_integrity)
|
|
66
|
-
WHERE published_at IS NULL;
|
|
67
74
|
CREATE INDEX IF NOT EXISTS managed_dependency_review_author_idx ON managed_dependency_review(user_id, time, id);
|
|
68
75
|
CREATE TABLE IF NOT EXISTS managed_dependency_file_review (
|
|
69
76
|
id TEXT PRIMARY KEY, review_id TEXT NOT NULL REFERENCES managed_dependency_review(id) ON DELETE CASCADE,
|
|
@@ -79,18 +86,28 @@ CREATE TABLE IF NOT EXISTS managed_dependency_review_team (
|
|
|
79
86
|
CREATE INDEX IF NOT EXISTS managed_dependency_review_team_idx ON managed_dependency_review_team(team_id, review_id);
|
|
80
87
|
`
|
|
81
88
|
// The columns added since the table was, for a database that has it already.
|
|
82
|
-
export const DEPENDENCY_REVIEW_COLUMNS = [['shasum', "TEXT NOT NULL DEFAULT ''"], ['base_shasum', "TEXT NOT NULL DEFAULT ''"]
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
export const DEPENDENCY_REVIEW_COLUMNS = [['shasum', "TEXT NOT NULL DEFAULT ''"], ['base_shasum', "TEXT NOT NULL DEFAULT ''"],
|
|
90
|
+
['patch_hash', "TEXT NOT NULL DEFAULT ''"], ['patch_source', 'TEXT']] as const
|
|
91
|
+
// Each author's one draft of a comparison, its patch's too, made once those
|
|
92
|
+
// columns are: the index held to it before patches were reviewed gone.
|
|
93
|
+
export const DEPENDENCY_REVIEW_INDEXES = `
|
|
94
|
+
DROP INDEX IF EXISTS managed_dependency_review_unpublished_idx;
|
|
95
|
+
CREATE UNIQUE INDEX IF NOT EXISTS managed_dependency_review_draft_idx
|
|
96
|
+
ON managed_dependency_review(user_id, ecosystem, package, version, integrity, patch_hash, base_package, base_version, base_integrity)
|
|
97
|
+
WHERE published_at IS NULL;`
|
|
98
|
+
const values = (c: ReviewContext) => [c.ecosystem, c.package, c.version, c.integrity, c.patchHash ?? '', c.basePackage ?? '', c.baseVersion ?? '', c.baseIntegrity ?? '']
|
|
99
|
+
const targetWhere = 'ecosystem = ? AND package = ? AND version = ? AND integrity = ? AND patch_hash = ?'
|
|
85
100
|
const contextWhere = `${targetWhere} AND base_package = ? AND base_version = ? AND base_integrity = ?`
|
|
86
101
|
// Its author by the login they reviewed as, and by the name the app's user
|
|
87
102
|
// has now (null once they are none, or have none), as the lists show them.
|
|
88
103
|
const columns = `id, user_id AS user, user_login AS userLogin, (SELECT n.name FROM managed_user n WHERE n.id = user_id) AS userName, ecosystem, package, version, integrity,
|
|
89
|
-
NULLIF(base_package, '') AS basePackage, NULLIF(base_version, '') AS baseVersion, NULLIF(base_integrity, '') AS baseIntegrity,
|
|
90
|
-
NULLIF(shasum, '') AS shasum, NULLIF(base_shasum, '') AS baseShasum,
|
|
104
|
+
NULLIF(patch_hash, '') AS patchHash, NULLIF(base_package, '') AS basePackage, NULLIF(base_version, '') AS baseVersion, NULLIF(base_integrity, '') AS baseIntegrity,
|
|
105
|
+
NULLIF(shasum, '') AS shasum, NULLIF(base_shasum, '') AS baseShasum, patch_source AS patchSource,
|
|
91
106
|
time, revision, published_at AS publishedAt, revoked_at AS revokedAt, files, status, comment`
|
|
92
107
|
const conflict = () => { throw new ManagedMutationError(409, 'review-conflict') }
|
|
93
|
-
|
|
108
|
+
// What a review keeps of the bytes it read: its tarballs' shasums, and where
|
|
109
|
+
// its patch was.
|
|
110
|
+
const shasums = (tarballs: ReviewedTarballs | undefined) => [tarballs?.shasum ?? '', tarballs?.baseShasum ?? '', tarballs?.patch ? JSON.stringify(tarballs.patch) : null]
|
|
94
111
|
|
|
95
112
|
// Who reviews dependencies in team `t`, user `u` as the query joins them: an
|
|
96
113
|
// admin, in any team; a member who manages, in theirs; a member who triages,
|
|
@@ -136,7 +153,7 @@ async function requireReviewer(db: ManagedSql, user: string) {
|
|
|
136
153
|
if ((await teams(db, user)).length === 0) throw new ManagedMutationError(403, 'review-access-required')
|
|
137
154
|
}
|
|
138
155
|
|
|
139
|
-
type ReviewRow = Omit<DependencyReview, 'files' | 'entries' | 'teams' | keyof ReviewSummary> & { files: string | null }
|
|
156
|
+
type ReviewRow = Omit<DependencyReview, 'files' | 'entries' | 'teams' | 'patchSource' | keyof ReviewSummary> & { files: string | null; patchSource: string | null }
|
|
140
157
|
async function expandReviews(db: ManagedSql, rows: unknown[], user: string): Promise<DependencyReview[]> {
|
|
141
158
|
const reviews = rows as ReviewRow[]
|
|
142
159
|
if (reviews.length === 0) return []
|
|
@@ -162,13 +179,14 @@ async function expandReviews(db: ManagedSql, rows: unknown[], user: string): Pro
|
|
|
162
179
|
}
|
|
163
180
|
// A review as its row, its entries and the teams its reader reads it in
|
|
164
181
|
// tell it: its files' snapshot, its entries a file's each (fileEntries), and
|
|
165
|
-
// what they sum up to.
|
|
182
|
+
// what they sum up to; where its patch was read.
|
|
166
183
|
function expanded(review: ReviewRow, entries: ReviewEntry[], readIn: ReviewTeam[]): DependencyReview {
|
|
167
184
|
const snapshot = review.files === null ? null : JSON.parse(review.files) as string[] | { paths: string[]; nonTextFiles: string[] }
|
|
168
185
|
const files = Array.isArray(snapshot) ? snapshot : snapshot?.paths ?? null
|
|
169
186
|
const nonTextFiles = Array.isArray(snapshot) ? null : snapshot?.nonTextFiles ?? []
|
|
170
187
|
const ownEntries = fileEntries(entries)
|
|
171
|
-
return { ...review,
|
|
188
|
+
return { ...review, patchSource: review.patchSource ? JSON.parse(review.patchSource) as PatchSource : null, files, nonTextFiles, entries: ownEntries,
|
|
189
|
+
...summarizeReview(ownEntries, files, nonTextFiles), teams: readIn }
|
|
172
190
|
}
|
|
173
191
|
// Rows gathered into one JSON array value (json_group_array): its text on
|
|
174
192
|
// SQLite, parsed already on Postgres, where none gather as NULL.
|
|
@@ -185,7 +203,7 @@ async function publish(db: ManagedSql, current: DependencyReview | undefined, us
|
|
|
185
203
|
if (!summarizeReview(entries, change.files, change.nonTextFiles).checkedFiles) throw new ManagedMutationError(400, 'no-reviewed-files')
|
|
186
204
|
if (!change.final || !allowedReviewVerdicts(entries.filter(entry => !change.nonTextFiles.includes(entry.file))).includes(change.final.status)) throw new ManagedMutationError(400, 'bad-final-verdict')
|
|
187
205
|
if (needsPackageJsonReview(entries, change.files, change.nonTextFiles)) throw new ManagedMutationError(400, 'package-json-not-reviewed')
|
|
188
|
-
await db.prepare(`UPDATE managed_dependency_review SET published_at = ?, files = ?, status = ?, comment = ?, shasum = ?, base_shasum = ?, revision = revision + 1
|
|
206
|
+
await db.prepare(`UPDATE managed_dependency_review SET published_at = ?, files = ?, status = ?, comment = ?, shasum = ?, base_shasum = ?, patch_source = ?, revision = revision + 1
|
|
189
207
|
WHERE id = ?`).run(now, JSON.stringify({ paths: [...new Set(change.files)].toSorted(), nonTextFiles: [...new Set(change.nonTextFiles)].toSorted() }),
|
|
190
208
|
change.final.status, change.final.comment, ...shasums(tarballs), current.id)
|
|
191
209
|
} else {
|
|
@@ -211,7 +229,7 @@ async function listContext(db: ManagedSql, context: ReviewContext, user: string,
|
|
|
211
229
|
AND (user_id = ? OR (published_at IS NOT NULL AND EXISTS (
|
|
212
230
|
SELECT 1 FROM managed_dependency_review_team rt JOIN managed_team t ON t.id = rt.team_id JOIN managed_user u ON u.id = ?
|
|
213
231
|
WHERE rt.review_id = r.id AND ${reviewsIn(false)})))
|
|
214
|
-
ORDER BY time, id`).all(...values(context).slice(0, allBases ?
|
|
232
|
+
ORDER BY time, id`).all(...values(context).slice(0, allBases ? 5 : 8), user, user)
|
|
215
233
|
return expandReviews(db, rows, user)
|
|
216
234
|
}
|
|
217
235
|
|
|
@@ -277,8 +295,8 @@ export function dependencyReviewMethods(db: ManagedSql): DependencyReviewStore {
|
|
|
277
295
|
listTeamDependencyReviews: (teamId, user, before) => listTeam(db, teamId, user, before),
|
|
278
296
|
async getLatestOwnDependencyReview(user, target) {
|
|
279
297
|
const row = await db.prepare(`SELECT ${columns} FROM managed_dependency_review
|
|
280
|
-
WHERE user_id = ? AND
|
|
281
|
-
.get(user, target.ecosystem, target.package, target.version, target.integrity)
|
|
298
|
+
WHERE user_id = ? AND ${targetWhere} ORDER BY time DESC, id DESC LIMIT 1`)
|
|
299
|
+
.get(user, target.ecosystem, target.package, target.version, target.integrity, target.patchHash ?? '')
|
|
282
300
|
return row ? (await expandReviews(db, [row], user))[0]! : null
|
|
283
301
|
},
|
|
284
302
|
async discardDependencyReview(sessionId, reviewId, revision) {
|
|
@@ -312,8 +330,8 @@ export function dependencyReviewMethods(db: ManagedSql): DependencyReviewStore {
|
|
|
312
330
|
if (await unpublished()) return conflict()
|
|
313
331
|
const next = randomUUID()
|
|
314
332
|
await db.prepare(`INSERT INTO managed_dependency_review
|
|
315
|
-
(id, user_id, user_login, ecosystem, package, version, integrity, base_package, base_version, base_integrity, shasum, base_shasum, time, revision)
|
|
316
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`).run(next, user.id, user.login, ...values(context), ...shasums(tarballs), now)
|
|
333
|
+
(id, user_id, user_login, ecosystem, package, version, integrity, patch_hash, base_package, base_version, base_integrity, shasum, base_shasum, patch_source, time, revision)
|
|
334
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)`).run(next, user.id, user.login, ...values(context), ...shasums(tarballs), now)
|
|
317
335
|
return next
|
|
318
336
|
}
|
|
319
337
|
if (change.action === 'start') {
|
|
@@ -336,7 +354,7 @@ export function dependencyReviewMethods(db: ManagedSql): DependencyReviewStore {
|
|
|
336
354
|
} else {
|
|
337
355
|
if (current?.publishedAt != null) return conflict()
|
|
338
356
|
if (change.entry.status === null && !change.entryId) throw new ManagedMutationError(400, 'bad-review')
|
|
339
|
-
if (id) await db.prepare('UPDATE managed_dependency_review SET time = ?, shasum = ?, base_shasum = ?, revision = revision + 1 WHERE id = ?').run(now, ...shasums(tarballs), id)
|
|
357
|
+
if (id) await db.prepare('UPDATE managed_dependency_review SET time = ?, shasum = ?, base_shasum = ?, patch_source = ?, revision = revision + 1 WHERE id = ?').run(now, ...shasums(tarballs), id)
|
|
340
358
|
else { if (change.entryId) return conflict(); id = await create() }
|
|
341
359
|
await saveFile(db, id, change.entry, change.entryId, now)
|
|
342
360
|
}
|
|
@@ -6,16 +6,13 @@ import { Buffer } from 'node:buffer'
|
|
|
6
6
|
import { createHash, createSign } from 'node:crypto'
|
|
7
7
|
import { createClient } from '@preventive/upstream/github.js'
|
|
8
8
|
import type { ManagedConfig } from './config.ts'
|
|
9
|
-
import { GithubApiError,
|
|
10
|
-
import {
|
|
11
|
-
import { timed } from './request-timing.ts'
|
|
9
|
+
import { GithubApiError, upstreamGithub } from './github-errors.ts'
|
|
10
|
+
import { USER_AGENT, githubJson, onTokenRejected } from './github-requests.ts'
|
|
12
11
|
|
|
13
12
|
export { GithubApiError } from './github-errors.ts'
|
|
14
|
-
export { onTokenRejected } from './github-requests.ts'
|
|
13
|
+
export { githubJson, onTokenRejected } from './github-requests.ts'
|
|
15
14
|
|
|
16
15
|
const GITHUB_API = 'https://api.github.com'
|
|
17
|
-
const API_VERSION = '2022-11-28'
|
|
18
|
-
const USER_AGENT = 'deepview-triage'
|
|
19
16
|
const PER_PAGE = 100
|
|
20
17
|
const INSTALLATION_TOKEN_TTL_MS = 5 * 60_000
|
|
21
18
|
const INSTALLATION_TOKEN_EXPIRY_MARGIN_MS = 60_000
|
|
@@ -69,42 +66,6 @@ export function mergeRepos(...lists: ConnectedRepo[][]): ConnectedRepo[] {
|
|
|
69
66
|
return [...byName.values()].toSorted((a, b) => a.fullName.localeCompare(b.fullName))
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
// One GitHub API call returning parsed JSON, optionally Bearer-authed by a
|
|
73
|
-
// user token, App JWT, or installation token, `body` sent as JSON (a GraphQL
|
|
74
|
-
// query's, given longer to answer). Network / non-2xx / malformed fold into a
|
|
75
|
-
// GithubApiError (401 passes through for the caller to handle). Identical GETs
|
|
76
|
-
// share an outstanding read through its body. POSTs only opt in for read-only
|
|
77
|
-
// GraphQL queries (repository-history.ts), never for mutations or token mints.
|
|
78
|
-
export function githubJson(url: string, token: string | null, fetchImpl: typeof fetch, method: 'GET' | 'POST' = 'GET', body?: unknown,
|
|
79
|
-
readOnly = method === 'GET'): Promise<unknown> {
|
|
80
|
-
if (readOnly) return shareGithubRead(fetchImpl, JSON.stringify([url, token, method, body]), () => githubJson(url, token, fetchImpl, method, body, false))
|
|
81
|
-
// Each call told in its request's Server-Timing (request-timing.ts).
|
|
82
|
-
return timed('gh', async () => {
|
|
83
|
-
let res: Response
|
|
84
|
-
const started = Date.now()
|
|
85
|
-
try {
|
|
86
|
-
res = await fetchImpl(url, {
|
|
87
|
-
method,
|
|
88
|
-
signal: AbortSignal.timeout(body === undefined ? 10_000 : 20_000),
|
|
89
|
-
redirect: 'error',
|
|
90
|
-
headers: {
|
|
91
|
-
...(token == null ? {} : { authorization: `Bearer ${token}` }), 'accept': 'application/vnd.github+json',
|
|
92
|
-
...(body === undefined ? {} : { 'content-type': 'application/json' }), 'user-agent': USER_AGENT, 'x-github-api-version': API_VERSION,
|
|
93
|
-
},
|
|
94
|
-
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
95
|
-
})
|
|
96
|
-
} catch (error) { throw githubUnreachable(error, started) }
|
|
97
|
-
if (res.status === 401 && token) tokenRejected(token, fetchImpl)
|
|
98
|
-
if (!res.ok) await throwGithubResponseError(res)
|
|
99
|
-
// The answer's body is read within the same timeout: one not read whole is
|
|
100
|
-
// GitHub not reached, as one not answered is; only one that isn't JSON is
|
|
101
|
-
// malformed.
|
|
102
|
-
try { return await res.json() } catch (error) {
|
|
103
|
-
throw error instanceof SyntaxError ? new GithubApiError(502, 'github-malformed') : githubUnreachable(error, started)
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
}
|
|
107
|
-
|
|
108
69
|
// Validate one repo object into a ConnectedRepo (or null to skip), tagging it
|
|
109
70
|
// with the installation it was reached through (null for the user-token path). A
|
|
110
71
|
// missing numeric id or full name → skip (can't key/locate it). Archived repos
|