also looking at this
feat(ci): let @buddy fix-ci find the run a comment cannot name
#1413
10 files
+697
-121
| @@ -60,6 +60,16 @@ export interface FixOptions { | ||
| 60 | 60 | rerun?: () => Promise<boolean> |
| 61 | 61 | /** Diagnose and report without changing anything */ |
| 62 | 62 | dryRun?: boolean |
| 63 | /** | |
| 64 | * Diagnose without repairing, because the caller has nowhere to repair. | |
| 65 | * | |
| 66 | * Distinct from `dryRun`, which is a choice someone made. This is a | |
| 67 | * constraint: answering an `@buddy fix-ci` comment runs on the default | |
| 68 | * branch, so rewriting a lock file or letting the agent edit files would | |
| 69 | * change the wrong tree entirely. Re-running a flake is still fine — that is | |
| 70 | * an API call, not a change to a checkout. | |
| 71 | */ | |
| 72 | analysisOnly?: boolean | |
| 63 | 73 | logger?: Logger |
| 64 | 74 | } |
| 65 | 75 | |
| @@ -108,6 +118,29 @@ function lockfileReport(regenerated: boolean, pushed: boolean): string { | ||
| 108 | 118 | return 'Regenerated the lock file and pushed the result. No model was needed.' |
| 109 | 119 | } |
| 110 | 120 | |
| 121 | /** | |
| 122 | * Explain a diagnosis that could not be acted on from where it was made. | |
| 123 | * | |
| 124 | * Says which repair was available rather than only that none happened — a | |
| 125 | * report that stops at "I cannot fix this here" reads as a limitation, where | |
| 126 | * the useful version names the fix and who will apply it. | |
| 127 | * | |
| 128 | * @param failure - The classified failure | |
| 129 | */ | |
| 130 | function analysisOnlyNote(failure: ClassifiedFailure): string { | |
| 131 | const shared = 'Repairs run on the pull request\'s branch, and answering a comment runs on the default branch, ' | |
| 132 | + 'so anything I changed here would land on the wrong tree. ' | |
| 133 | ||
| 134 | if (failure.kind === 'lockfile-drift') { | |
| 135 | return `Regenerating the lock file would fix this, and no model would be needed. ${shared}` | |
| 136 | + 'The `fix-ci` job will do it on the next failing run, or you can run ' | |
| 137 | + '`buddy fix-ci --run-id <id> --pr <number>` yourself.' | |
| 138 | } | |
| 139 | ||
| 140 | return `${shared}The \`fix-ci\` job runs on the failing run itself and can repair this; ` | |
| 141 | + 'this reply is the diagnosis it would start from.' | |
| 142 | } | |
| 143 | ||
| 111 | 144 | export async function attemptFix(options: FixOptions): Promise<FixOutcome> { |
| 112 | 145 | const logger = options.logger ?? getDefaultLogger() |
| 113 | 146 | const failure = classifyFailure(options.log) |
| @@ -140,6 +173,17 @@ export async function attemptFix(options: FixOptions): Promise<FixOutcome> { | ||
| 140 | 173 | } |
| 141 | 174 | } |
| 142 | 175 | |
| 176 | // Everything below either writes to the workspace or hands it to the agent. | |
| 177 | // A flake is the exception, and it is handled on its own further down. | |
| 178 | if (options.analysisOnly && failure.kind !== 'flake') { | |
| 179 | return { | |
| 180 | failure, | |
| 181 | action: 'reported', | |
| 182 | fixed: false, | |
| 183 | report: report(failure, analysisOnlyNote(failure)), | |
| 184 | } | |
| 185 | } | |
| 186 | ||
| 143 | 187 | if (failure.kind === 'lockfile-drift' && options.regenerateLockfile) { |
| 144 | 188 | if (options.dryRun) { |
| 145 | 189 | return { |