@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,233 @@
|
|
|
1
|
+
import { autofixFeedbackCommentMarker, iterationLabel } from 'doistbot-shared-contracts';
|
|
2
|
+
import { logger } from '../../../core/logging.js';
|
|
3
|
+
import { isFixLoopTerminal, ISSUE_FIX_INITIAL_STATE, isTerminal, next, } from './state-machine.js';
|
|
4
|
+
const CI_FAILED_LABEL = 'doistbot:ci-failed';
|
|
5
|
+
const NEEDS_HUMAN_LABEL = 'doistbot:needs-human';
|
|
6
|
+
const AWAITING_CI_LABEL = 'doistbot:awaiting-ci';
|
|
7
|
+
// Each terminal comment carries its outcome in the marker so retries update
|
|
8
|
+
// the same reaction target and closure tracking can classify the result.
|
|
9
|
+
export const PR_FEEDBACK_FOOTER = '<sub>🤖🫵 Please, react 👍 or 👎 on this comment to rate this bot-authored PR.</sub>';
|
|
10
|
+
function withFeedbackFooter(body) {
|
|
11
|
+
return `${body}\n\n${PR_FEEDBACK_FOOTER}`;
|
|
12
|
+
}
|
|
13
|
+
async function postNeedsHumanFeedbackComment(effects, prNumber, body = 'This PR needs human review.') {
|
|
14
|
+
await effects.postIssueComment({
|
|
15
|
+
prNumber,
|
|
16
|
+
marker: autofixFeedbackCommentMarker('needs_human'),
|
|
17
|
+
body: withFeedbackFooter(body),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function formatFailedChecks(checks) {
|
|
21
|
+
return checks.map((c) => (c.url ? `- [${c.name}](${c.url})` : `- ${c.name}`)).join('\n');
|
|
22
|
+
}
|
|
23
|
+
function createEventApplier(onTransition) {
|
|
24
|
+
return (state, event) => {
|
|
25
|
+
const nextState = next(state, event);
|
|
26
|
+
onTransition?.({ from: state, event, to: nextState });
|
|
27
|
+
return nextState;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Drives the fix loop state machine to a terminal-for-fix-loop state
|
|
32
|
+
* (PUSHED_AWAITING_CI, DONE_PR_UPDATED, or DONE_NEEDS_HUMAN). For the
|
|
33
|
+
* doistbot_fix_ci policy, the promotion flow takes over from
|
|
34
|
+
* PUSHED_AWAITING_CI on the check_suite webhook; for the `none` policy
|
|
35
|
+
* (address_review), the workflow terminates in-process at DONE_PR_UPDATED.
|
|
36
|
+
*/
|
|
37
|
+
export async function runFixLoop(input) {
|
|
38
|
+
const { effects, initial, onTransition } = input;
|
|
39
|
+
const applyEvent = createEventApplier(onTransition);
|
|
40
|
+
let state = initial ?? ISSUE_FIX_INITIAL_STATE;
|
|
41
|
+
while (!isFixLoopTerminal(state)) {
|
|
42
|
+
await input.beforeStep?.(state);
|
|
43
|
+
if (state.kind === 'AWAITING_FIRST_FIX' || state.kind === 'ITERATING') {
|
|
44
|
+
const fixInput = state.kind === 'ITERATING'
|
|
45
|
+
? {
|
|
46
|
+
iteration: state.iteration,
|
|
47
|
+
prevHeadSha: state.prevHeadSha,
|
|
48
|
+
previousFindings: state.findings,
|
|
49
|
+
}
|
|
50
|
+
: { iteration: 0 };
|
|
51
|
+
const fix = await effects.runFix(fixInput);
|
|
52
|
+
// runFixCore validates the diff internally and returns ok:false
|
|
53
|
+
// (with a "Diff validation failed: ..." reason) when invalid; no
|
|
54
|
+
// need for the runner to re-validate.
|
|
55
|
+
if (!fix.ok) {
|
|
56
|
+
state = applyEvent(state, { kind: 'FIX_FAILED', reason: fix.reason });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const pushInput = {
|
|
60
|
+
iteration: fixInput.iteration,
|
|
61
|
+
diff: fix.diff,
|
|
62
|
+
codexOutput: fix.codexOutput,
|
|
63
|
+
generatedPRTitle: fix.generatedPRTitle,
|
|
64
|
+
generatedPRBody: fix.generatedPRBody,
|
|
65
|
+
};
|
|
66
|
+
if (state.kind === 'ITERATING') {
|
|
67
|
+
pushInput.branch = state.branch;
|
|
68
|
+
}
|
|
69
|
+
const push = await effects.pushCommit(pushInput);
|
|
70
|
+
const prNumber = state.kind === 'ITERATING' ? state.prNumber : push.prNumber;
|
|
71
|
+
if (prNumber === undefined) {
|
|
72
|
+
state = applyEvent(state, {
|
|
73
|
+
kind: 'FIX_FAILED',
|
|
74
|
+
reason: 'Fix push did not produce a PR number.',
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
state = applyEvent(state, {
|
|
79
|
+
kind: 'FIX_PUSHED',
|
|
80
|
+
headSha: push.headSha,
|
|
81
|
+
branch: push.branch,
|
|
82
|
+
prNumber,
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (state.kind === 'REVIEWING') {
|
|
87
|
+
const reviewInput = {
|
|
88
|
+
iteration: state.iteration,
|
|
89
|
+
};
|
|
90
|
+
if (state.reviewBaseSha !== undefined) {
|
|
91
|
+
reviewInput.reviewBaseSha = state.reviewBaseSha;
|
|
92
|
+
}
|
|
93
|
+
const result = await effects.runReview(reviewInput);
|
|
94
|
+
if (result.kind === 'unverified') {
|
|
95
|
+
state = applyEvent(state, { kind: 'REVIEW_UNVERIFIED', reason: result.reason });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
state = applyEvent(state, { kind: 'REVIEW_COMPLETE', findings: result.findings });
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Defensive: any other intermediate state is a bug.
|
|
103
|
+
throw new Error(`runFixLoop: unexpected state ${state.kind}`);
|
|
104
|
+
}
|
|
105
|
+
await input.beforeFinish?.(state);
|
|
106
|
+
if (state.kind === 'PUSHED_AWAITING_CI' &&
|
|
107
|
+
state.iteration !== undefined &&
|
|
108
|
+
state.promotionPolicy.kind === 'doistbot_fix_ci') {
|
|
109
|
+
// Local review accepted the patch; hand off to CI. awaiting-ci must
|
|
110
|
+
// NOT appear earlier: a fast CI suite could otherwise finalize checks
|
|
111
|
+
// for a patch the bot is still reviewing or about to iterate on.
|
|
112
|
+
// Assignee-reviewed PRs may already be ready, but still need this handoff.
|
|
113
|
+
// The iter-N label event also triggers reconciliation if CI already
|
|
114
|
+
// finished before awaiting-ci existed. Keep both labels in one call.
|
|
115
|
+
await tryApplyLabels(effects, state.prNumber, [
|
|
116
|
+
AWAITING_CI_LABEL,
|
|
117
|
+
iterationLabel(state.iteration),
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
if (state.kind === 'DONE_NEEDS_HUMAN' && state.prNumber !== undefined) {
|
|
121
|
+
await postNeedsHumanFeedbackComment(effects, state.prNumber);
|
|
122
|
+
}
|
|
123
|
+
return state;
|
|
124
|
+
}
|
|
125
|
+
async function tryApplyLabels(effects, prNumber, labels) {
|
|
126
|
+
try {
|
|
127
|
+
await effects.applyLabels({ prNumber, labels });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
logger.error('fix-loop: failed to apply tracking labels to PR', {
|
|
131
|
+
task: 'fix-loop',
|
|
132
|
+
pr_number: prNumber,
|
|
133
|
+
labels,
|
|
134
|
+
error: error instanceof Error ? error.message : String(error),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Drives the promotion flow from PUSHED_AWAITING_CI through CI verification.
|
|
140
|
+
* Terminates at DONE_PROMOTED or DONE_NEEDS_HUMAN, or returns the unchanged
|
|
141
|
+
* PUSHED_AWAITING_CI state when CI is still pending (the caller re-invokes
|
|
142
|
+
* on the next webhook delivery).
|
|
143
|
+
*/
|
|
144
|
+
export async function runPromotionFlow(effects, input) {
|
|
145
|
+
// Only the doistbot auto-fix workflow ever reaches CI promotion; the
|
|
146
|
+
// address_review path terminates at DONE_PR_UPDATED in-process and never
|
|
147
|
+
// invokes this entry point. The upstream webhook handler is responsible
|
|
148
|
+
// for filtering (e.g. by PR label) so this function is only called for
|
|
149
|
+
// issue_fix PRs.
|
|
150
|
+
const initial = {
|
|
151
|
+
kind: 'PUSHED_AWAITING_CI',
|
|
152
|
+
headSha: input.headSha,
|
|
153
|
+
prNumber: input.prNumber,
|
|
154
|
+
intent: 'issue_fix',
|
|
155
|
+
promotionPolicy: { kind: 'doistbot_fix_ci' },
|
|
156
|
+
maxIterations: 2,
|
|
157
|
+
};
|
|
158
|
+
// The webhook handler's atomic claim removed AWAITING_CI_LABEL before
|
|
159
|
+
// dispatching this task. Every exit path from here must either re-arm
|
|
160
|
+
// the label (pending CI, side-effect failure) or leave it off
|
|
161
|
+
// intentionally (terminal success / human escalation). Each side effect
|
|
162
|
+
// below is idempotent on GitHub's side, so a future webhook redelivery
|
|
163
|
+
// can safely re-enter promotion.
|
|
164
|
+
try {
|
|
165
|
+
const ciStatus = await effects.checkCIStatus(input.headSha);
|
|
166
|
+
const state = next(initial, { kind: 'CI_RESULT', status: ciStatus });
|
|
167
|
+
if (state.kind === 'PUSHED_AWAITING_CI') {
|
|
168
|
+
// CI still in flight. Re-arm so the next check_suite event can
|
|
169
|
+
// re-claim and re-dispatch.
|
|
170
|
+
await effects.applyLabels({
|
|
171
|
+
prNumber: input.prNumber,
|
|
172
|
+
labels: [AWAITING_CI_LABEL],
|
|
173
|
+
});
|
|
174
|
+
return state;
|
|
175
|
+
}
|
|
176
|
+
if (state.kind === 'DONE_PROMOTED') {
|
|
177
|
+
await effects.requestHeroReview(input.prNumber);
|
|
178
|
+
await effects.applyAskLabel(input.prNumber);
|
|
179
|
+
// Publish the successful validation outcome before making a
|
|
180
|
+
// remaining draft ready, so an immediate merge cannot beat the
|
|
181
|
+
// feedback comment and closure tracking.
|
|
182
|
+
await effects.postIssueComment({
|
|
183
|
+
prNumber: input.prNumber,
|
|
184
|
+
marker: autofixFeedbackCommentMarker('success'),
|
|
185
|
+
// This comment doubles as the thumbs-feedback prompt: closure
|
|
186
|
+
// tracking reads 👍/👎 reactions on this exact comment (located
|
|
187
|
+
// via the marker above) when the PR closes. Keep the reaction
|
|
188
|
+
// ask on the comment people are invited to react to.
|
|
189
|
+
body: withFeedbackFooter('CI passed. Review has been requested; any remaining draft status will be cleared.'),
|
|
190
|
+
});
|
|
191
|
+
await effects.promoteToReady(input.prNumber);
|
|
192
|
+
return state;
|
|
193
|
+
}
|
|
194
|
+
if (state.kind === 'DONE_NEEDS_HUMAN') {
|
|
195
|
+
const labels = ciStatus.kind === 'failed'
|
|
196
|
+
? [CI_FAILED_LABEL, NEEDS_HUMAN_LABEL]
|
|
197
|
+
: [NEEDS_HUMAN_LABEL];
|
|
198
|
+
await effects.applyLabels({ prNumber: input.prNumber, labels });
|
|
199
|
+
const body = ciStatus.kind === 'failed'
|
|
200
|
+
? `CI failed for this fix attempt. Failing checks:\n${formatFailedChecks(ciStatus.failedChecks)}\n\nMarking this for human review.`
|
|
201
|
+
: ciStatus.kind === 'no_checks'
|
|
202
|
+
? 'No CI checks ran on this PR — please verify manually before merging.'
|
|
203
|
+
: 'This PR needs human review.';
|
|
204
|
+
await postNeedsHumanFeedbackComment(effects, input.prNumber, body);
|
|
205
|
+
return state;
|
|
206
|
+
}
|
|
207
|
+
if (!isTerminal(state)) {
|
|
208
|
+
throw new Error(`runPromotionFlow: unexpected non-terminal state ${state.kind}`);
|
|
209
|
+
}
|
|
210
|
+
return state;
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
await reapplyAwaitingCi(effects, input.prNumber, error);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async function reapplyAwaitingCi(effects, prNumber, originalError) {
|
|
218
|
+
try {
|
|
219
|
+
await effects.applyLabels({ prNumber, labels: [AWAITING_CI_LABEL] });
|
|
220
|
+
}
|
|
221
|
+
catch (rearmError) {
|
|
222
|
+
// The original error is the primary failure operators need to see;
|
|
223
|
+
// surface the re-arm failure alongside it so the recovery hint
|
|
224
|
+
// ("awaiting-ci is still missing, redelivery will not retry") is
|
|
225
|
+
// visible, but don't replace the cause.
|
|
226
|
+
logger.error('runPromotionFlow: failed to re-apply awaiting-ci after side-effect failure', {
|
|
227
|
+
task: 'fix-loop-promotion',
|
|
228
|
+
pr_number: prNumber,
|
|
229
|
+
original_error: originalError instanceof Error ? originalError.message : String(originalError),
|
|
230
|
+
rearm_error: rearmError instanceof Error ? rearmError.message : String(rearmError),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export const ISSUE_FIX_INITIAL_STATE = {
|
|
2
|
+
kind: 'AWAITING_FIRST_FIX',
|
|
3
|
+
intent: 'issue_fix',
|
|
4
|
+
promotionPolicy: { kind: 'doistbot_fix_ci' },
|
|
5
|
+
maxIterations: 2,
|
|
6
|
+
};
|
|
7
|
+
export function isTerminal(state) {
|
|
8
|
+
return (state.kind === 'DONE_PROMOTED' ||
|
|
9
|
+
state.kind === 'DONE_PR_UPDATED' ||
|
|
10
|
+
state.kind === 'DONE_NEEDS_HUMAN');
|
|
11
|
+
}
|
|
12
|
+
export function isFixLoopTerminal(state) {
|
|
13
|
+
return isTerminal(state) || state.kind === 'PUSHED_AWAITING_CI';
|
|
14
|
+
}
|
|
15
|
+
function blockers(findings) {
|
|
16
|
+
return findings.filter((f) => f.severity === 'P0' || f.severity === 'P1');
|
|
17
|
+
}
|
|
18
|
+
function invariant(state, event) {
|
|
19
|
+
throw new Error(`fix-loop: invalid event ${event.kind} for state ${state.kind}`);
|
|
20
|
+
}
|
|
21
|
+
function withMeta(base, meta) {
|
|
22
|
+
const out = { ...base };
|
|
23
|
+
if (meta.prNumber !== undefined)
|
|
24
|
+
out.prNumber = meta.prNumber;
|
|
25
|
+
if (meta.branch !== undefined)
|
|
26
|
+
out.branch = meta.branch;
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function header(state) {
|
|
30
|
+
return {
|
|
31
|
+
intent: state.intent,
|
|
32
|
+
promotionPolicy: state.promotionPolicy,
|
|
33
|
+
maxIterations: state.maxIterations,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function next(state, event) {
|
|
37
|
+
switch (state.kind) {
|
|
38
|
+
case 'AWAITING_FIRST_FIX': {
|
|
39
|
+
if (event.kind === 'FIX_PUSHED') {
|
|
40
|
+
return {
|
|
41
|
+
kind: 'REVIEWING',
|
|
42
|
+
iteration: 0,
|
|
43
|
+
headSha: event.headSha,
|
|
44
|
+
branch: event.branch,
|
|
45
|
+
prNumber: event.prNumber,
|
|
46
|
+
...header(state),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (event.kind === 'FIX_FAILED') {
|
|
50
|
+
return {
|
|
51
|
+
kind: 'DONE_NEEDS_HUMAN',
|
|
52
|
+
reason: event.reason,
|
|
53
|
+
findings: [],
|
|
54
|
+
...header(state),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return invariant(state, event);
|
|
58
|
+
}
|
|
59
|
+
case 'REVIEWING': {
|
|
60
|
+
if (event.kind === 'REVIEW_UNVERIFIED') {
|
|
61
|
+
return withMeta({
|
|
62
|
+
kind: 'DONE_NEEDS_HUMAN',
|
|
63
|
+
reason: `Review unverified: ${event.reason}`,
|
|
64
|
+
findings: [],
|
|
65
|
+
...header(state),
|
|
66
|
+
}, { prNumber: state.prNumber, branch: state.branch });
|
|
67
|
+
}
|
|
68
|
+
if (event.kind !== 'REVIEW_COMPLETE')
|
|
69
|
+
return invariant(state, event);
|
|
70
|
+
const blocking = blockers(event.findings);
|
|
71
|
+
if (blocking.length > 0) {
|
|
72
|
+
if (state.iteration + 1 < state.maxIterations) {
|
|
73
|
+
return {
|
|
74
|
+
kind: 'ITERATING',
|
|
75
|
+
iteration: state.iteration + 1,
|
|
76
|
+
findings: blocking,
|
|
77
|
+
prevHeadSha: state.headSha,
|
|
78
|
+
branch: state.branch,
|
|
79
|
+
prNumber: state.prNumber,
|
|
80
|
+
...header(state),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return withMeta({
|
|
84
|
+
kind: 'DONE_NEEDS_HUMAN',
|
|
85
|
+
reason: 'P0/P1 findings persisting after retry — manual review required',
|
|
86
|
+
findings: event.findings,
|
|
87
|
+
...header(state),
|
|
88
|
+
}, { prNumber: state.prNumber, branch: state.branch });
|
|
89
|
+
}
|
|
90
|
+
if (state.promotionPolicy.kind === 'doistbot_fix_ci') {
|
|
91
|
+
return {
|
|
92
|
+
kind: 'PUSHED_AWAITING_CI',
|
|
93
|
+
iteration: state.iteration,
|
|
94
|
+
headSha: state.headSha,
|
|
95
|
+
prNumber: state.prNumber,
|
|
96
|
+
branch: state.branch,
|
|
97
|
+
...header(state),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return withMeta({
|
|
101
|
+
kind: 'DONE_PR_UPDATED',
|
|
102
|
+
headSha: state.headSha,
|
|
103
|
+
...header(state),
|
|
104
|
+
}, { prNumber: state.prNumber, branch: state.branch });
|
|
105
|
+
}
|
|
106
|
+
case 'ITERATING': {
|
|
107
|
+
if (event.kind === 'FIX_PUSHED') {
|
|
108
|
+
return {
|
|
109
|
+
kind: 'REVIEWING',
|
|
110
|
+
iteration: state.iteration,
|
|
111
|
+
headSha: event.headSha,
|
|
112
|
+
reviewBaseSha: state.prevHeadSha,
|
|
113
|
+
branch: event.branch,
|
|
114
|
+
prNumber: event.prNumber,
|
|
115
|
+
...header(state),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (event.kind === 'FIX_FAILED') {
|
|
119
|
+
return withMeta({
|
|
120
|
+
kind: 'DONE_NEEDS_HUMAN',
|
|
121
|
+
reason: event.reason,
|
|
122
|
+
findings: state.findings,
|
|
123
|
+
...header(state),
|
|
124
|
+
}, { prNumber: state.prNumber, branch: state.branch });
|
|
125
|
+
}
|
|
126
|
+
return invariant(state, event);
|
|
127
|
+
}
|
|
128
|
+
case 'PUSHED_AWAITING_CI': {
|
|
129
|
+
if (event.kind !== 'CI_RESULT')
|
|
130
|
+
return invariant(state, event);
|
|
131
|
+
const status = event.status;
|
|
132
|
+
// Ignore CI results carrying a SHA other than our current head.
|
|
133
|
+
// GitHub can deliver a check_suite for an earlier push after we
|
|
134
|
+
// have already pushed an iter-1 commit, in which case acting on
|
|
135
|
+
// the stale verdict would promote or escalate based on a SHA we
|
|
136
|
+
// have already superseded.
|
|
137
|
+
if (status.headSha !== state.headSha) {
|
|
138
|
+
return state;
|
|
139
|
+
}
|
|
140
|
+
if (status.kind === 'passed') {
|
|
141
|
+
return withMeta({
|
|
142
|
+
kind: 'DONE_PROMOTED',
|
|
143
|
+
headSha: status.headSha,
|
|
144
|
+
...header(state),
|
|
145
|
+
}, { prNumber: state.prNumber, branch: state.branch });
|
|
146
|
+
}
|
|
147
|
+
if (status.kind === 'failed') {
|
|
148
|
+
const failed = status.failedChecks.map((c) => c.name).join(', ');
|
|
149
|
+
return withMeta({
|
|
150
|
+
kind: 'DONE_NEEDS_HUMAN',
|
|
151
|
+
reason: `CI failed: ${failed || 'unknown checks'}`,
|
|
152
|
+
findings: [],
|
|
153
|
+
...header(state),
|
|
154
|
+
}, { prNumber: state.prNumber, branch: state.branch });
|
|
155
|
+
}
|
|
156
|
+
if (status.kind === 'pending') {
|
|
157
|
+
return state;
|
|
158
|
+
}
|
|
159
|
+
return withMeta({
|
|
160
|
+
kind: 'DONE_NEEDS_HUMAN',
|
|
161
|
+
reason: 'No CI checks configured — manual review required',
|
|
162
|
+
findings: [],
|
|
163
|
+
...header(state),
|
|
164
|
+
}, { prNumber: state.prNumber, branch: state.branch });
|
|
165
|
+
}
|
|
166
|
+
case 'DONE_PROMOTED':
|
|
167
|
+
case 'DONE_PR_UPDATED':
|
|
168
|
+
case 'DONE_NEEDS_HUMAN':
|
|
169
|
+
return invariant(state, event);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { repositoryKey } from 'doistbot-issue-routing';
|
|
2
|
+
import { normalizeScopedLabelValue, scopedLabelValue } from '../../core/label-parsing.js';
|
|
3
|
+
import { APP_LABEL_ALIASES, findIntegrationRoute, findIssueRoute, findOwningRoutes, findRepositoryTarget as findRoutingRepositoryTarget, INTEGRATION_LABEL_ALIASES, PRODUCT_LABEL_ALIASES, PRODUCT_ROUTES, } from '../../core/repository-map.js';
|
|
4
|
+
const HERO_GROUPS = {
|
|
5
|
+
'android-hero': { key: 'android-hero', mention: '@todoist/android-hero' },
|
|
6
|
+
'app-copy-hero': { key: 'app-copy-hero', mention: '@todoist/app-copy-hero' },
|
|
7
|
+
'apple-hero': { key: 'apple-hero', mention: '@todoist/apple-hero' },
|
|
8
|
+
'backend-hero': { key: 'backend-hero', mention: '@todoist/backend-hero' },
|
|
9
|
+
'brand-dev-hero': { key: 'brand-dev-hero', mention: '@todoist/brand-dev-hero' },
|
|
10
|
+
'cx-hero': { key: 'cx-hero', mention: '@todoist/cx-hero' },
|
|
11
|
+
dateist: { key: 'dateist', mention: '@amix' },
|
|
12
|
+
'design-brand-hero': { key: 'design-brand-hero', mention: '@todoist/design-brand-hero' },
|
|
13
|
+
'design-product-hero': {
|
|
14
|
+
key: 'design-product-hero',
|
|
15
|
+
mention: '@todoist/design-product-hero',
|
|
16
|
+
},
|
|
17
|
+
'desktop-hero': { key: 'desktop-hero', mention: '@todoist/desktop-hero' },
|
|
18
|
+
finance: { key: 'finance', mention: '@todoist/finance' },
|
|
19
|
+
'frontend-hero': { key: 'frontend-hero', mention: '@todoist/frontend-hero' },
|
|
20
|
+
platform: { key: 'platform', mention: '@todoist/platform' },
|
|
21
|
+
'product-hero': { key: 'product-hero', mention: '@todoist/product-hero' },
|
|
22
|
+
};
|
|
23
|
+
// Mirrored from Doist/Issues/.github/workflows/label-form-reports.yml on 2026-04-17.
|
|
24
|
+
const TEAM_LABEL_TO_HERO = {
|
|
25
|
+
android: 'android-hero',
|
|
26
|
+
appcopy: 'app-copy-hero',
|
|
27
|
+
'app-copy': 'app-copy-hero',
|
|
28
|
+
apple: 'apple-hero',
|
|
29
|
+
backend: 'backend-hero',
|
|
30
|
+
brand: 'design-brand-hero',
|
|
31
|
+
branddev: 'brand-dev-hero',
|
|
32
|
+
'brand-dev': 'brand-dev-hero',
|
|
33
|
+
dateist: 'dateist',
|
|
34
|
+
desktop: 'desktop-hero',
|
|
35
|
+
designproduct: 'design-product-hero',
|
|
36
|
+
'design-product': 'design-product-hero',
|
|
37
|
+
finance: 'finance',
|
|
38
|
+
frontend: 'frontend-hero',
|
|
39
|
+
platform: 'platform',
|
|
40
|
+
product: 'product-hero',
|
|
41
|
+
support: 'cx-hero',
|
|
42
|
+
};
|
|
43
|
+
export const CX_HERO_MANUAL_HANDLE = HERO_GROUPS['cx-hero'].mention;
|
|
44
|
+
/**
|
|
45
|
+
* Looks up a repository supported by issue routing.
|
|
46
|
+
*/
|
|
47
|
+
export function findRepositoryTarget(owner, repo) {
|
|
48
|
+
return findRoutingRepositoryTarget(owner, repo);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Derives the GitHub team slug for a PR from its configured issue route.
|
|
52
|
+
* Repositories that are unknown or belong to routes with different hero teams
|
|
53
|
+
* use cx-hero because the repository alone does not give enough context.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveHeroTeamSlug(target) {
|
|
56
|
+
const group = resolveHeroGroup([], target);
|
|
57
|
+
if (!group || group.key === 'dateist') {
|
|
58
|
+
return 'cx-hero';
|
|
59
|
+
}
|
|
60
|
+
// The check above excluded `dateist`, so TS narrows group.key from
|
|
61
|
+
// HeroGroupKey to HeroTeamSlug here.
|
|
62
|
+
return group.key;
|
|
63
|
+
}
|
|
64
|
+
function resolveAppLabelAlias(value) {
|
|
65
|
+
return APP_LABEL_ALIASES[normalizeScopedLabelValue(value)];
|
|
66
|
+
}
|
|
67
|
+
function resolveIntegrationLabelAlias(value) {
|
|
68
|
+
return INTEGRATION_LABEL_ALIASES[normalizeScopedLabelValue(value)];
|
|
69
|
+
}
|
|
70
|
+
export function isKnownHeroRoutingLabel(label) {
|
|
71
|
+
const teamValue = scopedLabelValue(label, 'Team');
|
|
72
|
+
if (teamValue && TEAM_LABEL_TO_HERO[normalizeScopedLabelValue(teamValue)]) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
const integrationValue = scopedLabelValue(label, 'Integration');
|
|
76
|
+
if (integrationValue && resolveIntegrationLabelAlias(integrationValue)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
const appValue = scopedLabelValue(label, 'App');
|
|
80
|
+
if (!appValue) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const appLabel = resolveAppLabelAlias(appValue);
|
|
84
|
+
return (appLabel !== undefined &&
|
|
85
|
+
appLabel !== 'all' &&
|
|
86
|
+
Object.values(PRODUCT_ROUTES).some((routes) => routes.some((route) => route.id === appLabel)));
|
|
87
|
+
}
|
|
88
|
+
function resolveProduct(labels) {
|
|
89
|
+
const products = new Set();
|
|
90
|
+
for (const label of labels) {
|
|
91
|
+
const productValue = scopedLabelValue(label, 'Product');
|
|
92
|
+
if (!productValue) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const product = PRODUCT_LABEL_ALIASES[normalizeScopedLabelValue(productValue)];
|
|
96
|
+
if (product) {
|
|
97
|
+
products.add(product);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return products.size === 1 ? products.values().next().value : undefined;
|
|
101
|
+
}
|
|
102
|
+
function resolveIntegrationHeroKeys(labels) {
|
|
103
|
+
const product = resolveProduct(labels);
|
|
104
|
+
const heroKeys = new Set();
|
|
105
|
+
for (const label of labels) {
|
|
106
|
+
const integrationValue = scopedLabelValue(label, 'Integration');
|
|
107
|
+
if (!integrationValue) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const integration = resolveIntegrationLabelAlias(integrationValue);
|
|
111
|
+
if (!integration) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const route = findIntegrationRoute(integration);
|
|
115
|
+
// An integration from another product is not in scope for this issue, so
|
|
116
|
+
// it must not contribute a hero the repository resolver would never pick.
|
|
117
|
+
if (product && route.product !== product) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
heroKeys.add(route.heroTeam);
|
|
121
|
+
}
|
|
122
|
+
return heroKeys;
|
|
123
|
+
}
|
|
124
|
+
function resolveAppRouteHeroKeys(labels) {
|
|
125
|
+
const routeIds = new Set();
|
|
126
|
+
for (const label of labels) {
|
|
127
|
+
const appValue = scopedLabelValue(label, 'App');
|
|
128
|
+
if (!appValue) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const appLabel = resolveAppLabelAlias(appValue);
|
|
132
|
+
// `all` spans every route and `integration` names no route of its own —
|
|
133
|
+
// the accompanying `Integration:*` label supplies that hero instead.
|
|
134
|
+
if (appLabel && appLabel !== 'all' && appLabel !== 'integration') {
|
|
135
|
+
routeIds.add(appLabel);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const product = resolveProduct(labels);
|
|
139
|
+
const heroKeys = new Set();
|
|
140
|
+
for (const routeId of routeIds) {
|
|
141
|
+
const routes = product
|
|
142
|
+
? [findIssueRoute(product, routeId)].filter((route) => route !== undefined)
|
|
143
|
+
: Object.values(PRODUCT_ROUTES)
|
|
144
|
+
.flat()
|
|
145
|
+
.filter((route) => route.id === routeId);
|
|
146
|
+
for (const route of routes) {
|
|
147
|
+
heroKeys.add(route.heroTeam);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return heroKeys;
|
|
151
|
+
}
|
|
152
|
+
export function resolveHeroGroup(labels, primaryRepo) {
|
|
153
|
+
const teamHeroKeys = new Set();
|
|
154
|
+
for (const label of labels) {
|
|
155
|
+
const teamValue = scopedLabelValue(label, 'Team');
|
|
156
|
+
if (!teamValue) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const heroKey = TEAM_LABEL_TO_HERO[normalizeScopedLabelValue(teamValue)];
|
|
160
|
+
if (heroKey) {
|
|
161
|
+
teamHeroKeys.add(heroKey);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (teamHeroKeys.size === 1) {
|
|
165
|
+
return HERO_GROUPS[teamHeroKeys.values().next().value];
|
|
166
|
+
}
|
|
167
|
+
if (teamHeroKeys.size > 1) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const integrationHeroKeys = resolveIntegrationHeroKeys(labels);
|
|
171
|
+
if (integrationHeroKeys.size === 1) {
|
|
172
|
+
return HERO_GROUPS[integrationHeroKeys.values().next().value];
|
|
173
|
+
}
|
|
174
|
+
if (integrationHeroKeys.size > 1) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const appHeroKeys = resolveAppRouteHeroKeys(labels);
|
|
178
|
+
if (appHeroKeys.size === 1) {
|
|
179
|
+
return HERO_GROUPS[appHeroKeys.values().next().value];
|
|
180
|
+
}
|
|
181
|
+
if (appHeroKeys.size > 1) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
if (primaryRepo) {
|
|
185
|
+
const repositoryHeroKeys = new Set(findOwningRoutes(primaryRepo).map((route) => route.heroTeam));
|
|
186
|
+
if (repositoryHeroKeys.size === 1) {
|
|
187
|
+
return HERO_GROUPS[repositoryHeroKeys.values().next().value];
|
|
188
|
+
}
|
|
189
|
+
if (repositoryHeroKeys.size > 1) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
/** The immutable branch retains Sentry ownership when a library has several owners. */
|
|
196
|
+
export function sentryBranchHeroTeam(branch, target) {
|
|
197
|
+
if (!branch.startsWith('doistbot/auto-fix/sentry-'))
|
|
198
|
+
return undefined;
|
|
199
|
+
const match = /^doistbot\/auto-fix\/sentry-(todoist|comms)-([a-z]+)-[a-z0-9][a-z0-9-]*-[1-9][0-9]*-[1-9][0-9]*-[0-9]+$/.exec(branch);
|
|
200
|
+
const route = match
|
|
201
|
+
? findIssueRoute(match[1] === 'todoist' ? 'Todoist' : 'Comms', match[2])
|
|
202
|
+
: undefined;
|
|
203
|
+
if (!route?.repositories.some((repo) => repositoryKey(repo.fullName) === repositoryKey(target.fullName)))
|
|
204
|
+
throw new Error('Sentry branch route does not match the target repository');
|
|
205
|
+
return route.heroTeam;
|
|
206
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const HERO_AUTOFIX_MARKER = '<!--doistbot-autofix-followup-->';
|
|
2
|
+
export const ASSIGNEE_REVIEW_MARKER = '<!--doistbot-assignee-review-requested-->';
|
|
3
|
+
export const FIX_LOOP_START_MARKER = '<!--doistbot-fixloop-start-->';
|
|
4
|
+
export const AUTOFIX_FAILURE_MARKER = '<!--doistbot-autofix-failure-->';
|
|
5
|
+
export const TRIAGE_MARKER = '<!--doistbot-issue-triage-->';
|
|
6
|
+
export const MAX_TRIAGE_COMMENT_CHARS = 60_000;
|