@todoist/doistbot-cli 0.0.0-stage → 1.0.16
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/dist/actions/auth.js +225 -0
- package/dist/actions/doctor.js +39 -0
- package/dist/actions/review-usage.js +25 -0
- package/dist/actions/review.js +311 -0
- package/dist/actions/setup.js +14 -0
- package/dist/auth.js +202 -0
- package/dist/config.js +145 -0
- package/dist/env.js +10 -0
- package/dist/file-store.js +112 -0
- package/dist/index.js +164 -0
- package/dist/input.js +40 -0
- package/dist/runtime.js +105 -0
- package/dist/secure-store.js +148 -0
- package/dist/terminal.js +185 -0
- package/dist/test-utils/secure-store-mocks.js +29 -0
- package/dist/update-cache-entrypoint.js +16 -0
- package/dist/update-check.js +235 -0
- package/package.json +65 -5
- package/sandbox/_node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/_node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/_node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/_node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/dist/core/autofix-resources.js +58 -0
- package/sandbox/dist/core/autofix-service-query.js +69 -0
- package/sandbox/dist/core/autofix-test-policy.js +11 -0
- package/sandbox/dist/core/autofix-verification.js +355 -0
- package/sandbox/dist/core/bot-logins.js +14 -0
- package/sandbox/dist/core/check-run.js +158 -0
- package/sandbox/dist/core/datadog-metrics.js +113 -0
- package/sandbox/dist/core/environment.js +49 -0
- package/sandbox/dist/core/git-auth.js +17 -0
- package/sandbox/dist/core/github-history.js +4 -0
- package/sandbox/dist/core/github-issues.js +18 -0
- package/sandbox/dist/core/label-parsing.js +1 -0
- package/sandbox/dist/core/langfuse-observation.js +33 -0
- package/sandbox/dist/core/langfuse-session.js +39 -0
- package/sandbox/dist/core/logger-span-processor.js +90 -0
- package/sandbox/dist/core/logging.js +18 -0
- package/sandbox/dist/core/pi-autofix-extension.js +188 -0
- package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
- package/sandbox/dist/core/pi-progress-logging.js +51 -0
- package/sandbox/dist/core/pi-tool-output-filter-extension.js +65 -0
- package/sandbox/dist/core/pi.js +870 -0
- package/sandbox/dist/core/pii-scrubber.js +31 -0
- package/sandbox/dist/core/prompt-template.js +63 -0
- package/sandbox/dist/core/reactions.js +36 -0
- package/sandbox/dist/core/regexp.js +3 -0
- package/sandbox/dist/core/repository-map.js +1 -0
- package/sandbox/dist/core/repository-resolver.js +1 -0
- package/sandbox/dist/core/retry.js +29 -0
- package/sandbox/dist/core/s3-utils.js +42 -0
- package/sandbox/dist/core/shared.js +124 -0
- package/sandbox/dist/core/snapshot-helpers.js +17 -0
- package/sandbox/dist/core/span-test-helpers.js +13 -0
- package/sandbox/dist/core/tool-environment.js +83 -0
- package/sandbox/dist/core/tool-output.js +18 -0
- package/sandbox/dist/core/tracing-setup.js +92 -0
- package/sandbox/dist/core/tracing.js +81 -0
- package/sandbox/dist/main.js +51 -0
- package/sandbox/dist/providers/config.js +23 -0
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +28 -0
- package/sandbox/dist/providers/helpers.js +253 -0
- package/sandbox/dist/providers/index.js +27 -0
- package/sandbox/dist/providers/openai.js +23 -0
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +301 -0
- package/sandbox/dist/providers/process.js +34 -0
- package/sandbox/dist/providers/schemas.js +61 -0
- package/sandbox/dist/providers/types.js +4 -0
- package/sandbox/dist/tasks/chat/chat.js +440 -0
- package/sandbox/dist/tasks/chat/prompt.js +20 -0
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +304 -0
- package/sandbox/dist/tasks/issue-summarize/context.js +21 -0
- package/sandbox/dist/tasks/issue-summarize/media.js +335 -0
- package/sandbox/dist/tasks/issue-summarize/model.js +230 -0
- package/sandbox/dist/tasks/issue-summarize/output.js +45 -0
- package/sandbox/dist/tasks/issue-summarize/prompt.js +11 -0
- package/sandbox/dist/tasks/issue-summarize/summarize.js +238 -0
- package/sandbox/dist/tasks/issue-summarize/thread.js +123 -0
- package/sandbox/dist/tasks/issue-summarize/types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/assignee-review.js +114 -0
- package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
- package/sandbox/dist/tasks/issue-triage/autofix-checkpoint.js +122 -0
- package/sandbox/dist/tasks/issue-triage/autofix-policy.js +36 -0
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +57 -0
- package/sandbox/dist/tasks/issue-triage/diff-validator.js +102 -0
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +617 -0
- package/sandbox/dist/tasks/issue-triage/fix-context.js +5 -0
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +263 -0
- package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +526 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +108 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/recovery.js +512 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +233 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +171 -0
- package/sandbox/dist/tasks/issue-triage/github-request.js +2 -0
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +206 -0
- package/sandbox/dist/tasks/issue-triage/markers.js +6 -0
- package/sandbox/dist/tasks/issue-triage/model.js +124 -0
- package/sandbox/dist/tasks/issue-triage/output.js +164 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +585 -0
- package/sandbox/dist/tasks/issue-triage/pr-readiness.js +25 -0
- package/sandbox/dist/tasks/issue-triage/prompt.js +63 -0
- package/sandbox/dist/tasks/issue-triage/repo-config.js +47 -0
- package/sandbox/dist/tasks/issue-triage/repo-prompts.js +77 -0
- package/sandbox/dist/tasks/issue-triage/report-fixture.js +53 -0
- package/sandbox/dist/tasks/issue-triage/report-history.js +113 -0
- package/sandbox/dist/tasks/issue-triage/report-schema.js +63 -0
- package/sandbox/dist/tasks/issue-triage/report.js +111 -0
- package/sandbox/dist/tasks/issue-triage/resolution.js +30 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +169 -0
- package/sandbox/dist/tasks/issue-triage/sandbox-environment.js +59 -0
- package/sandbox/dist/tasks/issue-triage/shared.js +86 -0
- package/sandbox/dist/tasks/issue-triage/side-effects.js +102 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +442 -0
- package/sandbox/dist/tasks/issue-triage/types.js +1 -0
- package/sandbox/dist/tasks/persist-logs/context.js +19 -0
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +116 -0
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +156 -0
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +491 -0
- package/sandbox/dist/tasks/refresh-s3-urls/refresh-s3-urls.js +214 -0
- package/sandbox/dist/tasks/review/check-run.js +326 -0
- package/sandbox/dist/tasks/review/comment-mapper.js +46 -0
- package/sandbox/dist/tasks/review/conversation-context.js +477 -0
- package/sandbox/dist/tasks/review/diff.js +95 -0
- package/sandbox/dist/tasks/review/engines/dedupe.js +291 -0
- package/sandbox/dist/tasks/review/engines/multi-focus.js +419 -0
- package/sandbox/dist/tasks/review/engines/shared.js +87 -0
- package/sandbox/dist/tasks/review/engines/types.js +1 -0
- package/sandbox/dist/tasks/review/finding-output.js +18 -0
- package/sandbox/dist/tasks/review/format.js +17 -0
- package/sandbox/dist/tasks/review/github-comment-format.js +94 -0
- package/sandbox/dist/tasks/review/local-git.js +374 -0
- package/sandbox/dist/tasks/review/local.js +118 -0
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +59 -0
- package/sandbox/dist/tasks/review/prompt.js +81 -0
- package/sandbox/dist/tasks/review/review-feedback.js +48 -0
- package/sandbox/dist/tasks/review/review-mode-github.js +58 -0
- package/sandbox/dist/tasks/review/review-mode-policy.js +232 -0
- package/sandbox/dist/tasks/review/review-mode-types.js +1 -0
- package/sandbox/dist/tasks/review/review-mode.js +296 -0
- package/sandbox/dist/tasks/review/review-summary.js +123 -0
- package/sandbox/dist/tasks/review/review.js +391 -0
- package/sandbox/dist/tasks/review/runner.js +255 -0
- package/sandbox/dist/tasks/review/severity.js +5 -0
- package/sandbox/dist/tasks/review/summary-model.js +271 -0
- package/sandbox/dist/tasks/review/thinking-level.js +14 -0
- package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
- package/sandbox/dist/tasks/review/types.js +1 -0
- package/sandbox/dist/tasks/review-slice-plan/context.js +66 -0
- package/sandbox/dist/tasks/review-slice-plan/plan.js +210 -0
- package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
- package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +367 -0
- package/sandbox/dist/tasks/sentry-triage/store.js +133 -0
- package/sandbox/dist/tasks/sentry-triage/triage.js +217 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/src/review/prompts/ai-internal-tools.md +254 -0
- package/sandbox/src/review/prompts/backend-repository-standards.md +102 -0
- package/sandbox/src/review/prompts/observability-standard.md +169 -0
- package/sandbox/src/review/prompts/outline-map.json +26 -0
- package/sandbox/src/review/prompts/secrets-management.md +286 -0
- package/sandbox/src/review/prompts/service-production-readiness.md +122 -0
- package/sandbox/src/review/prompts/workload-placement.md +345 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +16 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +32 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +20 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +21 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +19 -0
- package/sandbox/src/tasks/review/prompts/review-mode.md +40 -0
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +142 -0
- package/scripts/restore-runtime-assets.mjs +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
3
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
4
|
+
import { App } from 'octokit';
|
|
5
|
+
import { isKnownDoistbotLogin } from '../../core/bot-logins.js';
|
|
6
|
+
import { recordTaskMetrics, } from '../../core/datadog-metrics.js';
|
|
7
|
+
import { logger } from '../../core/logging.js';
|
|
8
|
+
import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
|
|
9
|
+
import { LOG_SNAPSHOT_MARKER, parseS3MetadataFromComment, PRESIGN_EXPIRY_SECONDS, } from '../../core/snapshot-helpers.js';
|
|
10
|
+
import { shutdownTracing } from '../../core/tracing-setup.js';
|
|
11
|
+
function requiredEnv(name) {
|
|
12
|
+
const value = process.env[name];
|
|
13
|
+
if (!value)
|
|
14
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function parseContext() {
|
|
18
|
+
return {
|
|
19
|
+
owner: requiredEnv('REPO_OWNER'),
|
|
20
|
+
repo: requiredEnv('REPO_NAME'),
|
|
21
|
+
issueNumber: Number(requiredEnv('ISSUE_NUMBER')),
|
|
22
|
+
installationId: Number(requiredEnv('INSTALLATION_ID')),
|
|
23
|
+
appId: requiredEnv('APP_ID'),
|
|
24
|
+
privateKey: requiredEnv('PRIVATE_KEY'),
|
|
25
|
+
logSnapshotBucket: requiredEnv('LOG_SNAPSHOT_BUCKET'),
|
|
26
|
+
commentId: process.env.COMMENT_ID ? Number(process.env.COMMENT_ID) : undefined,
|
|
27
|
+
reactionId: process.env.REACTION_ID ? Number(process.env.REACTION_ID) : undefined,
|
|
28
|
+
deliveryId: process.env.DELIVERY_ID?.trim() || 'unknown',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export async function main() {
|
|
32
|
+
const startedAt = performance.now();
|
|
33
|
+
const ctx = parseContext();
|
|
34
|
+
let exitCode = 0;
|
|
35
|
+
let metricOutcome = 'failure';
|
|
36
|
+
let metricReason;
|
|
37
|
+
logger.setBaseContext({
|
|
38
|
+
deliveryId: ctx.deliveryId,
|
|
39
|
+
repository: `${ctx.owner}/${ctx.repo}`,
|
|
40
|
+
issueNumber: ctx.issueNumber,
|
|
41
|
+
});
|
|
42
|
+
logger.info('Refresh-s3-urls task started', {
|
|
43
|
+
task: 'refresh-s3-urls',
|
|
44
|
+
issueNumber: ctx.issueNumber,
|
|
45
|
+
});
|
|
46
|
+
let octokit;
|
|
47
|
+
let cleanedUp = false;
|
|
48
|
+
async function cleanup() {
|
|
49
|
+
if (cleanedUp)
|
|
50
|
+
return;
|
|
51
|
+
cleanedUp = true;
|
|
52
|
+
if (!octokit)
|
|
53
|
+
return;
|
|
54
|
+
await removeIssueCommentReactionIfPresent({
|
|
55
|
+
octokit,
|
|
56
|
+
owner: ctx.owner,
|
|
57
|
+
repo: ctx.repo,
|
|
58
|
+
commentId: ctx.commentId,
|
|
59
|
+
reactionId: ctx.reactionId,
|
|
60
|
+
warnMessage: 'Failed to remove refresh-s3-urls reaction',
|
|
61
|
+
successMessage: 'Refresh-s3-urls reaction removed',
|
|
62
|
+
logMeta: { task: 'refresh-s3-urls', reactionId: ctx.reactionId },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const app = new App({ appId: ctx.appId, privateKey: ctx.privateKey });
|
|
67
|
+
octokit = await app.getInstallationOctokit(ctx.installationId);
|
|
68
|
+
const snapshotComment = await findSnapshotComment(octokit, ctx);
|
|
69
|
+
if (!snapshotComment) {
|
|
70
|
+
await octokit.rest.issues.createComment({
|
|
71
|
+
owner: ctx.owner,
|
|
72
|
+
repo: ctx.repo,
|
|
73
|
+
issue_number: ctx.issueNumber,
|
|
74
|
+
body: 'No log snapshot found on this issue. Run `@doistbot /persist-logs` first.',
|
|
75
|
+
});
|
|
76
|
+
metricOutcome = 'success';
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
logger.info('Found snapshot comment, parsing S3 metadata', {
|
|
80
|
+
task: 'refresh-s3-urls',
|
|
81
|
+
snapshotCommentId: snapshotComment.id,
|
|
82
|
+
});
|
|
83
|
+
const parsed = parseS3MetadataFromComment(snapshotComment.body ?? '');
|
|
84
|
+
if (!parsed) {
|
|
85
|
+
await octokit.rest.issues.createComment({
|
|
86
|
+
owner: ctx.owner,
|
|
87
|
+
repo: ctx.repo,
|
|
88
|
+
issue_number: ctx.issueNumber,
|
|
89
|
+
body: 'The existing log snapshot does not contain S3 metadata. Please run `@doistbot /persist-logs` again to create a new snapshot with refreshable links.',
|
|
90
|
+
});
|
|
91
|
+
metricOutcome = 'success';
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (parsed.bucket !== ctx.logSnapshotBucket) {
|
|
95
|
+
logger.warn('Snapshot bucket mismatch — refusing to sign', {
|
|
96
|
+
task: 'refresh-s3-urls',
|
|
97
|
+
expected: ctx.logSnapshotBucket,
|
|
98
|
+
found: parsed.bucket,
|
|
99
|
+
});
|
|
100
|
+
await octokit.rest.issues.createComment({
|
|
101
|
+
owner: ctx.owner,
|
|
102
|
+
repo: ctx.repo,
|
|
103
|
+
issue_number: ctx.issueNumber,
|
|
104
|
+
body: 'The log snapshot references an unexpected S3 bucket. Please run `@doistbot /persist-logs` again.',
|
|
105
|
+
});
|
|
106
|
+
metricOutcome = 'success';
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Generate fresh presigned URLs for all S3 keys
|
|
110
|
+
logger.info('Generating fresh presigned URLs', {
|
|
111
|
+
task: 'refresh-s3-urls',
|
|
112
|
+
bucket: parsed.bucket,
|
|
113
|
+
keyCount: parsed.keys.length,
|
|
114
|
+
});
|
|
115
|
+
const s3 = new S3Client({});
|
|
116
|
+
const urlMap = new Map();
|
|
117
|
+
for (const key of parsed.keys) {
|
|
118
|
+
const freshUrl = await getSignedUrl(s3, new GetObjectCommand({ Bucket: parsed.bucket, Key: key }), { expiresIn: PRESIGN_EXPIRY_SECONDS });
|
|
119
|
+
urlMap.set(key, freshUrl);
|
|
120
|
+
}
|
|
121
|
+
// Replace all presigned S3 URLs in the comment body
|
|
122
|
+
const updatedBody = replacePresignedUrls(snapshotComment.body ?? '', urlMap);
|
|
123
|
+
logger.info('Presigned URLs refreshed, updating comment', {
|
|
124
|
+
task: 'refresh-s3-urls',
|
|
125
|
+
urlsReplaced: urlMap.size,
|
|
126
|
+
});
|
|
127
|
+
await octokit.rest.issues.updateComment({
|
|
128
|
+
owner: ctx.owner,
|
|
129
|
+
repo: ctx.repo,
|
|
130
|
+
comment_id: snapshotComment.id,
|
|
131
|
+
body: updatedBody,
|
|
132
|
+
});
|
|
133
|
+
metricOutcome = 'success';
|
|
134
|
+
logger.info('Refresh-s3-urls task completed', {
|
|
135
|
+
task: 'refresh-s3-urls',
|
|
136
|
+
issueNumber: ctx.issueNumber,
|
|
137
|
+
snapshotCommentId: snapshotComment.id,
|
|
138
|
+
durationMs: performance.now() - startedAt,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
metricReason = 'exception';
|
|
143
|
+
logger.error('Refresh-s3-urls task failed', {
|
|
144
|
+
task: 'refresh-s3-urls',
|
|
145
|
+
issueNumber: ctx.issueNumber,
|
|
146
|
+
error: error instanceof Error ? error.message : String(error),
|
|
147
|
+
durationMs: performance.now() - startedAt,
|
|
148
|
+
});
|
|
149
|
+
if (octokit) {
|
|
150
|
+
try {
|
|
151
|
+
await octokit.rest.issues.createComment({
|
|
152
|
+
owner: ctx.owner,
|
|
153
|
+
repo: ctx.repo,
|
|
154
|
+
issue_number: ctx.issueNumber,
|
|
155
|
+
body: 'Sorry, I encountered an error while refreshing the log snapshot URLs. Please try again.',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Best effort
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exitCode = 1;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
// Independent network targets (GitHub vs Datadog) — safe to run concurrently.
|
|
166
|
+
await Promise.all([
|
|
167
|
+
cleanup(),
|
|
168
|
+
recordTaskMetrics({
|
|
169
|
+
type: 'refresh',
|
|
170
|
+
repository: `${ctx.owner}/${ctx.repo}`,
|
|
171
|
+
model: 'none',
|
|
172
|
+
outcome: metricOutcome,
|
|
173
|
+
durationMs: performance.now() - startedAt,
|
|
174
|
+
reason: metricReason,
|
|
175
|
+
}),
|
|
176
|
+
]);
|
|
177
|
+
}
|
|
178
|
+
if (exitCode !== 0) {
|
|
179
|
+
await shutdownTracing();
|
|
180
|
+
process.exit(exitCode);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function findSnapshotComment(octokit, ctx) {
|
|
184
|
+
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
|
|
185
|
+
owner: ctx.owner,
|
|
186
|
+
repo: ctx.repo,
|
|
187
|
+
issue_number: ctx.issueNumber,
|
|
188
|
+
per_page: 100,
|
|
189
|
+
});
|
|
190
|
+
// Return the most recent bot-authored snapshot so /refresh-s3-urls targets the latest one
|
|
191
|
+
let latest = null;
|
|
192
|
+
for (const comment of comments) {
|
|
193
|
+
const login = comment.user?.login;
|
|
194
|
+
if (isKnownDoistbotLogin(login) && comment.body?.includes(LOG_SNAPSHOT_MARKER)) {
|
|
195
|
+
latest = { id: comment.id, body: comment.body };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return latest;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Replaces presigned S3 URLs in a comment body with fresh ones.
|
|
202
|
+
* Matches any URL containing `X-Amz-` query params (the presigned URL signature)
|
|
203
|
+
* and swaps it if the URL path contains one of our known S3 keys.
|
|
204
|
+
*/
|
|
205
|
+
export function replacePresignedUrls(body, urlMap) {
|
|
206
|
+
return body.replace(/https:\/\/[^\s)]+X-Amz-[^\s)]+/g, (existingUrl) => {
|
|
207
|
+
for (const [key, freshUrl] of urlMap) {
|
|
208
|
+
if (existingUrl.includes(encodeURIComponent(key)) || existingUrl.includes(key)) {
|
|
209
|
+
return freshUrl;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return existingUrl;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { isAnotherDeliveryInProgress, postCheckRun } from '../../core/check-run.js';
|
|
2
|
+
import { logger } from '../../core/logging.js';
|
|
3
|
+
import { appendReviewFeedbackLink } from './review-feedback.js';
|
|
4
|
+
import { parseSeverity } from './severity.js';
|
|
5
|
+
const CHECK_RUN_NAME = 'doistbot/review';
|
|
6
|
+
/**
|
|
7
|
+
* Stale-pointer Check Run encoding — mirrors
|
|
8
|
+
* `apps/webhook-handler/src/tasks/review/stale-check-run.ts` so the two paths
|
|
9
|
+
* interoperate: a webhook-handler synchronize that lands after the sandbox
|
|
10
|
+
* posts a pointer here will find this pointer on `payload.before` and unwrap
|
|
11
|
+
* to the same `reviewedSha`. Kept in sync by convention; if we ever extract
|
|
12
|
+
* the upsert mechanics into a shared package, the constants move with it.
|
|
13
|
+
*/
|
|
14
|
+
const STALE_POINTER_EXTERNAL_ID_PREFIX = 'stale-pointer:';
|
|
15
|
+
const STALE_TITLE_PREFIX = 'Stale: ';
|
|
16
|
+
const MAX_TITLE_LENGTH = 100;
|
|
17
|
+
function truncateTitle(value) {
|
|
18
|
+
if (value.length <= MAX_TITLE_LENGTH)
|
|
19
|
+
return value;
|
|
20
|
+
return `${value.slice(0, MAX_TITLE_LENGTH - 1)}…`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Concurrency lock for the review task: returns `true` if a different
|
|
24
|
+
* delivery's `in_progress` Check Run is already running on this SHA. Skips
|
|
25
|
+
* the duplicate review. Replaces the pre-Check-Runs PR-comment-based lock.
|
|
26
|
+
*/
|
|
27
|
+
export function isAnotherReviewInProgress(params) {
|
|
28
|
+
return isAnotherDeliveryInProgress({ ...params, name: CHECK_RUN_NAME });
|
|
29
|
+
}
|
|
30
|
+
// Mapped type (not `Record<Severity, Lowercase<Severity>>`) so each key is
|
|
31
|
+
// tied to its own lowercase value. `{ P0: 'p1', ... }` would type-check
|
|
32
|
+
// under the looser `Record` form; this one rejects it.
|
|
33
|
+
const SEVERITY_TO_COUNTER_KEY = {
|
|
34
|
+
P0: 'p0',
|
|
35
|
+
P1: 'p1',
|
|
36
|
+
P2: 'p2',
|
|
37
|
+
P3: 'p3',
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Counts findings by their `[Pn]` severity tag, looking at both the inline
|
|
41
|
+
* comments and any summary-only findings the bot included. Findings without
|
|
42
|
+
* a recognizable tag are bucketed as `untagged`.
|
|
43
|
+
*/
|
|
44
|
+
export function countFindingsBySeverity(review) {
|
|
45
|
+
const counts = {
|
|
46
|
+
p0: 0,
|
|
47
|
+
p1: 0,
|
|
48
|
+
p2: 0,
|
|
49
|
+
p3: 0,
|
|
50
|
+
untagged: 0,
|
|
51
|
+
total: 0,
|
|
52
|
+
};
|
|
53
|
+
function increment(sev) {
|
|
54
|
+
if (sev)
|
|
55
|
+
counts[SEVERITY_TO_COUNTER_KEY[sev]] += 1;
|
|
56
|
+
else
|
|
57
|
+
counts.untagged += 1;
|
|
58
|
+
counts.total += 1;
|
|
59
|
+
}
|
|
60
|
+
for (const comment of review.comments ?? []) {
|
|
61
|
+
increment(comment.body ? parseSeverity(comment.body) : null);
|
|
62
|
+
}
|
|
63
|
+
// Summary may also contain `[Pn]`-tagged findings — the prompt's fallback
|
|
64
|
+
// tells the model to surface findings in the summary when inline placement
|
|
65
|
+
// fails (e.g. unclear line numbers). Scan line-by-line so we don't
|
|
66
|
+
// double-count tags repeated within the same sentence; only increment for
|
|
67
|
+
// lines that match.
|
|
68
|
+
for (const line of review.summary?.split('\n') ?? []) {
|
|
69
|
+
const sev = parseSeverity(line);
|
|
70
|
+
if (sev)
|
|
71
|
+
increment(sev);
|
|
72
|
+
}
|
|
73
|
+
return counts;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Builds a human-readable title for a successful check run.
|
|
77
|
+
* Examples: "No findings", "3 findings (2 P1, 1 P2)".
|
|
78
|
+
*/
|
|
79
|
+
export function buildSuccessTitle(counts) {
|
|
80
|
+
if (counts.total === 0) {
|
|
81
|
+
return 'No findings';
|
|
82
|
+
}
|
|
83
|
+
const parts = [];
|
|
84
|
+
if (counts.p0 > 0)
|
|
85
|
+
parts.push(`${counts.p0} P0`);
|
|
86
|
+
if (counts.p1 > 0)
|
|
87
|
+
parts.push(`${counts.p1} P1`);
|
|
88
|
+
if (counts.p2 > 0)
|
|
89
|
+
parts.push(`${counts.p2} P2`);
|
|
90
|
+
if (counts.p3 > 0)
|
|
91
|
+
parts.push(`${counts.p3} P3`);
|
|
92
|
+
if (counts.untagged > 0)
|
|
93
|
+
parts.push(`${counts.untagged} other`);
|
|
94
|
+
const breakdown = parts.length > 0 ? ` (${parts.join(', ')})` : '';
|
|
95
|
+
return `${counts.total} finding${counts.total === 1 ? '' : 's'}${breakdown}`;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Builds the markdown summary body for a successful check run. Renders in the
|
|
99
|
+
* Checks tab when expanded. Distinguishes between inline and summary-only
|
|
100
|
+
* findings so the wording matches reality (the prompt's fallback contract
|
|
101
|
+
* lets the model surface findings in `review.summary` when it can't place
|
|
102
|
+
* them inline — saying "inline comments" in that case sends authors to look
|
|
103
|
+
* for threads that don't exist).
|
|
104
|
+
*/
|
|
105
|
+
export function buildSuccessSummary(review, pullRequestUrl, counts = countFindingsBySeverity(review)) {
|
|
106
|
+
if (counts.total === 0) {
|
|
107
|
+
return [
|
|
108
|
+
'Doistbot reviewed this PR and surfaced no findings.',
|
|
109
|
+
'',
|
|
110
|
+
`[View PR](${pullRequestUrl})`,
|
|
111
|
+
].join('\n');
|
|
112
|
+
}
|
|
113
|
+
const inlineCount = review.comments?.length ?? 0;
|
|
114
|
+
let placement;
|
|
115
|
+
if (inlineCount === counts.total) {
|
|
116
|
+
placement = 'as inline comments';
|
|
117
|
+
}
|
|
118
|
+
else if (inlineCount === 0) {
|
|
119
|
+
placement = 'in the review summary';
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
placement = 'on this PR (some inline, some in the review summary)';
|
|
123
|
+
}
|
|
124
|
+
return [
|
|
125
|
+
`Doistbot posted **${counts.total} finding${counts.total === 1 ? '' : 's'}** ${placement}.`,
|
|
126
|
+
'',
|
|
127
|
+
`[View PR](${pullRequestUrl})`,
|
|
128
|
+
].join('\n');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Private helper: shared boilerplate (`detailsUrl`, `name`, footer links) for
|
|
132
|
+
* the in-progress, success, and failure wrappers below. Appends the standard
|
|
133
|
+
* Datadog-logs + feedback-issue footer used elsewhere in the review output, so
|
|
134
|
+
* authors can act on the Check Run summary directly without hopping back to the
|
|
135
|
+
* PR conversation comment.
|
|
136
|
+
*/
|
|
137
|
+
async function postReviewCheckRun(params) {
|
|
138
|
+
const { octokit, owner, repo, sha, pullNumber, deliveryId, checkRunId, title, summaryBody } = params;
|
|
139
|
+
const pullRequestUrl = `https://github.com/${owner}/${repo}/pull/${pullNumber}`;
|
|
140
|
+
const summary = appendReviewFeedbackLink(summaryBody, {
|
|
141
|
+
owner,
|
|
142
|
+
repo,
|
|
143
|
+
prNumber: pullNumber,
|
|
144
|
+
deliveryId,
|
|
145
|
+
});
|
|
146
|
+
const sharedArgs = {
|
|
147
|
+
octokit,
|
|
148
|
+
owner,
|
|
149
|
+
repo,
|
|
150
|
+
sha,
|
|
151
|
+
title,
|
|
152
|
+
summary,
|
|
153
|
+
detailsUrl: pullRequestUrl,
|
|
154
|
+
name: CHECK_RUN_NAME,
|
|
155
|
+
externalId: deliveryId,
|
|
156
|
+
checkRunId,
|
|
157
|
+
};
|
|
158
|
+
if (params.status === 'in_progress') {
|
|
159
|
+
await postCheckRun({ ...sharedArgs, status: 'in_progress' });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
await postCheckRun({ ...sharedArgs, conclusion: params.conclusion });
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Posts an `in_progress` check run when the review starts, so the PR's Checks
|
|
166
|
+
* tab shows that Doistbot is working. The summary carries the standard
|
|
167
|
+
* footer (Datadog logs + feedback link), so authors can follow live progress
|
|
168
|
+
* via the logs link without waiting for the final result. The completion call
|
|
169
|
+
* (`postReviewSuccessCheckRun` / `postReviewFailureCheckRun`) updates this
|
|
170
|
+
* same run via the upsert path.
|
|
171
|
+
*/
|
|
172
|
+
export async function postReviewInProgressCheckRun({ octokit, owner, repo, sha, pullNumber, deliveryId, }) {
|
|
173
|
+
await postReviewCheckRun({
|
|
174
|
+
octokit,
|
|
175
|
+
owner,
|
|
176
|
+
repo,
|
|
177
|
+
sha,
|
|
178
|
+
pullNumber,
|
|
179
|
+
deliveryId,
|
|
180
|
+
status: 'in_progress',
|
|
181
|
+
title: 'Doistbot review in progress',
|
|
182
|
+
summaryBody: 'Doistbot is reviewing this PR. Live logs are linked below.',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Phase-1 advisory mode: conclusion is always `success`, regardless of finding
|
|
187
|
+
* severity. Title summarizes the count + breakdown so authors see it at a
|
|
188
|
+
* glance in the Checks tab. Future phases (per the rollout proposal in
|
|
189
|
+
* `Doist/doistbot-app#237`) may flip to `failure` based on P0/P1 counts.
|
|
190
|
+
*/
|
|
191
|
+
export async function postReviewSuccessCheckRun({ octokit, owner, repo, sha, pullNumber, deliveryId, review, }) {
|
|
192
|
+
const pullRequestUrl = `https://github.com/${owner}/${repo}/pull/${pullNumber}`;
|
|
193
|
+
const counts = countFindingsBySeverity(review);
|
|
194
|
+
await postReviewCheckRun({
|
|
195
|
+
octokit,
|
|
196
|
+
owner,
|
|
197
|
+
repo,
|
|
198
|
+
sha,
|
|
199
|
+
pullNumber,
|
|
200
|
+
deliveryId,
|
|
201
|
+
conclusion: 'success',
|
|
202
|
+
title: buildSuccessTitle(counts),
|
|
203
|
+
summaryBody: buildSuccessSummary(review, pullRequestUrl, counts),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/** Shared title used by both the failure Check Run and stale-pointer recovery. */
|
|
207
|
+
const FAILURE_TITLE = 'Doistbot review unavailable';
|
|
208
|
+
/**
|
|
209
|
+
* Recovers the title that the terminal Check Run posted to `reviewedSha`,
|
|
210
|
+
* given the same input shape `postFinalCheckRun` already has. Lets the
|
|
211
|
+
* stale-pointer follow-up cite "Original result: …" without duplicating
|
|
212
|
+
* the title-building logic in two places.
|
|
213
|
+
*/
|
|
214
|
+
export function getReviewCheckRunTitle(input) {
|
|
215
|
+
if (input.conclusion === 'success') {
|
|
216
|
+
return buildSuccessTitle(countFindingsBySeverity(input.review));
|
|
217
|
+
}
|
|
218
|
+
return FAILURE_TITLE;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Posts a `neutral` check run with a "review unavailable" title so PR authors
|
|
222
|
+
* aren't left wondering whether the bot ran. `neutral` (gray −) visually
|
|
223
|
+
* distinguishes "didn't run" from "ran successfully" without blocking merges,
|
|
224
|
+
* per the rollout proposal: never block merges on Doistbot's own degradation.
|
|
225
|
+
*/
|
|
226
|
+
export async function postReviewFailureCheckRun({ octokit, owner, repo, sha, pullNumber, deliveryId, }) {
|
|
227
|
+
const pullRequestUrl = `https://github.com/${owner}/${repo}/pull/${pullNumber}`;
|
|
228
|
+
await postReviewCheckRun({
|
|
229
|
+
octokit,
|
|
230
|
+
owner,
|
|
231
|
+
repo,
|
|
232
|
+
sha,
|
|
233
|
+
pullNumber,
|
|
234
|
+
deliveryId,
|
|
235
|
+
conclusion: 'neutral',
|
|
236
|
+
title: FAILURE_TITLE,
|
|
237
|
+
summaryBody: [
|
|
238
|
+
"Doistbot's review didn't complete (provider outage, rate limit, or other failure).",
|
|
239
|
+
'',
|
|
240
|
+
"Optionally re-trigger with `@doistbot /review`. Don't block your PR on this.",
|
|
241
|
+
'',
|
|
242
|
+
`[View PR](${pullRequestUrl})`,
|
|
243
|
+
].join('\n'),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* After the terminal Check Run lands on `reviewedSha`, look up the PR's
|
|
248
|
+
* current head SHA. If it has moved during the review (i.e. the user pushed
|
|
249
|
+
* at least one commit while the review was in flight), post a neutral
|
|
250
|
+
* stale-pointer Check Run on the current head that links back to
|
|
251
|
+
* `reviewedSha`. Closes the in-progress-during-push gap that the webhook-side
|
|
252
|
+
* fix in [#311](https://github.com/Doist/doistbot-app/pull/311) can't cover
|
|
253
|
+
* on its own: that path only fires on `pull_request.synchronize` and only
|
|
254
|
+
* surfaces a pointer when a *completed* run sits on `payload.before`, so any
|
|
255
|
+
* commits pushed before the review terminated stayed empty.
|
|
256
|
+
*
|
|
257
|
+
* Uses the same `stale-pointer:<sha>` `external_id` format and same summary
|
|
258
|
+
* copy as `apps/webhook-handler/src/tasks/review/stale-check-run.ts` so the
|
|
259
|
+
* two paths interoperate: a subsequent synchronize-driven post finds this
|
|
260
|
+
* pointer on the prior head and unwraps to `reviewedSha`, keeping the
|
|
261
|
+
* "reference original" semantics across chained pushes.
|
|
262
|
+
*
|
|
263
|
+
* Skips quietly when:
|
|
264
|
+
* - Current head equals `reviewedSha` (no movement — the common case).
|
|
265
|
+
* - `pulls.get` fails (warn + return; restoring visibility is degraded UX,
|
|
266
|
+
* not a correctness issue).
|
|
267
|
+
*
|
|
268
|
+
* The `postCheckRun` call itself swallows write failures with a warn, same
|
|
269
|
+
* pattern as the other terminal-write helpers.
|
|
270
|
+
*
|
|
271
|
+
* TOCTOU caveat: a push between the `pulls.get` here and the `postCheckRun`
|
|
272
|
+
* write can leave the new pointer on a SHA that's no longer the head. Same
|
|
273
|
+
* race shape and same bound as the existing TOCTOU notes elsewhere; the
|
|
274
|
+
* webhook-side handler will paper over it on the next synchronize.
|
|
275
|
+
*/
|
|
276
|
+
export async function postStaleReviewPointerIfHeadMoved({ octokit, owner, repo, pullNumber, reviewedSha, reviewedTitle, }) {
|
|
277
|
+
let currentHeadSha;
|
|
278
|
+
try {
|
|
279
|
+
const { data } = await octokit.rest.pulls.get({
|
|
280
|
+
owner,
|
|
281
|
+
repo,
|
|
282
|
+
pull_number: pullNumber,
|
|
283
|
+
});
|
|
284
|
+
currentHeadSha = data.head.sha;
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
logger.warn('Failed to fetch PR current head; skipping stale pointer', {
|
|
288
|
+
component: 'stale-check-run',
|
|
289
|
+
pr_number: pullNumber,
|
|
290
|
+
reviewed_sha: reviewedSha,
|
|
291
|
+
error: error instanceof Error ? error.message : String(error),
|
|
292
|
+
});
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (currentHeadSha === reviewedSha) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const reviewedShaShort = reviewedSha.slice(0, 7);
|
|
299
|
+
const commitUrl = `https://github.com/${owner}/${repo}/pull/${pullNumber}/commits/${reviewedSha}`;
|
|
300
|
+
const summary = [
|
|
301
|
+
`⚠️ Doistbot reviewed an earlier commit ([\`${reviewedShaShort}\`](${commitUrl})).`,
|
|
302
|
+
'',
|
|
303
|
+
`**Original result:** ${reviewedTitle}`,
|
|
304
|
+
'',
|
|
305
|
+
'Inline review comments from the original run are still on this PR; ' +
|
|
306
|
+
'they were against the prior commit and may not reflect the current diff. ' +
|
|
307
|
+
'Comment `@doistbot /review` to re-run against the latest commit.',
|
|
308
|
+
].join('\n');
|
|
309
|
+
await postCheckRun({
|
|
310
|
+
octokit,
|
|
311
|
+
owner,
|
|
312
|
+
repo,
|
|
313
|
+
sha: currentHeadSha,
|
|
314
|
+
name: CHECK_RUN_NAME,
|
|
315
|
+
title: truncateTitle(`${STALE_TITLE_PREFIX}${reviewedTitle}`),
|
|
316
|
+
summary,
|
|
317
|
+
conclusion: 'neutral',
|
|
318
|
+
externalId: `${STALE_POINTER_EXTERNAL_ID_PREFIX}${reviewedSha}`,
|
|
319
|
+
});
|
|
320
|
+
logger.info('Posted stale-pointer Check Run on current head (head moved during review)', {
|
|
321
|
+
component: 'stale-check-run',
|
|
322
|
+
pr_number: pullNumber,
|
|
323
|
+
reviewed_sha: reviewedSha,
|
|
324
|
+
current_head_sha: currentHeadSha,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { logger } from '../../core/logging.js';
|
|
2
|
+
/**
|
|
3
|
+
* Converts model findings to review comments, filtering out invalid diff locations.
|
|
4
|
+
*/
|
|
5
|
+
function getInvalidFindingLocationReason(finding, lineLookup) {
|
|
6
|
+
const fileLines = lineLookup.get(finding.path);
|
|
7
|
+
if (!fileLines) {
|
|
8
|
+
return 'file not in diff';
|
|
9
|
+
}
|
|
10
|
+
const validLines = finding.side === 'LEFT' ? fileLines.old : fileLines.new;
|
|
11
|
+
if (!validLines.has(finding.line)) {
|
|
12
|
+
return 'line not in diff';
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
export function canMapFindingToComment(finding, lineLookup) {
|
|
17
|
+
return getInvalidFindingLocationReason(finding, lineLookup) == null;
|
|
18
|
+
}
|
|
19
|
+
export function convertFindingsToComments(findings, lineLookup) {
|
|
20
|
+
const comments = [];
|
|
21
|
+
const skipped = [];
|
|
22
|
+
for (const finding of findings) {
|
|
23
|
+
const invalidReason = getInvalidFindingLocationReason(finding, lineLookup);
|
|
24
|
+
if (invalidReason) {
|
|
25
|
+
skipped.push({
|
|
26
|
+
path: finding.path,
|
|
27
|
+
line: finding.line,
|
|
28
|
+
reason: invalidReason,
|
|
29
|
+
});
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
comments.push({
|
|
33
|
+
path: finding.path,
|
|
34
|
+
line: finding.line,
|
|
35
|
+
side: finding.side,
|
|
36
|
+
body: finding.body,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (skipped.length > 0) {
|
|
40
|
+
logger.info('Filtered invalid findings', {
|
|
41
|
+
skippedCount: skipped.length,
|
|
42
|
+
skipped,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return comments;
|
|
46
|
+
}
|