@preventive/triage 1.0.0-alpha.5 → 1.0.0-alpha.50
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/LICENSE-APACHE +202 -0
- package/README.md +16 -0
- package/api/reap.ts +17 -0
- package/cli.js +6 -0
- package/client/dependency-paths.d.ts +3 -0
- package/client/dependency-paths.js +36 -0
- package/client/finding-link.js +305 -0
- package/client/linked-findings.d.ts +1 -0
- package/client/linked-findings.js +115 -0
- package/common/bundle-commit.d.ts +1 -0
- package/common/bundle-commit.js +4 -0
- package/common/bundle-compare-diff.d.ts +3 -0
- package/common/bundle-compare-diff.js +357 -0
- package/common/bundle-compare-inputs.d.ts +2 -0
- package/common/bundle-compare-inputs.js +65 -0
- package/common/bundle-compare-versions.js +172 -0
- package/common/bundle-comparison.d.ts +8 -0
- package/common/bundle-comparison.js +62 -0
- package/common/bundle-metadata.d.ts +15 -0
- package/common/bundle-metadata.js +226 -0
- package/common/bundle-pkg-of.d.ts +1 -0
- package/common/bundle-pkg-of.js +90 -0
- package/common/bundle-reasons.d.ts +2 -0
- package/common/bundle-reasons.js +21 -0
- package/common/bundle-source-links.d.ts +6 -0
- package/common/bundle-source-links.js +216 -0
- package/common/bundle-sourcemap.d.ts +10 -0
- package/common/bundle-sourcemap.js +100 -0
- package/common/bundle-sources.d.ts +7 -0
- package/common/bundle-sources.js +333 -0
- package/common/bundle-stats.js +41 -0
- package/common/bundle-tabs.js +1 -0
- package/common/code-language.js +65 -0
- package/common/default-scan-models.ts +31 -0
- package/common/dependency-reviews.d.ts +18 -0
- package/common/dependency-reviews.js +64 -0
- package/common/finding-duplicates.js +58 -0
- package/common/finding-filters.js +274 -0
- package/common/finding-id.js +47 -0
- package/common/github-issue-labels.d.ts +1 -0
- package/common/github-issue-labels.js +11 -0
- package/common/github-pr.ts +56 -0
- package/common/gzip.js +59 -0
- package/common/line-diff.d.ts +8 -0
- package/common/line-diff.js +57 -0
- package/common/linked-workspace-groups.js +63 -0
- package/common/managed/agent-tasks.ts +292 -0
- package/common/managed/comments.ts +34 -0
- package/common/managed/finding-detail.d.ts +2 -0
- package/common/managed/finding-detail.js +32 -0
- package/common/managed/github-agents.ts +114 -0
- package/common/managed/github-logins.ts +49 -0
- package/common/managed/npm-packages.d.ts +5 -0
- package/common/managed/npm-packages.js +34 -0
- package/common/managed/permissions.ts +31 -0
- package/common/managed/report-content.ts +72 -0
- package/common/managed/report-filter.ts +109 -0
- package/common/managed/repository-alias.ts +65 -0
- package/common/managed/roles.ts +35 -0
- package/common/managed/routes.d.ts +2 -0
- package/common/managed/routes.js +347 -0
- package/common/managed/scan-models.ts +6 -0
- package/common/managed/team-app.d.ts +5 -0
- package/common/managed/team-app.js +31 -0
- package/common/managed/triage.ts +83 -0
- package/common/managed/upload-seal.ts +126 -0
- package/common/pretty-print.d.ts +5 -0
- package/common/pretty-print.js +24 -0
- package/common/revalidation-conflicts.js +48 -0
- package/common/revalidation-input-groups.js +56 -0
- package/common/save-error-reason.ts +20 -7
- package/common/scan-server.ts +13 -0
- package/common/security-links.d.ts +5 -0
- package/common/security-links.js +33 -0
- package/common/server-info.ts +46 -0
- package/common/utf8.d.ts +3 -0
- package/common/utf8.js +45 -0
- package/common/workspace-app.js +39 -0
- package/common/workspace-groups.js +176 -0
- package/out/activity.js +4 -0
- package/out/brotli-fallback.js +3 -3
- package/out/chunk-2WDZPXVL.js +3 -0
- package/out/chunk-2XUEZHIP.js +28 -0
- package/out/chunk-2ZDZWLFY.js +1 -0
- package/out/chunk-4HI2MSAV.js +17 -0
- package/out/chunk-4XMP7YVH.js +36 -0
- package/out/chunk-6LNQTKAV.js +52 -0
- package/out/chunk-6S2U2UYG.js +232 -0
- package/out/chunk-75YLHBCM.js +3 -0
- package/out/chunk-7BKKYVWR.js +7 -0
- package/out/chunk-7C7DWL7I.js +1 -0
- package/out/chunk-7VAWINQF.js +6 -0
- package/out/chunk-C4Z6ZBXV.js +10 -0
- package/out/chunk-FC65TPUB.js +1 -0
- package/out/chunk-FKBXNUW4.js +1 -0
- package/out/chunk-GDPCLSOS.js +2 -0
- package/out/chunk-GO7FT4OD.js +1 -0
- package/out/chunk-HTBYHSPG.js +1 -0
- package/out/chunk-IYPY47MC.js +1 -0
- package/out/chunk-IZ7POWI3.js +18 -0
- package/out/chunk-JCIIBLKR.js +1 -0
- package/out/chunk-NRPC74AM.js +1 -0
- package/out/chunk-OOJM4CTU.js +1 -0
- package/out/chunk-PBZPYIG7.js +1 -0
- package/out/chunk-PNHBZGDD.js +1 -0
- package/out/chunk-PWFUOLZK.js +17 -0
- package/out/chunk-PXK7EU4V.js +1 -0
- package/out/chunk-QB4CBOX2.js +1 -0
- package/out/chunk-R3NZFXVR.js +1 -0
- package/out/chunk-R5O6FAW4.js +2 -0
- package/out/chunk-SXV2T5C4.js +80 -0
- package/out/chunk-TQJDYHVE.js +2 -0
- package/out/chunk-TUII34K2.js +1 -0
- package/out/chunk-UK7RGJT6.js +3 -0
- package/out/chunk-VFWMQ3LG.js +2 -0
- package/out/chunk-VH4XMZIW.js +2 -0
- package/out/chunk-WCPJDAYA.js +10 -0
- package/out/chunk-WFYLMF6Z.js +10 -0
- package/out/chunk-WYLQWYLA.js +1 -0
- package/out/client-managed-import.js +4 -0
- package/out/client-managed.js +33 -0
- package/out/client-sync.js +11 -17
- package/out/compare.js +35 -0
- package/out/dialogs.js +55 -0
- package/out/github-issue-authorized.html +3 -0
- package/out/github-issue-authorized.js +1 -0
- package/out/graph.js +17 -4
- package/out/index.html +90 -8
- package/out/prism.js +14 -10
- package/out/scan.js +2 -0
- package/out/stasis.svg +20 -0
- package/out/terminal.js +366 -39
- package/out/view.css +1 -1
- package/out/view.js +19 -144
- package/package.json +362 -59
- package/risk/actions/findings.js +161 -0
- package/risk/actions/resolve.js +219 -0
- package/risk/actions/scripts/commands.js +182 -0
- package/risk/actions/scripts/grade.js +113 -0
- package/risk/actions/scripts/packages.js +123 -0
- package/risk/actions/scripts/shell.js +169 -0
- package/risk/actions/uses.js +76 -0
- package/risk/actions/workflow.js +110 -0
- package/risk/files/characters.js +196 -0
- package/risk/files/hosts.js +205 -0
- package/risk/files/readability.js +177 -0
- package/risk/files/runs-code.js +615 -0
- package/risk/files/script.js +118 -0
- package/risk/files/stamps.js +47 -0
- package/risk/index.d.ts +254 -0
- package/risk/index.js +11 -0
- package/risk/npm/assess.js +285 -0
- package/risk/npm/combined.js +68 -0
- package/risk/npm/compare.js +257 -0
- package/risk/npm/dependencies.js +172 -0
- package/risk/npm/diff.js +79 -0
- package/risk/npm/downloads.js +84 -0
- package/risk/npm/files.js +445 -0
- package/risk/npm/held.js +63 -0
- package/risk/npm/install-scripts.js +34 -0
- package/risk/npm/malware.js +36 -0
- package/risk/npm/manifest.js +65 -0
- package/risk/npm/new-dependency.js +60 -0
- package/risk/npm/old-version.js +108 -0
- package/risk/npm/registry.js +388 -0
- package/risk/npm/release.js +85 -0
- package/risk/npm/score.js +59 -0
- package/risk/npm/specs.js +124 -0
- package/risk/npm/tarball.js +96 -0
- package/server-common/database-config.ts +16 -0
- package/server-common/initialize.ts +18 -0
- package/server-common/npm-advisories.ts +101 -0
- package/{server → server-common}/origin.ts +5 -5
- package/server-common/reap.ts +48 -0
- package/server-common/scan-config.ts +19 -0
- package/server-common/standalone.ts +29 -0
- package/server-common/storage-crypto.ts +182 -0
- package/server-common/storage-log.ts +38 -0
- package/server-common/vercel-blob.ts +103 -0
- package/server-e2e/app.ts +487 -0
- package/{server → server-e2e}/bus-receiver.ts +2 -1
- package/{server → server-e2e}/cli.js +9 -4
- package/{server → server-e2e}/config.ts +50 -42
- package/{server → server-e2e}/db-neon.ts +2 -2
- package/{server → server-e2e}/db-revision-sql.ts +1 -1
- package/{server → server-e2e}/db-stmt.ts +2 -2
- package/{server → server-e2e}/db.ts +7 -7
- package/{server → server-e2e}/http.ts +44 -13
- package/{server → server-e2e}/hub.ts +40 -9
- package/server-e2e/index.ts +17 -0
- package/server-e2e/lifecycle.ts +95 -0
- package/{server → server-e2e}/neon-driver.ts +2 -2
- package/{server → server-e2e}/npm-proxy.ts +12 -145
- package/{server → server-e2e}/objstore/blob-vercel.ts +10 -114
- package/{server → server-e2e}/objstore/blob.ts +1 -1
- package/server-e2e/objstore/fetch-mint-guard.ts +74 -0
- package/{server → server-e2e}/objstore/handlers.ts +9 -8
- package/{server → server-e2e}/objstore/init.ts +19 -27
- package/{server → server-e2e}/objstore/reaper.ts +1 -1
- package/server-e2e/objstore/rest-deny.ts +28 -0
- package/server-e2e/objstore/rest-mint.ts +224 -0
- package/{server → server-e2e}/objstore/rest.ts +3 -3
- package/{server → server-e2e}/objstore/store-neon.ts +6 -10
- package/{server → server-e2e}/objstore/store.ts +7 -7
- package/{server → server-e2e}/objstore/tokens.ts +2 -2
- package/{server → server-e2e}/pubsub.ts +12 -9
- package/{server → server-e2e}/sign.ts +4 -4
- package/{server → server-e2e}/sse-server.ts +60 -33
- package/{server → server-e2e}/static.ts +31 -21
- package/{server → server-e2e}/sync-handlers.ts +64 -30
- package/{server → server-e2e}/ws-server.ts +20 -9
- package/server-managed/activity.ts +258 -0
- package/server-managed/agent-merges.ts +109 -0
- package/server-managed/app-agents.ts +21 -0
- package/server-managed/avatar-store.ts +13 -0
- package/server-managed/blob-store.ts +17 -0
- package/server-managed/brotli.ts +10 -0
- package/server-managed/bundle-advisories.ts +120 -0
- package/server-managed/bundle-advisory-inventory.ts +77 -0
- package/server-managed/bundle-build-diagnostics.d.ts +10 -0
- package/server-managed/bundle-build-diagnostics.js +23 -0
- package/server-managed/bundle-build-leases.ts +41 -0
- package/server-managed/bundle-build-worker.js +115 -0
- package/server-managed/bundle-build.ts +200 -0
- package/server-managed/bundle-cache.ts +211 -0
- package/server-managed/bundle-catalog.ts +29 -0
- package/server-managed/bundle-commits.ts +140 -0
- package/server-managed/bundle-store.ts +24 -0
- package/server-managed/bundle-summary-cache.ts +130 -0
- package/server-managed/bundle.ts +119 -0
- package/server-managed/cache-storage.ts +20 -0
- package/server-managed/cli.js +21 -0
- package/server-managed/combined.ts +47 -0
- package/server-managed/comments.ts +151 -0
- package/server-managed/commit-coauthors.ts +106 -0
- package/server-managed/commit-lockfiles.ts +73 -0
- package/server-managed/comparison.ts +41 -0
- package/server-managed/config.ts +164 -0
- package/server-managed/content-access.ts +15 -0
- package/server-managed/crypto.ts +25 -0
- package/server-managed/db-methods.ts +1923 -0
- package/server-managed/db-neon.ts +458 -0
- package/server-managed/db-schema.ts +238 -0
- package/server-managed/db-table-names.ts +22 -0
- package/server-managed/db.ts +142 -0
- package/server-managed/dependabot-access.ts +60 -0
- package/server-managed/dependabot-alerts.ts +185 -0
- package/server-managed/dependency-review-comments.ts +100 -0
- package/server-managed/dependency-reviews.ts +346 -0
- package/server-managed/github-accounts.ts +60 -0
- package/server-managed/github-actions.ts +71 -0
- package/server-managed/github-app.ts +415 -0
- package/server-managed/github-errors.ts +70 -0
- package/server-managed/github-graphql.ts +39 -0
- package/server-managed/github-issue-links.ts +140 -0
- package/server-managed/github-issue-oauth.ts +47 -0
- package/server-managed/github-issues.ts +152 -0
- package/server-managed/github-link-oauth.ts +69 -0
- package/server-managed/github-links.ts +204 -0
- package/server-managed/github-metadata.ts +213 -0
- package/server-managed/github-oauth.ts +238 -0
- package/server-managed/github-pulls.ts +227 -0
- package/server-managed/github-requests.ts +39 -0
- package/server-managed/github-users.ts +30 -0
- package/server-managed/http-response.ts +76 -0
- package/server-managed/http.ts +5329 -0
- package/server-managed/import-triage.ts +51 -0
- package/server-managed/index.ts +192 -0
- package/server-managed/kept-briefly.ts +37 -0
- package/server-managed/kept.ts +28 -0
- package/server-managed/link-reports.ts +162 -0
- package/server-managed/locked-packages.ts +129 -0
- package/server-managed/lockfile-scope.ts +49 -0
- package/server-managed/lru.ts +30 -0
- package/server-managed/managed-issues.ts +78 -0
- package/server-managed/management-catalog.ts +45 -0
- package/server-managed/management.ts +99 -0
- package/server-managed/npm-delta-worker.js +23 -0
- package/server-managed/npm-delta.ts +194 -0
- package/server-managed/npm-insights.ts +449 -0
- package/server-managed/npm-kept.ts +215 -0
- package/server-managed/npm-loads.ts +264 -0
- package/server-managed/npm-manifest.ts +115 -0
- package/server-managed/npm-packages.ts +454 -0
- package/server-managed/npm-provenance.ts +123 -0
- package/server-managed/npm-risk.ts +285 -0
- package/server-managed/object-storage-disk.ts +148 -0
- package/server-managed/object-storage-vercel.ts +67 -0
- package/server-managed/object-storage.ts +75 -0
- package/server-managed/package-entry-points.ts +51 -0
- package/server-managed/pretty-print.ts +434 -0
- package/server-managed/public-workspace.ts +238 -0
- package/server-managed/pull-comment-reviews.ts +161 -0
- package/server-managed/pull-dependencies.ts +348 -0
- package/server-managed/pull-dependency-reviews.ts +328 -0
- package/server-managed/pull-reviews.ts +205 -0
- package/server-managed/queue.ts +39 -0
- package/server-managed/release-commits.ts +84 -0
- package/server-managed/rename-diff-worker.js +26 -0
- package/server-managed/rename-diff.ts +279 -0
- package/server-managed/repo-path.ts +21 -0
- package/server-managed/report-migration.ts +35 -0
- package/server-managed/report-query.ts +10 -0
- package/server-managed/report-response.ts +16 -0
- package/server-managed/report-sources.ts +178 -0
- package/server-managed/report-store.ts +85 -0
- package/server-managed/repository-actions.ts +218 -0
- package/server-managed/repository-agents.ts +355 -0
- package/server-managed/repository-aliases.ts +91 -0
- package/server-managed/repository-browser.ts +294 -0
- package/server-managed/repository-changes.ts +284 -0
- package/server-managed/repository-code.ts +215 -0
- package/server-managed/repository-dependency-reviews.ts +165 -0
- package/server-managed/repository-discovery.ts +82 -0
- package/server-managed/repository-history.ts +432 -0
- package/server-managed/repository-issues.ts +195 -0
- package/server-managed/repository-policy.ts +27 -0
- package/server-managed/repository-pulls.ts +156 -0
- package/server-managed/repository-pulse.ts +460 -0
- package/server-managed/repository-risk-assess.ts +105 -0
- package/server-managed/repository-risk-audit.ts +79 -0
- package/server-managed/repository-risk-read.ts +170 -0
- package/server-managed/repository-risk.ts +491 -0
- package/server-managed/repository-squashes.ts +168 -0
- package/server-managed/request-timing.ts +63 -0
- package/server-managed/reviewed-dependencies.ts +150 -0
- package/server-managed/revisions.ts +35 -0
- package/server-managed/scan-downloads.ts +87 -0
- package/server-managed/session.ts +132 -0
- package/server-managed/slugs.ts +38 -0
- package/server-managed/solidity-entry-points.ts +71 -0
- package/server-managed/sql-postgres.ts +34 -0
- package/server-managed/sql.ts +88 -0
- package/server-managed/static.ts +28 -0
- package/server-managed/storage-command.ts +16 -0
- package/server-managed/storage-db.ts +183 -0
- package/server-managed/storage-encryption.ts +290 -0
- package/server-managed/storage-maintenance.ts +259 -0
- package/server-managed/storage-payload.ts +121 -0
- package/server-managed/storage-stores.ts +91 -0
- package/server-managed/storage.ts +82 -0
- package/server-managed/team-app.ts +134 -0
- package/server-managed/team-catalog.ts +7 -0
- package/server-managed/team-feed.ts +180 -0
- package/server-managed/team-npm-scopes.ts +106 -0
- package/server-managed/team-reports.ts +307 -0
- package/server-managed/triage-response.ts +16 -0
- package/server-managed/type-only-worker.js +19 -0
- package/server-managed/type-only.ts +105 -0
- package/server-managed/uploads.ts +106 -0
- package/server-managed/upstream-cache.ts +243 -0
- package/server-managed/view-as.ts +58 -0
- package/server-managed/workspace-shares.ts +169 -0
- package/server.ts +70 -0
- package/server/index.ts +0 -480
- package/server/lifecycle.ts +0 -208
- /package/{LICENSE → LICENSE-MIT} +0 -0
- /package/{server → server-e2e}/auth.ts +0 -0
- /package/{server → server-e2e}/config.example.json +0 -0
- /package/{server → server-e2e}/objstore/blob-fs.ts +0 -0
- /package/{server → server-e2e}/objstore/fs.ts +0 -0
- /package/{server → server-e2e}/objstore/sign.ts +0 -0
- /package/{server → server-e2e}/peer.ts +0 -0
- /package/{server → server-e2e}/sse-session.ts +0 -0
- /package/{server → server-e2e}/util.ts +0 -0
- /package/{server → server-e2e}/validation.ts +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// What the code viewer's Agents tab reconstructs agents' sessions
|
|
2
|
+
// from (GET /api/admin/repositories/agents, common/managed/agent-tasks.ts):
|
|
3
|
+
// a page of the repository's pull requests, the most recently updated first,
|
|
4
|
+
// as kept for the Pulse too (repository-pulls.ts readPulls), those past
|
|
5
|
+
// them searched for, and of its default branch's commits, the newest first,
|
|
6
|
+
// those no pull request merged, read from its history as the Commits tab
|
|
7
|
+
// keeps it, the
|
|
8
|
+
// marks an agent left kept with each (repository-history.ts commitMarks),
|
|
9
|
+
// whether a pull request brought one looked up for those marked alone, and
|
|
10
|
+
// kept by the commit; on the first page, its latest issues and the newest of
|
|
11
|
+
// its agents' branches (GitHub lists branches by name alone, so every one an
|
|
12
|
+
// agent's prefix names is read, as many as fifty pages of each, and the newest
|
|
13
|
+
// kept). Each artifact's own texts (never comments: an agent links its
|
|
14
|
+
// session in a pull request's body and its commits' messages) are read here
|
|
15
|
+
// for the marks an agent leaves, and a pull request's labels for one naming
|
|
16
|
+
// an agent, and those alone are answered, never the texts. GraphQL alone
|
|
17
|
+
// gives them, so it needs a token.
|
|
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'
|
|
20
|
+
import { AGENT_BRANCH_PREFIXES, APP_AGENTS } from './app-agents.ts'
|
|
21
|
+
import { GithubApiError } from './github-app.ts'
|
|
22
|
+
import type { GraphqlError } from './github-graphql.ts'
|
|
23
|
+
import { ACTOR, ALL_BRANCHES, type HistoryCommit, type HistoryPages, type NamedAccount, actorOf, commitAccount, commitMarks, githubGraphql, historyFrom, historyMarks,
|
|
24
|
+
inBatches, linkNoreplyUsers, messageMarks, variablesOf } from './repository-history.ts'
|
|
25
|
+
import type { PullList } from './repository-pulls.ts'
|
|
26
|
+
|
|
27
|
+
export const AGENT_PULLS_PAGE = 50
|
|
28
|
+
export const AGENT_COMMITS_PAGE = 100
|
|
29
|
+
const AGENT_ISSUES = 100
|
|
30
|
+
const AGENT_BRANCHES = 500
|
|
31
|
+
const MAX_BRANCH_PAGES = 50
|
|
32
|
+
const CLOSES = 5
|
|
33
|
+
// A pull request's labels read, for one naming an agent: GitHub's most a
|
|
34
|
+
// page.
|
|
35
|
+
const LABELS = 100
|
|
36
|
+
const MAX_TITLE = 1000
|
|
37
|
+
const MAX_NAME = 256
|
|
38
|
+
const LOGIN = /^[A-Za-z\d][A-Za-z\d-]{0,38}(?:\[bot\])?$/u
|
|
39
|
+
const SHA = /^[a-f\d]{40}$/u
|
|
40
|
+
// Commits whether a pull request brought each is looked up of, in a query,
|
|
41
|
+
// and queries at once.
|
|
42
|
+
const PULLED_COMMITS = 100
|
|
43
|
+
const PULLED_PARALLEL = 4
|
|
44
|
+
|
|
45
|
+
// The app agents' accounts' no-reply addresses, by each login of theirs.
|
|
46
|
+
const AGENT_EMAILS = APP_AGENTS.flatMap(({ id }) => GITHUB_AGENTS[id as GithubAgentId].accounts
|
|
47
|
+
.flatMap(({ id: account, logins }: GithubAgentAccount) => account === undefined ? [] : logins.map(login => `${account}+${login}@users.noreply.github.com`)))
|
|
48
|
+
|
|
49
|
+
// An agent's branches, a page at a time, as GitHub lists them, by name;
|
|
50
|
+
// each its tip commit's time, message and author.
|
|
51
|
+
const BRANCHES_QUERY = `query($owner: String!, $name: String!, $prefix: String!, $after: String) {
|
|
52
|
+
repository(owner: $owner, name: $name) {
|
|
53
|
+
refs(refPrefix: $prefix, first: 100, after: $after) {
|
|
54
|
+
pageInfo { hasNextPage endCursor }
|
|
55
|
+
nodes { prefix name target { ... on Commit { committedDate message author { email user { login databaseId } } } } }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}`
|
|
59
|
+
// What a commit of the default branch is read by.
|
|
60
|
+
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 commits the app agents' accounts
|
|
62
|
+
// authored, by their GitHub no-reply addresses, however far back: a bot's
|
|
63
|
+
// commits pushed with no pull request here (as before a repository moved)
|
|
64
|
+
// may lie deeper than pages are read on to.
|
|
65
|
+
const AUTHORED_QUERY = `query($owner: String!, $name: String!, $emails: [String!]!) {
|
|
66
|
+
repository(owner: $owner, name: $name) {
|
|
67
|
+
defaultBranchRef { target { ... on Commit { history(first: ${AGENT_COMMITS_PAGE}, author: { emails: $emails }) { nodes { ${COMMIT_FIELDS} } } } } }
|
|
68
|
+
}
|
|
69
|
+
}`
|
|
70
|
+
// What a pull request is read by, for its row (pull): its texts for the
|
|
71
|
+
// marks an agent leaves in them (its body, its last commit's message, its
|
|
72
|
+
// merge commit's), never answered; its commits counted beside (`commits {
|
|
73
|
+
// totalCount }`, as the reads of it ask theirs).
|
|
74
|
+
export const AGENT_PULL_FIELDS = `number title state isDraft createdAt updatedAt author { __typename ${ACTOR} } body headRefName isCrossRepository
|
|
75
|
+
headRepositoryOwner { login } additions deletions last: commits(last: 1) { nodes { commit { message } } } mergeCommit { message }
|
|
76
|
+
closingIssuesReferences(first: ${CLOSES}) { nodes { number } } labels(first: ${LABELS}) { nodes { name } }`
|
|
77
|
+
// On the first page, the default branch's head and the latest issues.
|
|
78
|
+
const PAGE_QUERY = `query($owner: String!, $name: String!) {
|
|
79
|
+
repository(owner: $owner, name: $name) {
|
|
80
|
+
defaultBranchRef { target { oid } }
|
|
81
|
+
issues(first: ${AGENT_ISSUES}, orderBy: { field: UPDATED_AT, direction: DESC }) {
|
|
82
|
+
nodes { number title state createdAt updatedAt author { ${ACTOR} } }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}`
|
|
86
|
+
// A page of the pull requests a search finds, as GitHub sorts them, on from
|
|
87
|
+
// where `after` says its page before ended.
|
|
88
|
+
const SEARCH_QUERY = `query($query: String!, $after: String) {
|
|
89
|
+
search(type: ISSUE, query: $query, first: ${AGENT_PULLS_PAGE}, after: $after) {
|
|
90
|
+
pageInfo { endCursor } nodes { ... on PullRequest { ${AGENT_PULL_FIELDS} commits { totalCount } } }
|
|
91
|
+
}
|
|
92
|
+
}`
|
|
93
|
+
|
|
94
|
+
type Login = { login?: unknown; databaseId?: unknown } | null | undefined
|
|
95
|
+
export type PullNode = { number?: unknown; title?: unknown; state?: unknown; isDraft?: unknown; createdAt?: unknown; updatedAt?: unknown; author?: Login; body?: unknown
|
|
96
|
+
headRefName?: unknown; isCrossRepository?: unknown; headRepositoryOwner?: Login; additions?: unknown; deletions?: unknown; commits?: { totalCount?: unknown } | null
|
|
97
|
+
last?: { nodes?: ({ commit?: { message?: unknown } | null } | null)[] } | null; mergeCommit?: { message?: unknown } | null
|
|
98
|
+
closingIssuesReferences?: { nodes?: ({ number?: unknown } | null)[] }; labels?: { nodes?: ({ name?: unknown } | null)[] } }
|
|
99
|
+
type GitActor = { email?: unknown; user?: Login }
|
|
100
|
+
type CommitNode = { oid?: unknown; messageHeadline?: unknown; message?: unknown; committedDate?: unknown; additions?: unknown; deletions?: unknown
|
|
101
|
+
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 }
|
|
104
|
+
type Connection<T> = { pageInfo?: { hasNextPage?: unknown; endCursor?: unknown }; nodes?: (T | null)[] }
|
|
105
|
+
// Whether a pull request brought each commit, `c<n>` the `n`th's.
|
|
106
|
+
const pulledQuery = (count: number) => `query($owner: String!, $name: String!, ${variablesOf('o', count, 'GitObjectID!')}) {
|
|
107
|
+
repository(owner: $owner, name: $name) {
|
|
108
|
+
${Array.from({ length: count }, (_, at) => `c${at}: object(oid: $o${at}) { ... on Commit { associatedPullRequests(first: 1) { totalCount } } }`).join('\n ')}
|
|
109
|
+
}
|
|
110
|
+
}`
|
|
111
|
+
type Pulled = { repository?: Record<string, { associatedPullRequests?: { totalCount?: unknown } } | null> | null }
|
|
112
|
+
type Page = { repository?: { defaultBranchRef?: { target?: { oid?: unknown; history?: Connection<CommitNode> } | null } | null
|
|
113
|
+
issues?: Connection<IssueNode> } | null }
|
|
114
|
+
type Found = { search?: { pageInfo?: { endCursor?: unknown }; nodes?: (PullNode | null)[] } | null } | null
|
|
115
|
+
type RefsPage = { repository?: { refs?: Connection<RefNode> } | null }
|
|
116
|
+
|
|
117
|
+
const count = (value: unknown) => Number.isSafeInteger(value) && (value as number) >= 0 ? value as number : 0
|
|
118
|
+
const number = (value: unknown) => Number.isSafeInteger(value) && (value as number) > 0 ? value as number : null
|
|
119
|
+
const timeOf = (value: unknown) => {
|
|
120
|
+
const at = typeof value === 'string' ? Date.parse(value) : Number.NaN
|
|
121
|
+
return Number.isSafeInteger(at) ? at : null
|
|
122
|
+
}
|
|
123
|
+
const nameOf = (value: unknown, max = MAX_NAME) => typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : null
|
|
124
|
+
const loginOf = (author: Login) => typeof author?.login === 'string' && LOGIN.test(author.login) ? author.login : null
|
|
125
|
+
const PULL_STATES: Record<string, AgentPull['state']> = { OPEN: 'open', MERGED: 'merged', CLOSED: 'closed' }
|
|
126
|
+
|
|
127
|
+
// A pull request's row: what it is, and the marks an agent left on it.
|
|
128
|
+
export function pull(node: PullNode | null): AgentPull | null {
|
|
129
|
+
const at = number(node?.number)
|
|
130
|
+
if (!node || !at) return null
|
|
131
|
+
const author = accountOf(node.author)
|
|
132
|
+
return { number: at, title: nameOf(node.title, MAX_TITLE) ?? '', state: PULL_STATES[String(node.state)] ?? 'closed', draft: node.isDraft === true,
|
|
133
|
+
author: loginOf(node.author), authorId: author?.id ?? null, createdAt: timeOf(node.createdAt), updatedAt: timeOf(node.updatedAt), head: nameOf(node.headRefName),
|
|
134
|
+
// A fork's, its owner; one GitHub no longer names, a fork of its own.
|
|
135
|
+
fork: node.isCrossRepository === true ? loginOf(node.headRepositoryOwner) ?? `pull-${at}` : null,
|
|
136
|
+
additions: count(node.additions), deletions: count(node.deletions), commits: count(node.commits?.totalCount),
|
|
137
|
+
closes: (node.closingIssuesReferences?.nodes ?? []).flatMap(closed => number(closed?.number) ?? []),
|
|
138
|
+
signals: agentSignals([node.body, node.last?.nodes?.[0]?.commit?.message, node.mergeCommit?.message], APP_AGENTS, nameOf(node.headRefName),
|
|
139
|
+
author, (node.labels?.nodes ?? []).map(label => label?.name)) }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// An account as GitHub names it, by its login and id, both.
|
|
143
|
+
const both = ({ login, id }: NamedAccount): AgentAuthor | null => login !== null && id !== null ? { login, id } : null
|
|
144
|
+
const accountOf = (actor: Login) => both(actorOf(actor))
|
|
145
|
+
// A commit's author: the account GitHub knows by its address, else the one
|
|
146
|
+
// its no-reply address names by id and login, as a bot's, Copilot's or
|
|
147
|
+
// Dependabot's, is told by that alone (repository-history.ts commitAccount).
|
|
148
|
+
const authorOf = (actor: GitActor | null | undefined) => both(commitAccount(actor))
|
|
149
|
+
|
|
150
|
+
// A commit to the default branch no pull request merged, where an agent left
|
|
151
|
+
// its marks on it, or Claude authored it (commitMarks).
|
|
152
|
+
function directCommit(node: CommitNode | null): AgentCommit | null {
|
|
153
|
+
if (!node || typeof node.oid !== 'string' || !SHA.test(node.oid) || count(node.associatedPullRequests?.totalCount) > 0) return null
|
|
154
|
+
const author = authorOf(node.author)
|
|
155
|
+
const signals = commitMarks(messageMarks(node.message), { name: null, email: node.author?.email, account: commitAccount(node.author) })
|
|
156
|
+
if (!signals) return null
|
|
157
|
+
return { oid: node.oid, title: nameOf(node.messageHeadline, MAX_TITLE) ?? '', at: timeOf(node.committedDate),
|
|
158
|
+
author: author?.login ?? loginOf(node.author?.user), authorId: author?.id ?? null,
|
|
159
|
+
additions: count(node.additions), deletions: count(node.deletions), signals }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// A commit of the history kept an agent marked (`signals`,
|
|
163
|
+
// repository-history.ts historyMarks), as one of the default branch's.
|
|
164
|
+
const keptCommit = (commit: HistoryCommit, signals: AgentSignals): AgentCommit => ({ oid: commit.sha, title: commit.subject, at: commit.committedAt,
|
|
165
|
+
author: commit.authorLogin, authorId: commit.authorId ?? null, additions: commit.additions ?? 0, deletions: commit.deletions ?? 0, signals })
|
|
166
|
+
|
|
167
|
+
// Whether a pull request brought each commit, kept by the commit.
|
|
168
|
+
export interface CommitPulls {
|
|
169
|
+
read(shas: string[]): Promise<Map<string, boolean>>
|
|
170
|
+
write(sha: string, pulled: boolean): Promise<void>
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// A commit GitHub has none of, as one rewritten away: its field null.
|
|
174
|
+
const notFound = (error: GraphqlError) => error?.type === 'NOT_FOUND'
|
|
175
|
+
|
|
176
|
+
// Those of `shas` a pull request brought: as kept (`kept`), the rest looked
|
|
177
|
+
// up, PULLED_COMMITS a query, and kept; one GitHub has none of, none.
|
|
178
|
+
async function pulledCommits(fullName: string, token: string, shas: string[], kept: CommitPulls, fetchImpl: typeof fetch) {
|
|
179
|
+
const [owner, name] = fullName.split('/') as [string, string]
|
|
180
|
+
const known = await kept.read(shas)
|
|
181
|
+
await inBatches(shas.filter(sha => !known.has(sha)), PULLED_COMMITS, PULLED_PARALLEL, async batch => {
|
|
182
|
+
const data = await githubGraphql(pulledQuery(batch.length), { owner, name, ...Object.fromEntries(batch.map((sha, at) => [`o${at}`, sha])) }, token, fetchImpl,
|
|
183
|
+
notFound) as Pulled
|
|
184
|
+
await Promise.all(batch.map((sha, at) => {
|
|
185
|
+
const node = data?.repository?.[`c${at}`]
|
|
186
|
+
if (!node) return Promise.resolve()
|
|
187
|
+
known.set(sha, count(node.associatedPullRequests?.totalCount) > 0)
|
|
188
|
+
return kept.write(sha, known.get(sha)!)
|
|
189
|
+
}))
|
|
190
|
+
})
|
|
191
|
+
return new Set(shas.filter(sha => known.get(sha) === true))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Where commits listed stop: those the cursor named but left unread still
|
|
195
|
+
// unlisted (`left`, newer than those below the commits read from the rest),
|
|
196
|
+
// then the parents of those listed not listed, the newest first (each by its
|
|
197
|
+
// child's time), as the Commits tab reads on from (repository-history.js):
|
|
198
|
+
// each read in its turn, as many as ALL_BRANCHES an answer, none dropped
|
|
199
|
+
// till MAX_FRONTIER are left (a cursor's length bounded, a URL's); null
|
|
200
|
+
// where none are.
|
|
201
|
+
function frontier(commits: HistoryCommit[], left: string[]) {
|
|
202
|
+
const listed = new Set(commits.map(commit => commit.sha))
|
|
203
|
+
const at = new Map<string, number>()
|
|
204
|
+
for (const commit of commits) {
|
|
205
|
+
for (const parent of commit.parents) if (!listed.has(parent)) at.set(parent, Math.max(at.get(parent) ?? Number.NEGATIVE_INFINITY, commit.committedAt ?? 0))
|
|
206
|
+
}
|
|
207
|
+
const below = [...at].toSorted((a, b) => b[1] - a[1]).map(([sha]) => sha)
|
|
208
|
+
const starts = [...new Set([...left.filter(sha => !listed.has(sha)), ...below])].slice(0, MAX_FRONTIER)
|
|
209
|
+
return starts.length > 0 ? starts.join(',') : null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function issue(node: IssueNode | null): AgentIssue | null {
|
|
213
|
+
const at = number(node?.number)
|
|
214
|
+
if (!node || !at) return null
|
|
215
|
+
return { number: at, title: nameOf(node.title, MAX_TITLE) ?? '', state: node.state === 'OPEN' ? 'open' : 'closed', author: loginOf(node.author),
|
|
216
|
+
authorId: accountOf(node.author)?.id ?? null, createdAt: timeOf(node.createdAt), updatedAt: timeOf(node.updatedAt) }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A branch under `prefix`, an agent's, named in full however GitHub splits
|
|
220
|
+
// its name from the prefix it was listed under.
|
|
221
|
+
const branch = (prefix: string) => (node: RefNode | null): AgentBranch | null => {
|
|
222
|
+
const ref = typeof node?.prefix === 'string' && typeof node.name === 'string' ? `${node.prefix}${node.name}` : null
|
|
223
|
+
const name = ref?.startsWith('refs/heads/') ? nameOf(ref.slice('refs/heads/'.length)) : null
|
|
224
|
+
const author = authorOf(node?.target?.author)
|
|
225
|
+
return name?.startsWith(prefix) ? { name, at: timeOf(node?.target?.committedDate), author: author?.login ?? loginOf(node?.target?.author?.user),
|
|
226
|
+
authorId: author?.id ?? null, signals: agentSignals([node?.target?.message], APP_AGENTS, name, author) } : null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The newest of an agent's branches, those under `prefix`: every one, as many
|
|
230
|
+
// as MAX_BRANCH_PAGES of them list, kept by their tips' times.
|
|
231
|
+
async function agentBranches(owner: string, name: string, prefix: string, token: string, fetchImpl: typeof fetch) {
|
|
232
|
+
const found: AgentBranch[] = []
|
|
233
|
+
let after: string | null = null
|
|
234
|
+
for (let page = 0; page < MAX_BRANCH_PAGES; page++) {
|
|
235
|
+
const refs: Connection<RefNode> | undefined = (await githubGraphql(BRANCHES_QUERY, { owner, name, prefix: `refs/heads/${prefix}`, after }, token, fetchImpl) as RefsPage)
|
|
236
|
+
?.repository?.refs
|
|
237
|
+
await linkNoreplyUsers((refs?.nodes ?? []).map(node => node?.target?.author), token, fetchImpl)
|
|
238
|
+
found.push(...(refs?.nodes ?? []).flatMap(node => branch(prefix)(node) ?? []))
|
|
239
|
+
const next: Connection<RefNode>['pageInfo'] = refs?.pageInfo
|
|
240
|
+
if (next?.hasNextPage !== true || typeof next.endCursor !== 'string') break
|
|
241
|
+
after = next.endCursor
|
|
242
|
+
}
|
|
243
|
+
return found.toSorted((a, b) => (b.at ?? 0) - (a.at ?? 0)).slice(0, AGENT_BRANCHES)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// What its answers are kept by, with their cursors (http.ts
|
|
247
|
+
// handleRepositoryAgents): bumped as what a page reads, or how it tells an
|
|
248
|
+
// agent, changes (with AGENT_MARKS_READ, app-agents.ts, where commits' marks
|
|
249
|
+
// do), so that no answer an older server kept is sent for a day.
|
|
250
|
+
export const AGENTS_READ_VERSION = 9
|
|
251
|
+
|
|
252
|
+
// A cursor of the default branch's commits: those the pages before stopped
|
|
253
|
+
// at (frontier), as many as MAX_FRONTIER, commas between, the first
|
|
254
|
+
// ALL_BRANCHES of them read next; one an older server gave, GitHub's,
|
|
255
|
+
// `<oid> <offset>`, read on from its commit.
|
|
256
|
+
const MAX_FRONTIER = 4 * ALL_BRANCHES
|
|
257
|
+
export const COMMITS_CURSOR = new RegExp(`^(?:[\\da-f]{40}(?:,[\\da-f]{40}){0,${MAX_FRONTIER - 1}}|[\\da-f]{40} \\d{1,9})$`, 'u')
|
|
258
|
+
const startsOf = (cursor: string) => cursor.split(/[ ,]/u).filter(part => SHA.test(part))
|
|
259
|
+
|
|
260
|
+
// A cursor of the pull requests: the last listed, by when it was updated and
|
|
261
|
+
// its number (`<ms>.<number>`), those below it read next; of a search's page
|
|
262
|
+
// all updated together, the time it searched by and where its page ended too
|
|
263
|
+
// (`~<ms>~<cursor>`), those updated with them read on through the same
|
|
264
|
+
// search; any other, as an older server gave (GitHub's), read from the
|
|
265
|
+
// newest again.
|
|
266
|
+
const POSITION = /^(\d{1,15})\.(\d{1,10})(?:~(\d{1,15})~([\w+/=-]{1,200}))?$/u
|
|
267
|
+
const positionOf = (item: AgentPull) => `${item.updatedAt ?? 0}.${item.number}`
|
|
268
|
+
// Those updated by a time, as GitHub's search takes it, to the second; any
|
|
269
|
+
// where it names none.
|
|
270
|
+
const updatedBy = (at: number) => Number.isFinite(at) ? ` updated:<=${new Date(at).toISOString().replace(/\.\d{3}Z$/u, '+00:00')}` : ''
|
|
271
|
+
|
|
272
|
+
// The repository's pull requests as kept (repository-pulls.ts PullList),
|
|
273
|
+
// read as far back as their first page where none are, and those updated
|
|
274
|
+
// since read as they are (http.ts keptPulls).
|
|
275
|
+
export type KeptPulls = () => Promise<PullList>
|
|
276
|
+
|
|
277
|
+
// A page of the repository's pull requests, the most recently updated
|
|
278
|
+
// first, below the one `cursor` names (none: from the newest): of those kept
|
|
279
|
+
// (`kept`), as far back as every one is read; past them, as GitHub's search
|
|
280
|
+
// finds those updated before, a page a query. With where it reads on from,
|
|
281
|
+
// null where none are left: past every one kept where all are.
|
|
282
|
+
async function agentPulls(fullName: string, token: string, cursor: string | null, kept: KeptPulls, fetchImpl: typeof fetch) {
|
|
283
|
+
const at = cursor === null ? null : POSITION.exec(cursor)
|
|
284
|
+
const [time, past] = at ? [Number(at[1]), Number(at[2])] : [Number.POSITIVE_INFINITY, 0]
|
|
285
|
+
const below = (item: AgentPull) => (item.updatedAt ?? 0) < time || ((item.updatedAt ?? 0) === time && item.number < past)
|
|
286
|
+
const list = await kept()
|
|
287
|
+
// Since when every one updated is read (as repository-pulls.ts pullsReadTo
|
|
288
|
+
// tells, which reads its pages by these fields): all since the year's
|
|
289
|
+
// start, -∞.
|
|
290
|
+
const read = list.next ? list.next.to ?? Number.POSITIVE_INFINITY : list.reach ?? Number.NEGATIVE_INFINITY
|
|
291
|
+
// As far back as every one is read: the year's start, where all since are.
|
|
292
|
+
const bottom = Math.max(read, list.since)
|
|
293
|
+
const listed = list.pulls.filter(item => below(item) && (item.updatedAt ?? 0) >= bottom)
|
|
294
|
+
const whole = list.all === true && read === Number.NEGATIVE_INFINITY
|
|
295
|
+
if (listed.length > 0) {
|
|
296
|
+
const page = listed.slice(0, AGENT_PULLS_PAGE)
|
|
297
|
+
return { pulls: page, after: listed.length > page.length || !whole ? positionOf(page.at(-1)!) : null }
|
|
298
|
+
}
|
|
299
|
+
if (whole) return { pulls: [], after: null }
|
|
300
|
+
// Those updated by when the last was, those updated with it again among
|
|
301
|
+
// them (GitHub orders those by no number), the last itself left out; a
|
|
302
|
+
// page all updated together read on through the same search, past them.
|
|
303
|
+
const [by, through] = at?.[3] === undefined ? [Math.min(time, bottom), null] : [Number(at[3]), at[4]!]
|
|
304
|
+
const found = await githubGraphql(SEARCH_QUERY, { query: `repo:${fullName} is:pr sort:updated-desc${updatedBy(by)}`, after: through }, token, fetchImpl) as Found
|
|
305
|
+
const nodes = found?.search?.nodes ?? []
|
|
306
|
+
const rows = nodes.flatMap(node => pull(node) ?? [])
|
|
307
|
+
const last = rows.at(-1)
|
|
308
|
+
const ended = found?.search?.pageInfo?.endCursor
|
|
309
|
+
const together = Number.isFinite(by) && rows.every(item => item.updatedAt === last?.updatedAt) && typeof ended === 'string' && /^[\w+/=-]{1,200}$/u.test(ended)
|
|
310
|
+
const after = nodes.length >= AGENT_PULLS_PAGE && last ? `${positionOf(last)}${together ? `~${by}~${ended}` : ''}` : null
|
|
311
|
+
return { pulls: rows.filter(item => item.updatedAt !== time || item.number !== past), after: after === cursor ? null : after }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// A page of what the repository's agents left: its pull requests below the
|
|
315
|
+
// one the cursor `pulls` names, as kept (`kept`, agentPulls), its default
|
|
316
|
+
// branch's commits on from those `commits` names, as its history is kept
|
|
317
|
+
// (`pages`, historyFrom: the first page from its head), whether a pull
|
|
318
|
+
// request brought those marked kept (`pulled`) (the first page where
|
|
319
|
+
// neither is named, with its issues, its
|
|
320
|
+
// agents' branches and the commits the app agents' accounts authored, and
|
|
321
|
+
// the app agents it may name), and the cursors reading on from each, null
|
|
322
|
+
// where either is read whole.
|
|
323
|
+
export async function repositoryAgents(fullName: string, token: string, cursors: { pulls: string | null; commits: string | null }, pages: HistoryPages,
|
|
324
|
+
pulled: CommitPulls, kept: KeptPulls, fetchImpl: typeof fetch = globalThis.fetch) {
|
|
325
|
+
const [owner, name] = fullName.split('/') as [string, string]
|
|
326
|
+
const first = cursors.pulls === null && cursors.commits === null
|
|
327
|
+
const [data, branches, authored, pulls] = await Promise.all([
|
|
328
|
+
first ? githubGraphql(PAGE_QUERY, { owner, name }, token, fetchImpl) as Promise<Page> : null,
|
|
329
|
+
first ? Promise.all(AGENT_BRANCH_PREFIXES.map(prefix => agentBranches(owner, name, prefix, token, fetchImpl))) : [],
|
|
330
|
+
first ? githubGraphql(AUTHORED_QUERY, { owner, name, emails: AGENT_EMAILS }, token, fetchImpl) as Promise<Page> : null,
|
|
331
|
+
first || cursors.pulls !== null ? agentPulls(fullName, token, cursors.pulls, kept, fetchImpl) : { pulls: [], after: null },
|
|
332
|
+
])
|
|
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
|
+
const named = cursors.commits === null ? first && typeof head === 'string' && SHA.test(head) ? [head] : [] : startsOf(cursors.commits)
|
|
337
|
+
const starts = named.slice(0, ALL_BRANCHES)
|
|
338
|
+
const history = starts.length > 0 ? (await historyFrom(fullName, token, starts, pages, fetchImpl)).commits : []
|
|
339
|
+
const marked = history.flatMap(commit => { const signals = historyMarks(commit); return signals ? [{ commit, signals }] : [] })
|
|
340
|
+
const nodes = authored?.repository?.defaultBranchRef?.target?.history?.nodes ?? []
|
|
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)])
|
|
343
|
+
const commits = [...marked.flatMap(({ commit, signals }) => brought.has(commit.sha) ? [] : [keptCommit(commit, signals)]), ...nodes.flatMap(node => directCommit(node) ?? [])]
|
|
344
|
+
const after = { pulls: pulls.after, commits: frontier(history, named.slice(ALL_BRANCHES)) }
|
|
345
|
+
return {
|
|
346
|
+
pulls: pulls.pulls.map(({ merged: _merged, ...row }: AgentPull & { merged?: unknown }) => row),
|
|
347
|
+
commits: commits.filter((commit, index) => commits.findIndex(other => other.oid === commit.oid) === index),
|
|
348
|
+
...first ? {
|
|
349
|
+
issues: (repository?.issues?.nodes ?? []).flatMap(node => issue(node) ?? []),
|
|
350
|
+
branches: branches.flat(),
|
|
351
|
+
agents: APP_AGENTS.map(({ id, name: agent, avatar }): AppAgent => ({ id, name: agent, ...avatar ? { avatar } : {} })),
|
|
352
|
+
} : {},
|
|
353
|
+
more: after.pulls !== null || after.commits !== null, after,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { reportRepoGithub } from '@preventive/report'
|
|
3
|
+
import { type RepositoryAliasInput, matchRepositoryAlias, repositoryAliasNeedsFilePrefix } from '../common/managed/repository-alias.ts'
|
|
4
|
+
import type { ManagedDb } from './db.ts'
|
|
5
|
+
import { ManagedMutationError } from './management.ts'
|
|
6
|
+
import { normalizeTeamPath } from './repo-path.ts'
|
|
7
|
+
import type { ManagedSql } from './sql.ts'
|
|
8
|
+
|
|
9
|
+
export const REPOSITORY_ALIAS_SCHEMA = `
|
|
10
|
+
CREATE TABLE IF NOT EXISTS managed_repository_alias (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
old_repo TEXT NOT NULL,
|
|
13
|
+
old_path TEXT NOT NULL DEFAULT '',
|
|
14
|
+
repo_id INTEGER NOT NULL REFERENCES managed_selected_repo(repo_id) ON DELETE CASCADE,
|
|
15
|
+
new_path TEXT NOT NULL DEFAULT '',
|
|
16
|
+
UNIQUE(old_repo, old_path)
|
|
17
|
+
) STRICT;
|
|
18
|
+
`
|
|
19
|
+
export interface RepositoryAlias extends RepositoryAliasInput { id: string }
|
|
20
|
+
export interface RepositoryAliasStore {
|
|
21
|
+
listRepositoryAliases(): Promise<RepositoryAlias[]>
|
|
22
|
+
saveRepositoryAlias(sessionId: string, id: string | null, input: unknown): Promise<RepositoryAlias>
|
|
23
|
+
deleteRepositoryAlias(sessionId: string, id: string): Promise<void>
|
|
24
|
+
getRepositoryImportLocation(github: string, directory: string, filePrefix?: string): Promise<{ repoId: number | null; directory: string }>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Keep expensive inventory reads outside database transactions and skip them
|
|
28
|
+
// unless a currently connected alias can use the bundle's existing paths.
|
|
29
|
+
export async function resolveRepositoryImportLocation(db: ManagedDb, github: string, directory: string, loadFilePrefix: () => Promise<string>) {
|
|
30
|
+
const aliases = await db.listRepositoryAliases()
|
|
31
|
+
let filePrefix = ''
|
|
32
|
+
if (repositoryAliasNeedsFilePrefix(github, directory, aliases)) {
|
|
33
|
+
const active = new Set((await db.listSelectedRepos()).map(repo => repo.repoId))
|
|
34
|
+
if (repositoryAliasNeedsFilePrefix(github, directory, aliases.filter(alias => active.has(alias.repoId)))) filePrefix = await loadFilePrefix()
|
|
35
|
+
}
|
|
36
|
+
return db.getRepositoryImportLocation(github, directory, filePrefix)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseAlias(input: unknown): RepositoryAliasInput {
|
|
40
|
+
if (!input || typeof input !== 'object') throw new ManagedMutationError(400, 'bad-alias')
|
|
41
|
+
const { oldRepo, oldPath, repoId, newPath } = input as RepositoryAliasInput
|
|
42
|
+
const github = typeof oldRepo === 'string' ? reportRepoGithub({ repo: { github: oldRepo.trim() } }) : null
|
|
43
|
+
if (!github) throw new ManagedMutationError(400, 'bad-old-repo')
|
|
44
|
+
if (typeof oldPath !== 'string' || typeof newPath !== 'string') throw new ManagedMutationError(400, 'bad-directory')
|
|
45
|
+
const old = normalizeTeamPath(oldPath), target = normalizeTeamPath(newPath)
|
|
46
|
+
if (!old.ok || !target.ok) throw new ManagedMutationError(400, 'bad-directory')
|
|
47
|
+
if (!Number.isSafeInteger(repoId) || repoId <= 0) throw new ManagedMutationError(400, 'bad-repo')
|
|
48
|
+
return { oldRepo: github.toLowerCase(), oldPath: old.path ?? '', repoId, newPath: target.path ?? '' }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function repositoryAliasMethods(db: ManagedSql): RepositoryAliasStore {
|
|
52
|
+
async function authorize(sessionId: string) {
|
|
53
|
+
const user = await db.prepare(`SELECT u.role FROM managed_session s JOIN managed_user u ON u.id = s.user_id
|
|
54
|
+
WHERE s.id = ? AND s.expires_at > ?`).get(sessionId, Date.now()) as { role: string } | undefined
|
|
55
|
+
if (!user) throw new ManagedMutationError(401, 'unauthenticated')
|
|
56
|
+
if (user.role !== 'admin') throw new ManagedMutationError(403, 'forbidden')
|
|
57
|
+
}
|
|
58
|
+
async function listRepositoryAliases() {
|
|
59
|
+
return await db.prepare(`SELECT id, old_repo AS oldRepo, old_path AS oldPath, repo_id AS repoId, new_path AS newPath
|
|
60
|
+
FROM managed_repository_alias ORDER BY old_repo, old_path, id`).all() as RepositoryAlias[]
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
listRepositoryAliases,
|
|
64
|
+
async saveRepositoryAlias(sessionId, id, input) {
|
|
65
|
+
await authorize(sessionId)
|
|
66
|
+
const alias = parseAlias(input)
|
|
67
|
+
if (!await db.prepare('SELECT repo_id FROM managed_selected_repo WHERE repo_id = ? AND active = 1').get(alias.repoId)) throw new ManagedMutationError(400, 'bad-repo')
|
|
68
|
+
if (id && !await db.prepare('SELECT id FROM managed_repository_alias WHERE id = ?').get(id)) throw new ManagedMutationError(404, 'no-alias')
|
|
69
|
+
const duplicate = await db.prepare('SELECT id FROM managed_repository_alias WHERE old_repo = ? AND old_path = ?').get(alias.oldRepo, alias.oldPath) as { id: string } | undefined
|
|
70
|
+
if (duplicate && duplicate.id !== id) throw new ManagedMutationError(409, 'alias-exists')
|
|
71
|
+
const key = id ?? randomUUID()
|
|
72
|
+
await db.prepare(`INSERT INTO managed_repository_alias (id, old_repo, old_path, repo_id, new_path) VALUES (?, ?, ?, ?, ?)
|
|
73
|
+
ON CONFLICT(id) DO UPDATE SET old_repo = excluded.old_repo, old_path = excluded.old_path, repo_id = excluded.repo_id, new_path = excluded.new_path`)
|
|
74
|
+
.run(key, alias.oldRepo, alias.oldPath, alias.repoId, alias.newPath)
|
|
75
|
+
return { id: key, ...alias }
|
|
76
|
+
},
|
|
77
|
+
async deleteRepositoryAlias(sessionId, id) {
|
|
78
|
+
await authorize(sessionId)
|
|
79
|
+
if (!(await db.prepare('DELETE FROM managed_repository_alias WHERE id = ?').run(id)).changes) throw new ManagedMutationError(404, 'no-alias')
|
|
80
|
+
},
|
|
81
|
+
async getRepositoryImportLocation(github, directory, filePrefix = '') {
|
|
82
|
+
const alias = matchRepositoryAlias(github, directory, await listRepositoryAliases(), filePrefix)
|
|
83
|
+
const normalized = normalizeTeamPath(alias?.directory ?? directory)
|
|
84
|
+
if (!normalized.ok) throw new ManagedMutationError(400, 'bad-directory')
|
|
85
|
+
const repo = await (alias
|
|
86
|
+
? db.prepare('SELECT repo_id AS repoId FROM managed_selected_repo WHERE active = 1 AND repo_id = ?').get(alias.repoId)
|
|
87
|
+
: db.prepare('SELECT repo_id AS repoId FROM managed_selected_repo WHERE active = 1 AND LOWER(full_name) = ?').get(github.toLowerCase())) as { repoId: number } | undefined
|
|
88
|
+
return { repoId: repo?.repoId ?? null, directory: normalized.path ?? '' }
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|