co-maintainer 0.4.0-beta.1
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 +21 -0
- package/README.md +105 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +9 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +2 -0
- package/dist/package.json +54 -0
- package/dist/src/ai/batch.d.ts +16 -0
- package/dist/src/ai/batch.js +117 -0
- package/dist/src/ai/fake.d.ts +7 -0
- package/dist/src/ai/fake.js +40 -0
- package/dist/src/ai/hetzner.d.ts +16 -0
- package/dist/src/ai/hetzner.js +69 -0
- package/dist/src/ai/mermaid.d.ts +27 -0
- package/dist/src/ai/mermaid.js +457 -0
- package/dist/src/ai/mermaid_loop.d.ts +12 -0
- package/dist/src/ai/mermaid_loop.js +100 -0
- package/dist/src/ai/openrouter.d.ts +9 -0
- package/dist/src/ai/openrouter.js +29 -0
- package/dist/src/ai/provider.d.ts +4 -0
- package/dist/src/ai/provider.js +63 -0
- package/dist/src/cli/args.d.ts +5 -0
- package/dist/src/cli/args.js +252 -0
- package/dist/src/cli/commands/probe.d.ts +2 -0
- package/dist/src/cli/commands/probe.js +122 -0
- package/dist/src/cli/commands/review.d.ts +3 -0
- package/dist/src/cli/commands/review.js +84 -0
- package/dist/src/cli/commands/serve.d.ts +18 -0
- package/dist/src/cli/commands/serve.js +183 -0
- package/dist/src/cli/commands/set.d.ts +7 -0
- package/dist/src/cli/commands/set.js +171 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +27 -0
- package/dist/src/cli/prompt.d.ts +4 -0
- package/dist/src/cli/prompt.js +26 -0
- package/dist/src/cli/review_args.d.ts +28 -0
- package/dist/src/cli/review_args.js +84 -0
- package/dist/src/cli/review_output.d.ts +2 -0
- package/dist/src/cli/review_output.js +10 -0
- package/dist/src/cli/review_result.d.ts +81 -0
- package/dist/src/cli/review_result.js +281 -0
- package/dist/src/config.d.ts +108 -0
- package/dist/src/config.js +138 -0
- package/dist/src/git/default_branch.d.ts +5 -0
- package/dist/src/git/default_branch.js +22 -0
- package/dist/src/github/app.d.ts +43 -0
- package/dist/src/github/app.js +120 -0
- package/dist/src/github/client.d.ts +17 -0
- package/dist/src/github/client.js +66 -0
- package/dist/src/github/collect.d.ts +7 -0
- package/dist/src/github/collect.js +368 -0
- package/dist/src/github/gh.d.ts +8 -0
- package/dist/src/github/gh.js +63 -0
- package/dist/src/github/jwt.d.ts +12 -0
- package/dist/src/github/jwt.js +84 -0
- package/dist/src/github/oauth.d.ts +9 -0
- package/dist/src/github/oauth.js +51 -0
- package/dist/src/github/pat.d.ts +10 -0
- package/dist/src/github/pat.js +60 -0
- package/dist/src/knowledge/facts.d.ts +3 -0
- package/dist/src/knowledge/facts.js +325 -0
- package/dist/src/knowledge/guide.d.ts +6 -0
- package/dist/src/knowledge/guide.js +82 -0
- package/dist/src/knowledge/probe.d.ts +12 -0
- package/dist/src/knowledge/probe.js +144 -0
- package/dist/src/knowledge/sections.d.ts +2 -0
- package/dist/src/knowledge/sections.js +13 -0
- package/dist/src/knowledge/skill.d.ts +10 -0
- package/dist/src/knowledge/skill.js +106 -0
- package/dist/src/knowledge/synthesis.d.ts +7 -0
- package/dist/src/knowledge/synthesis.js +315 -0
- package/dist/src/knowledge/types.d.ts +49 -0
- package/dist/src/knowledge/types.js +1 -0
- package/dist/src/knowledge/validate.d.ts +6 -0
- package/dist/src/knowledge/validate.js +134 -0
- package/dist/src/local/carry_over_store.d.ts +18 -0
- package/dist/src/local/carry_over_store.js +40 -0
- package/dist/src/local/codegraph_exclude.d.ts +3 -0
- package/dist/src/local/codegraph_exclude.js +26 -0
- package/dist/src/local/codegraph_prepare.d.ts +17 -0
- package/dist/src/local/codegraph_prepare.js +51 -0
- package/dist/src/local/git_ops.d.ts +12 -0
- package/dist/src/local/git_ops.js +98 -0
- package/dist/src/local/git_parse.d.ts +27 -0
- package/dist/src/local/git_parse.js +131 -0
- package/dist/src/local/git_revision.d.ts +15 -0
- package/dist/src/local/git_revision.js +141 -0
- package/dist/src/local/git_untracked.d.ts +21 -0
- package/dist/src/local/git_untracked.js +79 -0
- package/dist/src/local/review_local.d.ts +4 -0
- package/dist/src/local/review_local.js +273 -0
- package/dist/src/local/review_lock.d.ts +7 -0
- package/dist/src/local/review_lock.js +45 -0
- package/dist/src/pr/checkout.d.ts +25 -0
- package/dist/src/pr/checkout.js +136 -0
- package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
- package/dist/src/pr/codegraph_tool_args.js +34 -0
- package/dist/src/pr/codegraph_tools.d.ts +15 -0
- package/dist/src/pr/codegraph_tools.js +408 -0
- package/dist/src/pr/diff_summary.d.ts +6 -0
- package/dist/src/pr/diff_summary.js +52 -0
- package/dist/src/pr/findings.d.ts +14 -0
- package/dist/src/pr/findings.js +94 -0
- package/dist/src/pr/github_revision.d.ts +4 -0
- package/dist/src/pr/github_revision.js +39 -0
- package/dist/src/pr/hunks.d.ts +18 -0
- package/dist/src/pr/hunks.js +73 -0
- package/dist/src/pr/reviewer.d.ts +40 -0
- package/dist/src/pr/reviewer.js +582 -0
- package/dist/src/pr/rounds.d.ts +17 -0
- package/dist/src/pr/rounds.js +43 -0
- package/dist/src/pr/scope.d.ts +35 -0
- package/dist/src/pr/scope.js +84 -0
- package/dist/src/pr/snapshot.d.ts +11 -0
- package/dist/src/pr/snapshot.js +1 -0
- package/dist/src/pr/suggestion.d.ts +17 -0
- package/dist/src/pr/suggestion.js +70 -0
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +199 -0
- package/dist/src/remote/client_tools.d.ts +13 -0
- package/dist/src/remote/client_tools.js +30 -0
- package/dist/src/remote/fixtures.d.ts +1 -0
- package/dist/src/remote/fixtures.js +30 -0
- package/dist/src/remote/revision_from_submit.d.ts +2 -0
- package/dist/src/remote/revision_from_submit.js +26 -0
- package/dist/src/remote/schema.d.ts +14 -0
- package/dist/src/remote/schema.js +30 -0
- package/dist/src/remote/server/body.d.ts +18 -0
- package/dist/src/remote/server/body.js +73 -0
- package/dist/src/remote/server/routes.d.ts +1 -0
- package/dist/src/remote/server/routes.js +150 -0
- package/dist/src/remote/server/sessions.d.ts +16 -0
- package/dist/src/remote/server/sessions.js +48 -0
- package/dist/src/remote/server/sync.d.ts +3 -0
- package/dist/src/remote/server/sync.js +90 -0
- package/dist/src/remote/tool_bridge.d.ts +15 -0
- package/dist/src/remote/tool_bridge.js +84 -0
- package/dist/src/remote/validate.d.ts +8 -0
- package/dist/src/remote/validate.js +248 -0
- package/dist/src/review/carry_over.d.ts +63 -0
- package/dist/src/review/carry_over.js +372 -0
- package/dist/src/review/engine.d.ts +13 -0
- package/dist/src/review/engine.js +4 -0
- package/dist/src/review/guides.d.ts +12 -0
- package/dist/src/review/guides.js +44 -0
- package/dist/src/review/revision.d.ts +26 -0
- package/dist/src/review/revision.js +57 -0
- package/dist/src/server/api/activity.d.ts +1 -0
- package/dist/src/server/api/activity.js +12 -0
- package/dist/src/server/api/analytics.d.ts +1 -0
- package/dist/src/server/api/analytics.js +9 -0
- package/dist/src/server/api/installations.d.ts +4 -0
- package/dist/src/server/api/installations.js +24 -0
- package/dist/src/server/api/jobs.d.ts +1 -0
- package/dist/src/server/api/jobs.js +86 -0
- package/dist/src/server/api/json_body.d.ts +1 -0
- package/dist/src/server/api/json_body.js +14 -0
- package/dist/src/server/api/remote_tokens.d.ts +1 -0
- package/dist/src/server/api/remote_tokens.js +71 -0
- package/dist/src/server/api/repos.d.ts +4 -0
- package/dist/src/server/api/repos.js +136 -0
- package/dist/src/server/api/settings.d.ts +1 -0
- package/dist/src/server/api/settings.js +210 -0
- package/dist/src/server/app.d.ts +27 -0
- package/dist/src/server/app.js +147 -0
- package/dist/src/server/assets.d.ts +3 -0
- package/dist/src/server/assets.js +21 -0
- package/dist/src/server/auth.d.ts +44 -0
- package/dist/src/server/auth.js +188 -0
- package/dist/src/server/errors.d.ts +8 -0
- package/dist/src/server/errors.js +10 -0
- package/dist/src/server/http.d.ts +8 -0
- package/dist/src/server/http.js +117 -0
- package/dist/src/server/logo.d.ts +1 -0
- package/dist/src/server/logo.js +2 -0
- package/dist/src/server/pages/activity.d.ts +4 -0
- package/dist/src/server/pages/activity.js +200 -0
- package/dist/src/server/pages/add_repo.d.ts +9 -0
- package/dist/src/server/pages/add_repo.js +56 -0
- package/dist/src/server/pages/analytics.d.ts +2 -0
- package/dist/src/server/pages/analytics.js +193 -0
- package/dist/src/server/pages/client.d.ts +2 -0
- package/dist/src/server/pages/client.js +2 -0
- package/dist/src/server/pages/home.d.ts +16 -0
- package/dist/src/server/pages/home.js +87 -0
- package/dist/src/server/pages/knowledge.d.ts +2 -0
- package/dist/src/server/pages/knowledge.js +64 -0
- package/dist/src/server/pages/layout.d.ts +32 -0
- package/dist/src/server/pages/layout.js +230 -0
- package/dist/src/server/pages/pr.d.ts +2 -0
- package/dist/src/server/pages/pr.js +109 -0
- package/dist/src/server/pages/repo.d.ts +2 -0
- package/dist/src/server/pages/repo.js +92 -0
- package/dist/src/server/pages/repo_prs.d.ts +2 -0
- package/dist/src/server/pages/repo_prs.js +75 -0
- package/dist/src/server/pages/repo_remote.d.ts +2 -0
- package/dist/src/server/pages/repo_remote.js +38 -0
- package/dist/src/server/pages/repo_settings.d.ts +3 -0
- package/dist/src/server/pages/repo_settings.js +127 -0
- package/dist/src/server/pages/router.d.ts +17 -0
- package/dist/src/server/pages/router.js +312 -0
- package/dist/src/server/pages/settings.d.ts +2 -0
- package/dist/src/server/pages/settings.js +337 -0
- package/dist/src/server/pages/setup.d.ts +5 -0
- package/dist/src/server/pages/setup.js +38 -0
- package/dist/src/server/pages/styles.d.ts +2 -0
- package/dist/src/server/pages/styles.js +2 -0
- package/dist/src/server/webhook/index.d.ts +1 -0
- package/dist/src/server/webhook/index.js +64 -0
- package/dist/src/server/webhook/signature.d.ts +7 -0
- package/dist/src/server/webhook/signature.js +41 -0
- package/dist/src/services/credentials.d.ts +19 -0
- package/dist/src/services/credentials.js +146 -0
- package/dist/src/services/dashboard.d.ts +165 -0
- package/dist/src/services/dashboard.js +200 -0
- package/dist/src/services/drift.d.ts +13 -0
- package/dist/src/services/drift.js +76 -0
- package/dist/src/services/jobs.d.ts +34 -0
- package/dist/src/services/jobs.js +222 -0
- package/dist/src/services/remote_review.d.ts +10 -0
- package/dist/src/services/remote_review.js +274 -0
- package/dist/src/services/remote_review_abort.d.ts +3 -0
- package/dist/src/services/remote_review_abort.js +11 -0
- package/dist/src/services/remote_token_jobs.d.ts +1 -0
- package/dist/src/services/remote_token_jobs.js +20 -0
- package/dist/src/services/remote_tokens.d.ts +13 -0
- package/dist/src/services/remote_tokens.js +57 -0
- package/dist/src/services/replies.d.ts +7 -0
- package/dist/src/services/replies.js +248 -0
- package/dist/src/services/review.d.ts +35 -0
- package/dist/src/services/review.js +671 -0
- package/dist/src/services/setup.d.ts +29 -0
- package/dist/src/services/setup.js +364 -0
- package/dist/src/services/webhook.d.ts +22 -0
- package/dist/src/services/webhook.js +401 -0
- package/dist/src/store/app_db.d.ts +10 -0
- package/dist/src/store/app_db.js +85 -0
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +63 -0
- package/dist/src/store/deliveries.d.ts +12 -0
- package/dist/src/store/deliveries.js +23 -0
- package/dist/src/store/drift.d.ts +4 -0
- package/dist/src/store/drift.js +22 -0
- package/dist/src/store/findings.d.ts +31 -0
- package/dist/src/store/findings.js +64 -0
- package/dist/src/store/installations.d.ts +9 -0
- package/dist/src/store/installations.js +35 -0
- package/dist/src/store/job_logs.d.ts +5 -0
- package/dist/src/store/job_logs.js +17 -0
- package/dist/src/store/jobs.d.ts +27 -0
- package/dist/src/store/jobs.js +86 -0
- package/dist/src/store/migrations.d.ts +5 -0
- package/dist/src/store/migrations.js +258 -0
- package/dist/src/store/remote_review_inputs.d.ts +11 -0
- package/dist/src/store/remote_review_inputs.js +25 -0
- package/dist/src/store/remote_tokens.d.ts +10 -0
- package/dist/src/store/remote_tokens.js +45 -0
- package/dist/src/store/replies.d.ts +24 -0
- package/dist/src/store/replies.js +79 -0
- package/dist/src/store/repos.d.ts +11 -0
- package/dist/src/store/repos.js +59 -0
- package/dist/src/store/reviews.d.ts +86 -0
- package/dist/src/store/reviews.js +220 -0
- package/dist/src/store/rows.d.ts +179 -0
- package/dist/src/store/rows.js +4 -0
- package/dist/src/store/sessions.d.ts +8 -0
- package/dist/src/store/sessions.js +34 -0
- package/dist/src/store/skill_state.d.ts +3 -0
- package/dist/src/store/skill_state.js +24 -0
- package/dist/src/store/sqlite.d.ts +39 -0
- package/dist/src/store/sqlite.js +77 -0
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/subjects.js +78 -0
- package/dist/src/tools/codegraph.d.ts +58 -0
- package/dist/src/tools/codegraph.js +165 -0
- package/dist/src/tools/codegraph_exec.d.ts +13 -0
- package/dist/src/tools/codegraph_exec.js +66 -0
- package/dist/src/types.d.ts +89 -0
- package/dist/src/types.js +1 -0
- package/dist/src/util/atomic.d.ts +4 -0
- package/dist/src/util/atomic.js +78 -0
- package/dist/src/util/keyed_lock.d.ts +3 -0
- package/dist/src/util/keyed_lock.js +33 -0
- package/dist/src/util/log.d.ts +11 -0
- package/dist/src/util/log.js +42 -0
- package/dist/src/util/redact.d.ts +5 -0
- package/dist/src/util/redact.js +30 -0
- package/dist/src/util/runtime.d.ts +90 -0
- package/dist/src/util/runtime.js +206 -0
- package/dist/src/util/time.d.ts +9 -0
- package/dist/src/util/time.js +36 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +5 -0
- package/package.json +54 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/** Webhook dispatch: installation bookkeeping and the skip/enqueue rules
|
|
2
|
+
* for pull_request events. HTTP parsing and HMAC live in server/webhook/. */
|
|
3
|
+
import { enqueue } from "./jobs.js";
|
|
4
|
+
import { deactivateRepo, deactivateReposForInstallation, getRepo, setInstallationId, } from "../store/repos.js";
|
|
5
|
+
import { findFindingByPostedComment } from "../store/findings.js";
|
|
6
|
+
import { createReplyRequestAndJob, findReplyByPostedComment, } from "../store/replies.js";
|
|
7
|
+
import { latestPostedReview } from "../store/reviews.js";
|
|
8
|
+
import { markInstallationRemoved, markInstallationSuspended, upsertInstallation, } from "../store/installations.js";
|
|
9
|
+
export const REVIEW_DEBOUNCE_MS = 20_000;
|
|
10
|
+
const PR_ACTIONS = new Set([
|
|
11
|
+
"opened",
|
|
12
|
+
"reopened",
|
|
13
|
+
"synchronize",
|
|
14
|
+
"ready_for_review",
|
|
15
|
+
]);
|
|
16
|
+
function asRecord(value) {
|
|
17
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function asString(value) {
|
|
23
|
+
return typeof value === "string" ? value : undefined;
|
|
24
|
+
}
|
|
25
|
+
function asNumber(value) {
|
|
26
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
27
|
+
? value
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
function asBool(value) {
|
|
31
|
+
return value === true;
|
|
32
|
+
}
|
|
33
|
+
function appSlug(payload) {
|
|
34
|
+
return asString(asRecord(payload.installation)?.app_slug);
|
|
35
|
+
}
|
|
36
|
+
function hasAppMention(body, slug) {
|
|
37
|
+
const candidates = [slug, "co-maintainer"].filter((value) => Boolean(value));
|
|
38
|
+
return candidates.some((candidate) => {
|
|
39
|
+
const escaped = candidate.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40
|
+
const suffix = candidate === "co-maintainer" ? "(?:-[a-z0-9-]+)?" : "";
|
|
41
|
+
return new RegExp(`(^|[^\\w-])@${escaped}${suffix}(?![\\w-])`, "i").test(body);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function isBot(user) {
|
|
45
|
+
if (!user)
|
|
46
|
+
return false;
|
|
47
|
+
if (asString(user.type) === "Bot")
|
|
48
|
+
return true;
|
|
49
|
+
const login = asString(user.login) ?? "";
|
|
50
|
+
return login.endsWith("[bot]");
|
|
51
|
+
}
|
|
52
|
+
export function maybeEnqueueReview(input) {
|
|
53
|
+
const row = getRepo(input.repo);
|
|
54
|
+
if (!row || row.active !== 1) {
|
|
55
|
+
return {
|
|
56
|
+
outcome: "skipped",
|
|
57
|
+
reason: "repo-not-active",
|
|
58
|
+
repo: input.repo,
|
|
59
|
+
prNumber: input.prNumber,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (row.auto_review !== 1) {
|
|
63
|
+
return {
|
|
64
|
+
outcome: "skipped",
|
|
65
|
+
reason: "auto-review-off",
|
|
66
|
+
repo: input.repo,
|
|
67
|
+
prNumber: input.prNumber,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (row.skip_drafts === 1 && input.draft) {
|
|
71
|
+
return {
|
|
72
|
+
outcome: "skipped",
|
|
73
|
+
reason: "draft",
|
|
74
|
+
repo: input.repo,
|
|
75
|
+
prNumber: input.prNumber,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (row.skip_bots === 1 && input.bot) {
|
|
79
|
+
return {
|
|
80
|
+
outcome: "skipped",
|
|
81
|
+
reason: "bot-author",
|
|
82
|
+
repo: input.repo,
|
|
83
|
+
prNumber: input.prNumber,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (!row.knowledge_built_at) {
|
|
87
|
+
return {
|
|
88
|
+
outcome: "skipped",
|
|
89
|
+
reason: "no-knowledge-yet",
|
|
90
|
+
repo: input.repo,
|
|
91
|
+
prNumber: input.prNumber,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (input.changedFiles === 0) {
|
|
95
|
+
return {
|
|
96
|
+
outcome: "skipped",
|
|
97
|
+
reason: "no-code-changes",
|
|
98
|
+
repo: input.repo,
|
|
99
|
+
prNumber: input.prNumber,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (input.maxDiffLines !== undefined &&
|
|
103
|
+
input.additions + input.deletions > input.maxDiffLines) {
|
|
104
|
+
return {
|
|
105
|
+
outcome: "skipped",
|
|
106
|
+
reason: "diff-too-large",
|
|
107
|
+
repo: input.repo,
|
|
108
|
+
prNumber: input.prNumber,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if ((input.trigger === "review" || input.trigger === "review-comment") &&
|
|
112
|
+
input.headSha) {
|
|
113
|
+
const last = latestPostedReview(input.repo, input.prNumber);
|
|
114
|
+
if (last && last.head_sha === input.headSha) {
|
|
115
|
+
return {
|
|
116
|
+
outcome: "skipped",
|
|
117
|
+
reason: "nothing-new-since-last-round",
|
|
118
|
+
repo: input.repo,
|
|
119
|
+
prNumber: input.prNumber,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const last = latestPostedReview(input.repo, input.prNumber);
|
|
124
|
+
const incremental = row.review_scope === "incremental" && Boolean(last?.head_sha);
|
|
125
|
+
const { id } = enqueue({
|
|
126
|
+
type: "review",
|
|
127
|
+
repo: input.repo,
|
|
128
|
+
prNumber: input.prNumber,
|
|
129
|
+
deliveryId: input.deliveryId,
|
|
130
|
+
debounceMs: REVIEW_DEBOUNCE_MS,
|
|
131
|
+
args: {
|
|
132
|
+
trigger: input.trigger,
|
|
133
|
+
scope: incremental ? "incremental" : "whole-pr",
|
|
134
|
+
sinceCommit: incremental ? last.head_sha : undefined,
|
|
135
|
+
round: last ? last.round + 1 : 1,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
outcome: "enqueued",
|
|
140
|
+
repo: input.repo,
|
|
141
|
+
prNumber: input.prNumber,
|
|
142
|
+
jobId: id,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function applyInstallation(payload) {
|
|
146
|
+
const installation = asRecord(payload.installation);
|
|
147
|
+
const id = asNumber(installation?.id);
|
|
148
|
+
if (id === undefined)
|
|
149
|
+
return { outcome: "ignored" };
|
|
150
|
+
const action = asString(payload.action);
|
|
151
|
+
const account = asRecord(installation?.account);
|
|
152
|
+
if (action === "created") {
|
|
153
|
+
upsertInstallation({
|
|
154
|
+
id,
|
|
155
|
+
account_login: asString(account?.login) ?? "",
|
|
156
|
+
account_type: asString(account?.type) ?? "User",
|
|
157
|
+
permissions: installation?.permissions ?? {},
|
|
158
|
+
events: Array.isArray(installation?.events)
|
|
159
|
+
? installation.events.map(String)
|
|
160
|
+
: [],
|
|
161
|
+
});
|
|
162
|
+
return { outcome: "ignored", reason: "installation-created" };
|
|
163
|
+
}
|
|
164
|
+
if (action === "deleted") {
|
|
165
|
+
markInstallationRemoved(id);
|
|
166
|
+
deactivateReposForInstallation(id);
|
|
167
|
+
return { outcome: "ignored", reason: "installation-deleted" };
|
|
168
|
+
}
|
|
169
|
+
if (action === "suspend") {
|
|
170
|
+
markInstallationSuspended(id, true);
|
|
171
|
+
return { outcome: "ignored", reason: "installation-suspended" };
|
|
172
|
+
}
|
|
173
|
+
if (action === "unsuspend") {
|
|
174
|
+
markInstallationSuspended(id, false);
|
|
175
|
+
return { outcome: "ignored", reason: "installation-unsuspended" };
|
|
176
|
+
}
|
|
177
|
+
return { outcome: "ignored" };
|
|
178
|
+
}
|
|
179
|
+
function applyInstallationRepositories(payload) {
|
|
180
|
+
const action = asString(payload.action);
|
|
181
|
+
const installationId = asNumber(asRecord(payload.installation)?.id);
|
|
182
|
+
if (action === "added" && installationId !== undefined) {
|
|
183
|
+
const added = payload.repositories_added;
|
|
184
|
+
if (Array.isArray(added)) {
|
|
185
|
+
for (const item of added) {
|
|
186
|
+
const fullName = asString(asRecord(item)?.full_name);
|
|
187
|
+
if (fullName)
|
|
188
|
+
setInstallationId(fullName, installationId);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { outcome: "ignored", reason: "repositories-added" };
|
|
192
|
+
}
|
|
193
|
+
if (action === "removed") {
|
|
194
|
+
const removed = payload.repositories_removed;
|
|
195
|
+
if (Array.isArray(removed)) {
|
|
196
|
+
for (const item of removed) {
|
|
197
|
+
const fullName = asString(asRecord(item)?.full_name);
|
|
198
|
+
if (fullName)
|
|
199
|
+
deactivateRepo(fullName);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { outcome: "ignored", reason: "repositories-removed" };
|
|
203
|
+
}
|
|
204
|
+
return { outcome: "ignored", reason: "repositories-added" };
|
|
205
|
+
}
|
|
206
|
+
function pullRequestEvent(payload, deliveryId, maxDiffLines) {
|
|
207
|
+
const action = asString(payload.action) ?? "";
|
|
208
|
+
const pr = asRecord(payload.pull_request);
|
|
209
|
+
const repo = asString(asRecord(payload.repository)?.full_name);
|
|
210
|
+
const prNumber = asNumber(pr?.number) ?? asNumber(payload.number);
|
|
211
|
+
if (!PR_ACTIONS.has(action) || !repo || prNumber === undefined) {
|
|
212
|
+
return { outcome: "ignored", repo, prNumber };
|
|
213
|
+
}
|
|
214
|
+
const user = asRecord(pr?.user);
|
|
215
|
+
const installationId = asNumber(asRecord(payload.installation)?.id);
|
|
216
|
+
if (installationId !== undefined)
|
|
217
|
+
setInstallationId(repo, installationId);
|
|
218
|
+
return maybeEnqueueReview({
|
|
219
|
+
repo,
|
|
220
|
+
prNumber,
|
|
221
|
+
draft: asBool(pr?.draft),
|
|
222
|
+
bot: isBot(user),
|
|
223
|
+
additions: asNumber(pr?.additions) ?? 0,
|
|
224
|
+
deletions: asNumber(pr?.deletions) ?? 0,
|
|
225
|
+
changedFiles: asNumber(pr?.changed_files) ?? -1,
|
|
226
|
+
deliveryId,
|
|
227
|
+
trigger: action,
|
|
228
|
+
headSha: asString(asRecord(pr?.head)?.sha),
|
|
229
|
+
maxDiffLines,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function reviewWakeEvent(event, payload, deliveryId, maxDiffLines) {
|
|
233
|
+
const action = asString(payload.action) ?? "";
|
|
234
|
+
const allowed = event === "pull_request_review"
|
|
235
|
+
? action === "submitted"
|
|
236
|
+
: action === "created";
|
|
237
|
+
const pr = asRecord(payload.pull_request);
|
|
238
|
+
const repo = asString(asRecord(payload.repository)?.full_name);
|
|
239
|
+
const prNumber = asNumber(pr?.number);
|
|
240
|
+
if (!allowed || !repo || prNumber === undefined) {
|
|
241
|
+
return { outcome: "ignored", repo, prNumber };
|
|
242
|
+
}
|
|
243
|
+
const actor = asRecord(event === "pull_request_review"
|
|
244
|
+
? asRecord(payload.review)?.user
|
|
245
|
+
: asRecord(payload.comment)?.user);
|
|
246
|
+
if (isBot(actor)) {
|
|
247
|
+
return {
|
|
248
|
+
outcome: "skipped",
|
|
249
|
+
reason: "own-comment",
|
|
250
|
+
repo,
|
|
251
|
+
prNumber,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const installationId = asNumber(asRecord(payload.installation)?.id);
|
|
255
|
+
if (installationId !== undefined)
|
|
256
|
+
setInstallationId(repo, installationId);
|
|
257
|
+
return maybeEnqueueReview({
|
|
258
|
+
repo,
|
|
259
|
+
prNumber,
|
|
260
|
+
draft: asBool(pr?.draft),
|
|
261
|
+
bot: false,
|
|
262
|
+
additions: asNumber(pr?.additions) ?? 0,
|
|
263
|
+
deletions: asNumber(pr?.deletions) ?? 0,
|
|
264
|
+
changedFiles: asNumber(pr?.changed_files) ?? -1,
|
|
265
|
+
deliveryId,
|
|
266
|
+
trigger: event === "pull_request_review" ? "review" : "review-comment",
|
|
267
|
+
headSha: asString(asRecord(pr?.head)?.sha),
|
|
268
|
+
maxDiffLines,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function replyTarget(repo, prNumber, parentCommentId) {
|
|
272
|
+
if (findFindingByPostedComment(repo, prNumber, parentCommentId)) {
|
|
273
|
+
return parentCommentId;
|
|
274
|
+
}
|
|
275
|
+
return (findReplyByPostedComment(repo, prNumber, parentCommentId)
|
|
276
|
+
?.target_comment_id ?? undefined);
|
|
277
|
+
}
|
|
278
|
+
function enqueueReply(payload, deliveryId, sourceKind, sourceCommentId, repo, prNumber, sourceBody, sourceAuthor, targetCommentId, sourcePath, sourceLine, sourceCommitId) {
|
|
279
|
+
const installationId = asNumber(asRecord(payload.installation)?.id);
|
|
280
|
+
if (installationId !== undefined)
|
|
281
|
+
setInstallationId(repo, installationId);
|
|
282
|
+
const row = getRepo(repo);
|
|
283
|
+
if (!row || row.active !== 1) {
|
|
284
|
+
return {
|
|
285
|
+
outcome: "skipped",
|
|
286
|
+
reason: "repo-not-active",
|
|
287
|
+
repo,
|
|
288
|
+
prNumber,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const result = createReplyRequestAndJob({
|
|
292
|
+
repo,
|
|
293
|
+
prNumber,
|
|
294
|
+
sourceKind,
|
|
295
|
+
sourceCommentId,
|
|
296
|
+
targetCommentId,
|
|
297
|
+
sourceBody: sourceBody.slice(0, 20_000),
|
|
298
|
+
sourceAuthor,
|
|
299
|
+
sourcePath,
|
|
300
|
+
sourceLine,
|
|
301
|
+
sourceCommitId,
|
|
302
|
+
deliveryId,
|
|
303
|
+
});
|
|
304
|
+
if (!result.created) {
|
|
305
|
+
return {
|
|
306
|
+
outcome: "skipped",
|
|
307
|
+
reason: "duplicate-reply",
|
|
308
|
+
repo,
|
|
309
|
+
prNumber,
|
|
310
|
+
jobId: result.request.job_id ?? undefined,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
outcome: "enqueued",
|
|
315
|
+
repo,
|
|
316
|
+
prNumber,
|
|
317
|
+
jobId: result.request.job_id ?? undefined,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function reviewCommentEvent(payload, deliveryId, maxDiffLines) {
|
|
321
|
+
const comment = asRecord(payload.comment);
|
|
322
|
+
const repo = asString(asRecord(payload.repository)?.full_name);
|
|
323
|
+
const pr = asRecord(payload.pull_request);
|
|
324
|
+
const prNumber = asNumber(pr?.number);
|
|
325
|
+
const action = asString(payload.action) ?? "";
|
|
326
|
+
const sourceId = comment?.id;
|
|
327
|
+
const parentId = comment?.in_reply_to_id;
|
|
328
|
+
const author = asRecord(comment?.user);
|
|
329
|
+
if (action !== "created" || !repo || prNumber === undefined) {
|
|
330
|
+
return { outcome: "ignored", repo, prNumber };
|
|
331
|
+
}
|
|
332
|
+
if (isBot(author)) {
|
|
333
|
+
return {
|
|
334
|
+
outcome: "skipped",
|
|
335
|
+
reason: "own-comment",
|
|
336
|
+
repo,
|
|
337
|
+
prNumber,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if ((typeof sourceId === "number" || typeof sourceId === "string") &&
|
|
341
|
+
(typeof parentId === "number" || typeof parentId === "string")) {
|
|
342
|
+
const target = replyTarget(repo, prNumber, String(parentId));
|
|
343
|
+
if (target) {
|
|
344
|
+
return enqueueReply(payload, deliveryId, "review_comment", String(sourceId), repo, prNumber, asString(comment?.body) ?? "", asString(author?.login), target, asString(comment?.path), asNumber(comment?.line) ?? asNumber(comment?.original_line), asString(comment?.commit_id));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return reviewWakeEvent("pull_request_review_comment", payload, deliveryId, maxDiffLines);
|
|
348
|
+
}
|
|
349
|
+
function issueCommentEvent(payload, deliveryId) {
|
|
350
|
+
const action = asString(payload.action) ?? "";
|
|
351
|
+
const issue = asRecord(payload.issue);
|
|
352
|
+
const comment = asRecord(payload.comment);
|
|
353
|
+
const repo = asString(asRecord(payload.repository)?.full_name);
|
|
354
|
+
const prNumber = asNumber(issue?.number);
|
|
355
|
+
const sourceId = comment?.id;
|
|
356
|
+
const author = asRecord(comment?.user);
|
|
357
|
+
const body = asString(comment?.body) ?? "";
|
|
358
|
+
if (action !== "created" ||
|
|
359
|
+
!repo ||
|
|
360
|
+
prNumber === undefined ||
|
|
361
|
+
!asRecord(issue?.pull_request) ||
|
|
362
|
+
(typeof sourceId !== "number" && typeof sourceId !== "string")) {
|
|
363
|
+
return { outcome: "ignored", repo, prNumber };
|
|
364
|
+
}
|
|
365
|
+
if (isBot(author)) {
|
|
366
|
+
return {
|
|
367
|
+
outcome: "skipped",
|
|
368
|
+
reason: "own-comment",
|
|
369
|
+
repo,
|
|
370
|
+
prNumber,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (!hasAppMention(body, appSlug(payload))) {
|
|
374
|
+
return {
|
|
375
|
+
outcome: "ignored",
|
|
376
|
+
reason: "no-app-mention",
|
|
377
|
+
repo,
|
|
378
|
+
prNumber,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return enqueueReply(payload, deliveryId, "issue_comment", String(sourceId), repo, prNumber, body, asString(author?.login));
|
|
382
|
+
}
|
|
383
|
+
export function dispatchGithubEvent(event, payload, deliveryId, maxDiffLines) {
|
|
384
|
+
if (event === "installation")
|
|
385
|
+
return applyInstallation(payload);
|
|
386
|
+
if (event === "installation_repositories") {
|
|
387
|
+
return applyInstallationRepositories(payload);
|
|
388
|
+
}
|
|
389
|
+
if (event === "pull_request") {
|
|
390
|
+
return pullRequestEvent(payload, deliveryId, maxDiffLines);
|
|
391
|
+
}
|
|
392
|
+
if (event === "pull_request_review_comment") {
|
|
393
|
+
return reviewCommentEvent(payload, deliveryId, maxDiffLines);
|
|
394
|
+
}
|
|
395
|
+
if (event === "pull_request_review") {
|
|
396
|
+
return reviewWakeEvent(event, payload, deliveryId, maxDiffLines);
|
|
397
|
+
}
|
|
398
|
+
if (event === "issue_comment")
|
|
399
|
+
return issueCommentEvent(payload, deliveryId);
|
|
400
|
+
return { outcome: "ignored" };
|
|
401
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Database } from "./sqlite.ts";
|
|
2
|
+
export declare function appDbDir(): string;
|
|
3
|
+
/** `CM_APP_DB` overrides the path — tests point it at a temp file. */
|
|
4
|
+
export declare function appDbPath(): string;
|
|
5
|
+
export declare function releaseLock(): Promise<void>;
|
|
6
|
+
/** Opens (creating and migrating if needed) the single shared `app.db`
|
|
7
|
+
* connection every store module reuses. Call once at `serve` startup. */
|
|
8
|
+
export declare function openAppDb(): Promise<Database>;
|
|
9
|
+
export declare function getAppDb(): Database;
|
|
10
|
+
export declare function closeAppDb(): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Database } from "./sqlite.js";
|
|
2
|
+
import { getCacheDir } from "../config.js";
|
|
3
|
+
import { migrations } from "./migrations.js";
|
|
4
|
+
import { getEnv, isNotFound, isProcessAlive, mkdir, readTextFile, remove, writeTextFile, } from "../util/runtime.js";
|
|
5
|
+
export function appDbDir() {
|
|
6
|
+
return `${getCacheDir()}/co-maintainer`;
|
|
7
|
+
}
|
|
8
|
+
/** `CM_APP_DB` overrides the path — tests point it at a temp file. */
|
|
9
|
+
export function appDbPath() {
|
|
10
|
+
return getEnv("CM_APP_DB") ?? `${appDbDir()}/app.db`;
|
|
11
|
+
}
|
|
12
|
+
function lockPath() {
|
|
13
|
+
return `${appDbPath()}.lock`;
|
|
14
|
+
}
|
|
15
|
+
/** A PID file next to the database is the lock: a live PID inside it
|
|
16
|
+
* refuses a second start, a dead one is taken over silently. */
|
|
17
|
+
async function acquireLock() {
|
|
18
|
+
const path = lockPath();
|
|
19
|
+
let existingPid;
|
|
20
|
+
try {
|
|
21
|
+
existingPid = Number((await readTextFile(path)).trim());
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (!isNotFound(error))
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
if (existingPid !== undefined &&
|
|
28
|
+
Number.isInteger(existingPid) &&
|
|
29
|
+
isProcessAlive(existingPid)) {
|
|
30
|
+
throw new Error(`co-maintainer serve is already running (pid ${existingPid}) against this app.db. Only one serve process may write to it at a time.`);
|
|
31
|
+
}
|
|
32
|
+
await writeTextFile(path, String(process.pid));
|
|
33
|
+
}
|
|
34
|
+
export async function releaseLock() {
|
|
35
|
+
await remove(lockPath()).catch(() => { });
|
|
36
|
+
}
|
|
37
|
+
/** A database already at a migration this binary does not know about means
|
|
38
|
+
* an older release started against one a newer release already touched —
|
|
39
|
+
* refuse loudly instead of running unfamiliar structure through old code. */
|
|
40
|
+
function migrate(database) {
|
|
41
|
+
const current = database.prepare("PRAGMA user_version").get()
|
|
42
|
+
?.user_version ?? 0;
|
|
43
|
+
if (current > migrations.length) {
|
|
44
|
+
throw new Error(`app.db is at migration ${current}, but this build only knows ${migrations.length}. ` +
|
|
45
|
+
`Installing an older release after a newer one already migrated the database is unsupported.`);
|
|
46
|
+
}
|
|
47
|
+
for (let index = current; index < migrations.length; index++) {
|
|
48
|
+
database.exec("BEGIN");
|
|
49
|
+
try {
|
|
50
|
+
for (const statement of migrations[index])
|
|
51
|
+
database.exec(statement);
|
|
52
|
+
database.exec(`PRAGMA user_version = ${index + 1}`);
|
|
53
|
+
database.exec("COMMIT");
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
database.exec("ROLLBACK");
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
let db;
|
|
62
|
+
/** Opens (creating and migrating if needed) the single shared `app.db`
|
|
63
|
+
* connection every store module reuses. Call once at `serve` startup. */
|
|
64
|
+
export async function openAppDb() {
|
|
65
|
+
if (db)
|
|
66
|
+
return db;
|
|
67
|
+
await mkdir(appDbDir(), { recursive: true });
|
|
68
|
+
await acquireLock();
|
|
69
|
+
const database = new Database(appDbPath());
|
|
70
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
71
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
72
|
+
migrate(database);
|
|
73
|
+
db = database;
|
|
74
|
+
return database;
|
|
75
|
+
}
|
|
76
|
+
export function getAppDb() {
|
|
77
|
+
if (!db)
|
|
78
|
+
throw new Error("app.db is not open; call openAppDb() first");
|
|
79
|
+
return db;
|
|
80
|
+
}
|
|
81
|
+
export async function closeAppDb() {
|
|
82
|
+
db?.close();
|
|
83
|
+
db = undefined;
|
|
84
|
+
await releaseLock();
|
|
85
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function cacheGet(namespace: string, key: string): Promise<string | undefined>;
|
|
2
|
+
export declare function cacheSet(namespace: string, key: string, value: string): Promise<void>;
|
|
3
|
+
export declare function cacheDelete(namespace: string, key: string): Promise<void>;
|
|
4
|
+
export declare function cacheDeletePrefix(namespace: string, prefix: string): Promise<void>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Database } from "./sqlite.js";
|
|
2
|
+
import { cacheDbPath, getCacheDir } from "../config.js";
|
|
3
|
+
import { mkdirSync } from "../util/runtime.js";
|
|
4
|
+
let initialized = false;
|
|
5
|
+
function openDatabase() {
|
|
6
|
+
mkdirSync(`${getCacheDir()}/co-maintainer`, { recursive: true });
|
|
7
|
+
const db = new Database(cacheDbPath());
|
|
8
|
+
if (!initialized) {
|
|
9
|
+
db.exec(`
|
|
10
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
11
|
+
namespace TEXT NOT NULL,
|
|
12
|
+
cache_key TEXT NOT NULL,
|
|
13
|
+
value TEXT NOT NULL,
|
|
14
|
+
updated_at TEXT NOT NULL,
|
|
15
|
+
PRIMARY KEY (namespace, cache_key)
|
|
16
|
+
)
|
|
17
|
+
`);
|
|
18
|
+
initialized = true;
|
|
19
|
+
}
|
|
20
|
+
return db;
|
|
21
|
+
}
|
|
22
|
+
export async function cacheGet(namespace, key) {
|
|
23
|
+
const db = openDatabase();
|
|
24
|
+
try {
|
|
25
|
+
const row = db
|
|
26
|
+
.prepare("SELECT value FROM cache WHERE namespace = ? AND cache_key = ?")
|
|
27
|
+
.get(namespace, key);
|
|
28
|
+
return row?.value;
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
db.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function cacheSet(namespace, key, value) {
|
|
35
|
+
const db = openDatabase();
|
|
36
|
+
try {
|
|
37
|
+
db.prepare(`INSERT INTO cache(namespace, cache_key, value, updated_at)
|
|
38
|
+
VALUES (?, ?, ?, ?)
|
|
39
|
+
ON CONFLICT(namespace, cache_key) DO UPDATE SET
|
|
40
|
+
value = excluded.value, updated_at = excluded.updated_at`).run(namespace, key, value, new Date().toISOString());
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
db.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function cacheDelete(namespace, key) {
|
|
47
|
+
const db = openDatabase();
|
|
48
|
+
try {
|
|
49
|
+
db.prepare("DELETE FROM cache WHERE namespace = ? AND cache_key = ?").run(namespace, key);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
db.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function cacheDeletePrefix(namespace, prefix) {
|
|
56
|
+
const db = openDatabase();
|
|
57
|
+
try {
|
|
58
|
+
db.prepare("DELETE FROM cache WHERE namespace = ? AND cache_key LIKE ?").run(namespace, `${prefix}%`);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
db.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DeliveryRow } from "./rows.ts";
|
|
2
|
+
export declare function recordDelivery(row: {
|
|
3
|
+
deliveryId: string;
|
|
4
|
+
event: string;
|
|
5
|
+
action?: string;
|
|
6
|
+
repo?: string;
|
|
7
|
+
prNumber?: number;
|
|
8
|
+
outcome: string;
|
|
9
|
+
reason?: string;
|
|
10
|
+
}): void;
|
|
11
|
+
export declare function hasDelivery(deliveryId: string): boolean;
|
|
12
|
+
export declare function listSkipped(repo?: string): DeliveryRow[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Every webhook delivery is recorded here, whether it was acted on or not,
|
|
2
|
+
* so a skipped pull request stays visible instead of silent. */
|
|
3
|
+
import { getAppDb } from "./app_db.js";
|
|
4
|
+
import { nowIso } from "../util/time.js";
|
|
5
|
+
export function recordDelivery(row) {
|
|
6
|
+
getAppDb()
|
|
7
|
+
.prepare(`INSERT INTO deliveries
|
|
8
|
+
(delivery_id, event, action, repo, pr_number, received_at, outcome, reason)
|
|
9
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
10
|
+
.run(row.deliveryId, row.event, row.action ?? null, row.repo ?? null, row.prNumber ?? null, nowIso(), row.outcome, row.reason ?? null);
|
|
11
|
+
}
|
|
12
|
+
export function hasDelivery(deliveryId) {
|
|
13
|
+
return (getAppDb()
|
|
14
|
+
.prepare(`SELECT 1 FROM deliveries WHERE delivery_id = ?`)
|
|
15
|
+
.get(deliveryId) !== undefined);
|
|
16
|
+
}
|
|
17
|
+
export function listSkipped(repo) {
|
|
18
|
+
const where = repo
|
|
19
|
+
? `WHERE outcome != 'enqueued' AND repo = ?`
|
|
20
|
+
: `WHERE outcome != 'enqueued'`;
|
|
21
|
+
const db = getAppDb().prepare(`SELECT * FROM deliveries ${where} ORDER BY received_at DESC`);
|
|
22
|
+
return repo ? db.all(repo) : db.all();
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getAppDb } from "./app_db.js";
|
|
2
|
+
export function upsertDrift(row) {
|
|
3
|
+
getAppDb()
|
|
4
|
+
.prepare(`INSERT INTO drift
|
|
5
|
+
(repo, as_of, prs_since, prs_updated, commits_since, files_changed)
|
|
6
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
7
|
+
ON CONFLICT(repo) DO UPDATE SET
|
|
8
|
+
as_of = excluded.as_of,
|
|
9
|
+
prs_since = excluded.prs_since,
|
|
10
|
+
prs_updated = excluded.prs_updated,
|
|
11
|
+
commits_since = excluded.commits_since,
|
|
12
|
+
files_changed = excluded.files_changed`)
|
|
13
|
+
.run(row.repo, row.as_of, row.prs_since, row.prs_updated, row.commits_since, row.files_changed);
|
|
14
|
+
}
|
|
15
|
+
export function deleteDrift(repo) {
|
|
16
|
+
getAppDb().prepare(`DELETE FROM drift WHERE repo = ?`).run(repo);
|
|
17
|
+
}
|
|
18
|
+
export function getDrift(repo) {
|
|
19
|
+
return getAppDb()
|
|
20
|
+
.prepare(`SELECT * FROM drift WHERE repo = ?`)
|
|
21
|
+
.get(repo);
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FindingRow } from "./rows.ts";
|
|
2
|
+
export declare function insertFinding(row: {
|
|
3
|
+
id: string;
|
|
4
|
+
reviewId: string;
|
|
5
|
+
severity: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
lineFrom?: number;
|
|
8
|
+
lineTo?: number;
|
|
9
|
+
title: string;
|
|
10
|
+
bodyMd: string;
|
|
11
|
+
firstSeenReviewId?: string;
|
|
12
|
+
threadCommentId?: string;
|
|
13
|
+
anchorText?: string | null;
|
|
14
|
+
state?: string;
|
|
15
|
+
carriedFromFindingId?: string | null;
|
|
16
|
+
closeReason?: string | null;
|
|
17
|
+
}): void;
|
|
18
|
+
export declare function listCarryableFindingsForReview(reviewId: string): FindingRow[];
|
|
19
|
+
export declare function setFindingPosted(id: string, postedCommentId: string, threadCommentId?: string): void;
|
|
20
|
+
export declare function listFindingsForReview(reviewId: string): FindingRow[];
|
|
21
|
+
export declare function listFindingsForPr(repo: string, prNumber: number): FindingRow[];
|
|
22
|
+
export declare function findFindingByPostedComment(repo: string, prNumber: number, commentId: string): FindingRow | undefined;
|
|
23
|
+
export type SeverityStats = {
|
|
24
|
+
severity: string;
|
|
25
|
+
findings: number;
|
|
26
|
+
repeats: number;
|
|
27
|
+
posted: number;
|
|
28
|
+
};
|
|
29
|
+
/** A repeat is a finding the previous round already raised, so the repeat
|
|
30
|
+
* column reads as "raised again after the author saw it". */
|
|
31
|
+
export declare function findingStatsBySeverity(sinceIso: string): SeverityStats[];
|