@doist/doistbot-cli 1.0.1 → 1.0.3
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/review-usage.js +20 -0
- package/dist/actions/review.js +126 -10
- package/dist/actions/setup.js +3 -2
- package/dist/index.js +18 -8
- package/dist/runtime.js +3 -3
- package/dist/update-check.js +1 -1
- package/package.json +6 -5
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +1 -0
- package/sandbox/dist/core/bot-logins.js +13 -0
- package/sandbox/dist/core/retry.js +29 -0
- package/sandbox/dist/core/shared.js +2 -2
- package/sandbox/dist/main.js +3 -0
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +1 -7
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +157 -28
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +190 -0
- package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +464 -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 +110 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +227 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +165 -0
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +32 -0
- package/sandbox/dist/tasks/issue-triage/markers.js +2 -0
- package/sandbox/dist/tasks/issue-triage/output.js +24 -3
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +72 -30
- package/sandbox/dist/tasks/issue-triage/repo-config.js +13 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +31 -10
- package/sandbox/dist/tasks/issue-triage/types.js +1 -0
- package/sandbox/dist/tasks/review/author-format.js +14 -0
- package/sandbox/dist/tasks/review/author-profile.js +67 -0
- package/sandbox/dist/tasks/review/check-run.js +8 -1
- package/sandbox/dist/tasks/review/comment-mapper.js +17 -12
- package/sandbox/dist/tasks/review/conversation-context.js +174 -20
- package/sandbox/dist/tasks/review/council/summary.js +141 -12
- package/sandbox/dist/tasks/review/engines/council.js +19 -3
- package/sandbox/dist/tasks/review/engines/dedupe.js +37 -28
- package/sandbox/dist/tasks/review/engines/multi-focus.js +123 -6
- package/sandbox/dist/tasks/review/engines/shared.js +28 -5
- package/sandbox/dist/tasks/review/finding-output.js +18 -0
- package/sandbox/dist/tasks/review/github-comment-format.js +32 -0
- package/sandbox/dist/tasks/review/local.js +1 -0
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +0 -1
- package/sandbox/dist/tasks/review/review.js +12 -2
- package/sandbox/dist/tasks/review/runner.js +36 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +1 -0
- package/sandbox/review-prompt.md +18 -1
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +10 -6
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +2 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +15 -1
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +2 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +14 -1
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +31 -4
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +0 -13
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const REVIEW_USAGE_COMMANDS = ['doistbot review [staged]', 'db review [staged]'];
|
|
2
|
+
const REVIEW_SETUP_COMMANDS = [
|
|
3
|
+
'doistbot review',
|
|
4
|
+
'doistbot review staged',
|
|
5
|
+
'db review',
|
|
6
|
+
'db review staged',
|
|
7
|
+
];
|
|
8
|
+
export function formatReviewUsage() {
|
|
9
|
+
return `Usage: ${formatCommandList(REVIEW_USAGE_COMMANDS)}`;
|
|
10
|
+
}
|
|
11
|
+
export function formatSetupCompleteMessage() {
|
|
12
|
+
return `Setup complete. Run ${formatCommandList(REVIEW_SETUP_COMMANDS)} from a git repository.`;
|
|
13
|
+
}
|
|
14
|
+
function formatCommandList(commands) {
|
|
15
|
+
const quotedCommands = commands.map((command) => `\`${command}\``);
|
|
16
|
+
if (quotedCommands.length <= 2) {
|
|
17
|
+
return quotedCommands.join(' or ');
|
|
18
|
+
}
|
|
19
|
+
return `${quotedCommands.slice(0, -1).join(', ')}, or ${quotedCommands.at(-1)}`;
|
|
20
|
+
}
|
package/dist/actions/review.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { getApiKeys } from '../config.js';
|
|
2
2
|
import { getRepositoryRoot, loadSandboxFormatModule, loadSandboxReviewModule } from '../runtime.js';
|
|
3
3
|
import { formatReviewProgress, renderAlert, renderMarkdown, withSpinner, writeLine, } from '../terminal.js';
|
|
4
|
+
import { formatReviewUsage } from './review-usage.js';
|
|
4
5
|
const LOCAL_REVIEW_SOURCE_STAGED = 'staged';
|
|
6
|
+
const JSON_SCHEMA_VERSION = 1;
|
|
5
7
|
const PROVIDER_DISPLAY_NAMES = {
|
|
6
8
|
gemini: 'Gemini',
|
|
7
9
|
openai: 'OpenAI Codex',
|
|
@@ -41,14 +43,17 @@ function getReadableProviderFailure(failure) {
|
|
|
41
43
|
?.trim();
|
|
42
44
|
return `${PROVIDER_DISPLAY_NAMES[failure.model]} failed: ${firstLine || 'unknown error'}`;
|
|
43
45
|
}
|
|
44
|
-
function
|
|
46
|
+
function getReadableProviderFailureEntries(failures) {
|
|
45
47
|
const messagesByModel = new Map();
|
|
46
48
|
for (const failure of failures) {
|
|
47
49
|
if (!messagesByModel.has(failure.model)) {
|
|
48
50
|
messagesByModel.set(failure.model, getReadableProviderFailure(failure));
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
|
-
return
|
|
53
|
+
return Array.from(messagesByModel, ([model, message]) => ({ model, message }));
|
|
54
|
+
}
|
|
55
|
+
function getReadableProviderFailures(failures) {
|
|
56
|
+
return getReadableProviderFailureEntries(failures).map(({ message }) => message);
|
|
52
57
|
}
|
|
53
58
|
function formatProviderFailureMessage(failures) {
|
|
54
59
|
const messages = getReadableProviderFailures(failures);
|
|
@@ -86,19 +91,130 @@ function handleSkippedReview(result) {
|
|
|
86
91
|
function formatReviewOutput({ formatReviewForTerminal, review, }) {
|
|
87
92
|
return formatReviewForTerminal({ review });
|
|
88
93
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
function getJsonStatus(result) {
|
|
95
|
+
function mapping(statusMapping, canTrustCleanReview = statusMapping.status === 'reviewed') {
|
|
96
|
+
return { mapping: statusMapping, canTrustCleanReview };
|
|
92
97
|
}
|
|
93
|
-
|
|
98
|
+
if (result.reviewResult.status === 'skipped') {
|
|
99
|
+
switch (result.reviewResult.reason) {
|
|
100
|
+
case 'empty_review':
|
|
101
|
+
case 'no_models':
|
|
102
|
+
case 'no_review':
|
|
103
|
+
return mapping({
|
|
104
|
+
status: 'unverified',
|
|
105
|
+
verified: false,
|
|
106
|
+
unverifiedReason: result.reviewResult.reason,
|
|
107
|
+
}, false);
|
|
108
|
+
default:
|
|
109
|
+
return mapping({
|
|
110
|
+
status: 'skipped',
|
|
111
|
+
verified: false,
|
|
112
|
+
skipReason: result.reviewResult.reason,
|
|
113
|
+
}, false);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return mapping({ status: 'reviewed', verified: true });
|
|
117
|
+
}
|
|
118
|
+
function finalizeJsonStatus({ initialStatus, findingCount, providerFailureCount, }) {
|
|
119
|
+
if (initialStatus.canTrustCleanReview && providerFailureCount > 0 && findingCount === 0) {
|
|
120
|
+
return { status: 'unverified', verified: false, unverifiedReason: 'provider_failures' };
|
|
121
|
+
}
|
|
122
|
+
return initialStatus.mapping;
|
|
123
|
+
}
|
|
124
|
+
function getReviewForJson(result) {
|
|
125
|
+
if (result.reviewResult.status === 'reviewed') {
|
|
126
|
+
return result.reviewResult.review;
|
|
127
|
+
}
|
|
128
|
+
return { summary: '', comments: [] };
|
|
129
|
+
}
|
|
130
|
+
function hasLocation(finding) {
|
|
131
|
+
return typeof finding.path === 'string' && typeof finding.line === 'number';
|
|
132
|
+
}
|
|
133
|
+
function formatReviewJsonFinding(finding) {
|
|
134
|
+
return {
|
|
135
|
+
body: finding.body,
|
|
136
|
+
severity: finding.severity,
|
|
137
|
+
...(hasLocation(finding)
|
|
138
|
+
? {
|
|
139
|
+
location: {
|
|
140
|
+
path: finding.path,
|
|
141
|
+
line: finding.line,
|
|
142
|
+
...(finding.side ? { side: finding.side } : {}),
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function getReviewFindingsForJson(review) {
|
|
149
|
+
if (review.findings) {
|
|
150
|
+
return review.findings;
|
|
151
|
+
}
|
|
152
|
+
return review.comments.map((comment) => ({
|
|
153
|
+
body: comment.body,
|
|
154
|
+
severity: null,
|
|
155
|
+
placement: 'inline',
|
|
156
|
+
path: comment.path,
|
|
157
|
+
line: comment.line,
|
|
158
|
+
side: comment.side,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
function getReviewFindingCount(review) {
|
|
162
|
+
return getReviewFindingsForJson(review).length;
|
|
163
|
+
}
|
|
164
|
+
function formatReviewForJson(result) {
|
|
165
|
+
const review = getReviewForJson(result);
|
|
166
|
+
const reviewFindings = getReviewFindingsForJson(review);
|
|
167
|
+
const findings = reviewFindings.map(formatReviewJsonFinding);
|
|
168
|
+
const statusMapping = finalizeJsonStatus({
|
|
169
|
+
initialStatus: getJsonStatus(result),
|
|
170
|
+
findingCount: findings.length,
|
|
171
|
+
providerFailureCount: result.reviewResult.providerFailures.length,
|
|
172
|
+
});
|
|
173
|
+
return {
|
|
174
|
+
schemaVersion: JSON_SCHEMA_VERSION,
|
|
175
|
+
...statusMapping,
|
|
176
|
+
reviewSource: result.source,
|
|
177
|
+
repository: {
|
|
178
|
+
owner: result.owner,
|
|
179
|
+
repo: result.repo,
|
|
180
|
+
branch: result.branch,
|
|
181
|
+
baseBranch: result.baseBranch,
|
|
182
|
+
},
|
|
183
|
+
review: {
|
|
184
|
+
count: findings.length,
|
|
185
|
+
findings,
|
|
186
|
+
},
|
|
187
|
+
providerFailures: getReadableProviderFailureEntries(result.reviewResult.providerFailures),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function runLocalReviewForCli({ source, onProgress, }) {
|
|
94
191
|
const reviewModule = await loadSandboxReviewModule();
|
|
95
|
-
|
|
96
|
-
? 'Reviewing staged changes...'
|
|
97
|
-
: 'Reviewing local changes...', async (status) => await reviewModule.runLocalReview({
|
|
192
|
+
return await reviewModule.runLocalReview({
|
|
98
193
|
workspaceDir: getRepositoryRoot(),
|
|
99
194
|
source,
|
|
100
195
|
apiKeys: await getApiKeys(),
|
|
101
196
|
suppressLogs: true,
|
|
197
|
+
onProgress,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
export async function runReview(sourceArg, options = {}) {
|
|
201
|
+
if (sourceArg !== undefined && sourceArg !== LOCAL_REVIEW_SOURCE_STAGED) {
|
|
202
|
+
throw new Error(formatReviewUsage());
|
|
203
|
+
}
|
|
204
|
+
const source = sourceArg === LOCAL_REVIEW_SOURCE_STAGED ? LOCAL_REVIEW_SOURCE_STAGED : 'base';
|
|
205
|
+
if (options.json === true) {
|
|
206
|
+
const result = await runLocalReviewForCli({ source });
|
|
207
|
+
const output = formatReviewForJson(result);
|
|
208
|
+
writeLine(JSON.stringify(output, null, 2));
|
|
209
|
+
if (output.status === 'unverified') {
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const result = await withSpinner(source === LOCAL_REVIEW_SOURCE_STAGED
|
|
215
|
+
? 'Reviewing staged changes...'
|
|
216
|
+
: 'Reviewing local changes...', async (status) => await runLocalReviewForCli({
|
|
217
|
+
source,
|
|
102
218
|
onProgress: status.enabled
|
|
103
219
|
? (event) => status.update(formatReviewProgress(event))
|
|
104
220
|
: undefined,
|
|
@@ -109,7 +225,7 @@ export async function runReview(sourceArg) {
|
|
|
109
225
|
}
|
|
110
226
|
const formatModule = await loadSandboxFormatModule();
|
|
111
227
|
if (result.reviewResult.providerFailures.length > 0 &&
|
|
112
|
-
result.reviewResult.review
|
|
228
|
+
getReviewFindingCount(result.reviewResult.review) === 0) {
|
|
113
229
|
throw new Error(formatProviderFailureMessage(result.reviewResult.providerFailures));
|
|
114
230
|
}
|
|
115
231
|
const output = formatReviewOutput({
|
package/dist/actions/setup.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { checkGit, checkPiDependency, checkSandboxBuild } from '../runtime.js';
|
|
2
2
|
import { renderDiagnosticCheck, writeLine } from '../terminal.js';
|
|
3
3
|
import { runAuthSetup } from './auth.js';
|
|
4
|
+
import { formatSetupCompleteMessage } from './review-usage.js';
|
|
4
5
|
export async function runSetup() {
|
|
5
|
-
writeLine('Checking
|
|
6
|
+
writeLine('Checking Doistbot CLI prerequisites...');
|
|
6
7
|
await renderDiagnosticCheck('git', checkGit);
|
|
7
8
|
await renderDiagnosticCheck('pi', checkPiDependency);
|
|
8
9
|
await renderDiagnosticCheck('sandbox build', checkSandboxBuild);
|
|
9
10
|
writeLine();
|
|
10
11
|
await runAuthSetup();
|
|
11
12
|
writeLine();
|
|
12
|
-
writeLine(
|
|
13
|
+
writeLine(formatSetupCompleteMessage());
|
|
13
14
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import { Command } from 'commander';
|
|
5
|
+
import { Command, Option } from 'commander';
|
|
6
6
|
import { AUTH_LOGOUT_PROVIDER_USAGE, runAuthLogout, runAuthSetup, runAuthStatus, } from './actions/auth.js';
|
|
7
7
|
import { runDoctor } from './actions/doctor.js';
|
|
8
8
|
import { runReview } from './actions/review.js';
|
|
@@ -34,18 +34,27 @@ export function createProgram({ runAuthLogout: authLogout = runAuthLogout, runAu
|
|
|
34
34
|
automaticUpdateCommands.add(command);
|
|
35
35
|
return command;
|
|
36
36
|
}
|
|
37
|
+
function shouldRunAutomaticUpdateCheck(actionCommand) {
|
|
38
|
+
if (!automaticUpdateCommands.has(actionCommand)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (actionCommand.name() === 'review' && actionCommand.opts().json) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
37
46
|
program
|
|
38
47
|
.name('doistbot')
|
|
39
|
-
.description('
|
|
48
|
+
.description('Doistbot local self-review CLI')
|
|
40
49
|
.version(PACKAGE_VERSION)
|
|
41
50
|
.hook('postAction', async (_thisCommand, actionCommand) => {
|
|
42
|
-
if (
|
|
51
|
+
if (shouldRunAutomaticUpdateCheck(actionCommand)) {
|
|
43
52
|
await automaticUpdateCheck();
|
|
44
53
|
}
|
|
45
54
|
});
|
|
46
55
|
withAutomaticUpdateCheck(program
|
|
47
56
|
.command('doctor')
|
|
48
|
-
.description('Diagnose local
|
|
57
|
+
.description('Diagnose local Doistbot CLI setup')
|
|
49
58
|
.action(async () => await doctor()));
|
|
50
59
|
withAutomaticUpdateCheck(program
|
|
51
60
|
.command('setup')
|
|
@@ -53,7 +62,7 @@ export function createProgram({ runAuthLogout: authLogout = runAuthLogout, runAu
|
|
|
53
62
|
.action(async () => await setup()));
|
|
54
63
|
const authCommand = withAutomaticUpdateCheck(program
|
|
55
64
|
.command('auth')
|
|
56
|
-
.description('Authenticate
|
|
65
|
+
.description('Authenticate Doistbot CLI with local review providers')
|
|
57
66
|
.action(async () => await authSetup()));
|
|
58
67
|
withAutomaticUpdateCheck(authCommand
|
|
59
68
|
.command('status')
|
|
@@ -65,15 +74,16 @@ export function createProgram({ runAuthLogout: authLogout = runAuthLogout, runAu
|
|
|
65
74
|
.action(async () => await authSetup()));
|
|
66
75
|
withAutomaticUpdateCheck(authCommand
|
|
67
76
|
.command('logout [provider]')
|
|
68
|
-
.description(`Clear stored
|
|
77
|
+
.description(`Clear stored Doistbot CLI auth for ${AUTH_LOGOUT_PROVIDER_USAGE}`)
|
|
69
78
|
.action(async (provider) => await authLogout(provider)));
|
|
70
79
|
withAutomaticUpdateCheck(program
|
|
71
80
|
.command('review [source]')
|
|
72
81
|
.description('Run a local self-review. Use `staged` to review only staged changes.')
|
|
73
|
-
.
|
|
82
|
+
.addOption(new Option('--json', 'Print machine-readable JSON instead of terminal-formatted markdown').hideHelp())
|
|
83
|
+
.action(async (source, options) => await review(source, { json: options?.json === true })));
|
|
74
84
|
program
|
|
75
85
|
.command('update')
|
|
76
|
-
.description('Check whether a newer
|
|
86
|
+
.description('Check whether a newer Doistbot CLI version is available')
|
|
77
87
|
.action(async () => await updateCheck());
|
|
78
88
|
return program;
|
|
79
89
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -53,10 +53,10 @@ async function loadBundledModule(modulePath, missingMessage) {
|
|
|
53
53
|
return (await import(pathToFileURL(modulePath).href));
|
|
54
54
|
}
|
|
55
55
|
export async function loadSandboxReviewModule() {
|
|
56
|
-
return loadBundledModule(getSandboxReviewModulePath(), 'Sandbox review runner is missing. Rebuild or reinstall the
|
|
56
|
+
return loadBundledModule(getSandboxReviewModulePath(), 'Sandbox review runner is missing. Rebuild or reinstall the Doistbot CLI package.');
|
|
57
57
|
}
|
|
58
58
|
export async function loadSandboxFormatModule() {
|
|
59
|
-
return loadBundledModule(getSandboxFormatModulePath(), 'Sandbox review formatter is missing. Rebuild or reinstall the
|
|
59
|
+
return loadBundledModule(getSandboxFormatModulePath(), 'Sandbox review formatter is missing. Rebuild or reinstall the Doistbot CLI package.');
|
|
60
60
|
}
|
|
61
61
|
export function runGit(args, cwd = process.cwd()) {
|
|
62
62
|
return execFileSync('git', args, {
|
|
@@ -89,7 +89,7 @@ export function checkSandboxBuild() {
|
|
|
89
89
|
const modulePath = getSandboxReviewModulePath();
|
|
90
90
|
return fs.existsSync(modulePath)
|
|
91
91
|
? { ok: true, message: 'sandbox review runner is built' }
|
|
92
|
-
: { ok: false, message: 'rebuild or reinstall the
|
|
92
|
+
: { ok: false, message: 'rebuild or reinstall the Doistbot CLI package' };
|
|
93
93
|
}
|
|
94
94
|
export function checkPiDependency() {
|
|
95
95
|
try {
|
package/dist/update-check.js
CHANGED
|
@@ -38,7 +38,7 @@ export async function notifyIfCliUpdateAvailable({ cachePath = getDefaultUpdateC
|
|
|
38
38
|
timeoutMs,
|
|
39
39
|
});
|
|
40
40
|
if (!latestVersion) {
|
|
41
|
-
write('Unable to check for
|
|
41
|
+
write('Unable to check for Doistbot CLI updates.');
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
if (!isNewerVersion(latestVersion, currentVersion)) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doist/doistbot-cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Local self-review CLI for
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Local self-review CLI for Doistbot",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
+
"db": "dist/index.js",
|
|
8
9
|
"doistbot": "dist/index.js"
|
|
9
10
|
},
|
|
10
11
|
"keywords": [
|
|
@@ -41,8 +42,8 @@
|
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@inquirer/checkbox": "5.1.5",
|
|
43
44
|
"@inquirer/password": "5.0.13",
|
|
44
|
-
"@mariozechner/pi-ai": "0.73.
|
|
45
|
-
"@mariozechner/pi-coding-agent": "0.73.
|
|
45
|
+
"@mariozechner/pi-ai": "0.73.1",
|
|
46
|
+
"@mariozechner/pi-coding-agent": "0.73.1",
|
|
46
47
|
"@napi-rs/keyring": "1.3.0",
|
|
47
48
|
"ajv": "8.20.0",
|
|
48
49
|
"chalk": "4.1.2",
|
|
@@ -52,6 +53,6 @@
|
|
|
52
53
|
"yocto-spinner": "1.2.0"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
|
-
"vitest": "4.1.
|
|
56
|
+
"vitest": "4.1.6"
|
|
56
57
|
}
|
|
57
58
|
}
|
|
@@ -6,9 +6,22 @@ export const KNOWN_DOISTBOT_LOGINS = new Set([
|
|
|
6
6
|
'doistbot-app',
|
|
7
7
|
'doistbot-app[bot]',
|
|
8
8
|
]);
|
|
9
|
+
export const KNOWN_AUTOMATION_LOGINS = new Set([
|
|
10
|
+
...KNOWN_DOISTBOT_LOGINS,
|
|
11
|
+
'dependabot',
|
|
12
|
+
'dependabot[bot]',
|
|
13
|
+
'renovate',
|
|
14
|
+
'renovate[bot]',
|
|
15
|
+
]);
|
|
9
16
|
export function isKnownDoistbotLogin(login) {
|
|
10
17
|
if (!login) {
|
|
11
18
|
return false;
|
|
12
19
|
}
|
|
13
20
|
return KNOWN_DOISTBOT_LOGINS.has(login.toLowerCase());
|
|
14
21
|
}
|
|
22
|
+
export function isKnownAutomationLogin(login) {
|
|
23
|
+
if (!login) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return KNOWN_AUTOMATION_LOGINS.has(login.toLowerCase());
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Default per-attempt delays for retrying transient HTTP failures. */
|
|
2
|
+
export const HTTP_RETRY_DELAYS_MS = [250, 500, 1000];
|
|
3
|
+
/** True for HTTP errors that are typically worth retrying: 429 and any 5xx. */
|
|
4
|
+
function isTransientHttpError(error) {
|
|
5
|
+
const status = error?.status;
|
|
6
|
+
return typeof status === 'number' && (status === 429 || (status >= 500 && status < 600));
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Retries `fn` on errors classified as retryable, sleeping between attempts.
|
|
10
|
+
* Total attempts = 1 + delaysMs.length. Throws immediately on a non-retryable
|
|
11
|
+
* error, or after the budget is exhausted (re-throwing the last error).
|
|
12
|
+
*
|
|
13
|
+
* Defaults match HTTP usage (429 / 5xx with exponential backoff). Pass
|
|
14
|
+
* `delaysMs` / `isRetryable` to retry against a different signal.
|
|
15
|
+
*/
|
|
16
|
+
export async function withRetry(fn, delaysMs = HTTP_RETRY_DELAYS_MS, isRetryable = isTransientHttpError) {
|
|
17
|
+
let attempt = 0;
|
|
18
|
+
while (true) {
|
|
19
|
+
try {
|
|
20
|
+
return await fn();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (!isRetryable(error) || attempt >= delaysMs.length)
|
|
24
|
+
throw error;
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, delaysMs[attempt]));
|
|
26
|
+
attempt++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -44,8 +44,8 @@ export function parseBaseContext() {
|
|
|
44
44
|
return {
|
|
45
45
|
owner: requiredEnv('REPO_OWNER'),
|
|
46
46
|
repo: requiredEnv('REPO_NAME'),
|
|
47
|
-
prNumber:
|
|
48
|
-
installationId:
|
|
47
|
+
prNumber: requiredPositiveInt('PR_NUMBER'),
|
|
48
|
+
installationId: requiredPositiveInt('INSTALLATION_ID'),
|
|
49
49
|
appId: requiredEnv('APP_ID'),
|
|
50
50
|
privateKey: requiredEnv('PRIVATE_KEY'),
|
|
51
51
|
githubToken: process.env.GITHUB_TOKEN?.trim() || undefined,
|
package/sandbox/dist/main.js
CHANGED
|
@@ -12,6 +12,9 @@ switch (taskType) {
|
|
|
12
12
|
case 'issue-fix-retry':
|
|
13
13
|
await import('./tasks/issue-fix-retry/fix-retry.js');
|
|
14
14
|
break;
|
|
15
|
+
case 'fix-loop-promotion':
|
|
16
|
+
await import('./tasks/issue-triage/fix-loop/promotion-task.js');
|
|
17
|
+
break;
|
|
15
18
|
case 'persist-logs': {
|
|
16
19
|
const { main } = await import('./tasks/persist-logs/persist-logs.js');
|
|
17
20
|
await main();
|
|
@@ -161,13 +161,7 @@ async function main() {
|
|
|
161
161
|
octokit: writeOctokit,
|
|
162
162
|
triageOutput,
|
|
163
163
|
fixAssessment,
|
|
164
|
-
clonedRepos: clonedRepositories
|
|
165
|
-
owner: repository.owner,
|
|
166
|
-
repo: repository.repo,
|
|
167
|
-
fullName: repository.fullName,
|
|
168
|
-
localPath,
|
|
169
|
-
repository,
|
|
170
|
-
})),
|
|
164
|
+
clonedRepos: clonedRepositories,
|
|
171
165
|
issueTitle: issue.title ?? 'Untitled issue',
|
|
172
166
|
issueUrl: issue.html_url ?? getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber),
|
|
173
167
|
ghToken: issueInstallationAuth.token,
|