ReviewOS

also looking at this

stacks/buddy

fix(ci): land the lock-file repair instead of describing it

#1408
Merged glennmichael123 wants to merge fix/persist-the-mechanical-lockfile-repair into main
6 files +193 -15
src/ci/fix.tsmodified+53-10
Changes to src/ci/fix.ts
@@ -35,8 +35,15 @@ export interface FixOptions {
3535 maxAttempts?: number
3636 /** Client for agent-driven repair; absent means analysis only */
3737 ai?: AiClient | null
38 /** Run the mechanical repair for a stale lock file */
39 regenerateLockfile?: () => Promise<boolean>
38 /**
39 * Run the mechanical repair for a stale lock file.
40 *
41 * Returns what actually happened, because regenerating a file and landing it
42 * on the branch are different outcomes and the report distinguishes them.
43 */
44 regenerateLockfile?: () => Promise<LockfileRepair>
45 /** Diagnose and report without changing anything */
46 dryRun?: boolean
4047 logger?: Logger
4148}
4249
@@ -56,6 +63,35 @@ export interface FixOptions {
5663 * await provider.createComment(pr, outcome.report)
5764 * ```
5865 */
66/** What the mechanical lock-file repair managed to do. */
67export interface LockfileRepair {
68 /** The lock file was rewritten in the workspace */
69 regenerated: boolean
70 /** The rewritten file was committed and pushed to the branch */
71 pushed: boolean
72}
73
74/**
75 * Describe a lock-file repair without overstating it.
76 *
77 * This message claimed "and pushed the result" long before anything pushed
78 * anything, which is the worst kind of report: confident, specific and wrong.
79 *
80 * @param regenerated - Whether the file was rewritten
81 * @param pushed - Whether the rewrite reached the branch
82 */
83function lockfileReport(regenerated: boolean, pushed: boolean): string {
84 if (!regenerated)
85 return 'Tried to regenerate the lock file, but the install did not succeed.'
86
87 if (!pushed) {
88 return 'Regenerated the lock file, but could not push it to the branch. '
89 + 'The change is in the workspace only, so the run will still be red until someone commits it.'
90 }
91
92 return 'Regenerated the lock file and pushed the result. No model was needed.'
93}
94
5995export async function attemptFix(options: FixOptions): Promise<FixOutcome> {
6096 const logger = options.logger ?? getDefaultLogger()
6197 const failure = classifyFailure(options.log)
@@ -85,19 +121,26 @@ export async function attemptFix(options: FixOptions): Promise<FixOutcome> {
85121 }
86122
87123 if (failure.kind === 'lockfile-drift' && options.regenerateLockfile) {
124 if (options.dryRun) {
125 return {
126 failure,
127 action: 'mechanical-fix',
128 fixed: false,
129 report: report(failure, 'Regenerating the lock file would fix this. No model would be needed. '
130 + 'Nothing was changed, because this was a dry run.'),
131 }
132 }
133
88134 logger.info('🔧 Regenerating the lock file')
89 const regenerated = await options.regenerateLockfile()
135 const { regenerated, pushed } = await options.regenerateLockfile()
90136
91137 return {
92138 failure,
93139 action: 'mechanical-fix',
94 fixed: regenerated,
95 report: report(
96 failure,
97 regenerated
98 ? 'Regenerated the lock file and pushed the result. No model was needed.'
99 : 'Tried to regenerate the lock file, but the install did not succeed.',
100 ),
140 // Only a pushed fix has actually repaired the run. A regenerated file
141 // sitting in a workspace that is about to be discarded has not.
142 fixed: pushed,
143 report: report(failure, lockfileReport(regenerated, pushed)),
101144 }
102145 }
103146