Skip to content

Commit 6a654fb

Browse files
frostebiteclaude
andauthored
fix: mark parent job failed when build times out or is manually cleared (#98)
When cleanUpBuildsThatDidntReportBack marks a build as failed after the 6-hour GitHub Actions timeout, or manualCleanUp marks a build as failed, the parent job was left in its current state (typically inProgress). The Ingeminator only processes jobs in "failed" status, so these builds were never retried. Worse, with maxConcurrentJobs=9 the ghost-inProgress jobs consumed all queue capacity, blocking all 53 created jobs from being scheduled. Three-part fix: - cleanUpBuildsThatDidntReportBack: call markFailureForJob after marking build as failed so the Ingeminator picks it up next cycle - manualCleanUp: same fix for the manual path - transitionStaleInProgressJobsToFailed: new cleaner step that detects active jobs where all builds have settled (none are started) and transitions them to failed, recovering capacity for builds already stuck Adds CiBuilds.hasAnyStartedBuildsForJob to support the new step. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9bb8b01 commit 6a654fb

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

functions/src/logic/buildQueue/cleaner.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export class Cleaner {
1616
public static async cleanUp(latestRepoVersion: string) {
1717
this.buildsProcessed = 0;
1818
await this.requeueActiveJobsWithoutBuilds();
19+
await this.transitionStaleInProgressJobsToFailed();
1920
await this.recoverMaxedOutFailedBuilds(latestRepoVersion);
2021
await this.reconcileStartedBuildsThatMayHavePublished();
2122
await this.cleanUpBuildsThatDidntReportBack();
@@ -60,6 +61,40 @@ export class Cleaner {
6061
}
6162
}
6263

64+
/**
65+
* An inProgress job whose builds have all finished (none are "started") is
66+
* stuck — the job will never self-transition because only the build-report
67+
* path calls markFailureForJob. Move these to "failed" so the Ingeminator
68+
* can schedule retries and free up queue capacity for fresh jobs.
69+
*/
70+
private static async transitionStaleInProgressJobsToFailed() {
71+
const activeJobs = await CiJobs.getActiveJobs();
72+
const staleThresholdMs = this.activeJobWithoutBuildsAfterMinutes * 60 * 1000;
73+
74+
for (const activeJob of activeJobs) {
75+
if (this.buildsProcessed >= this.maxBuildsProcessedPerRun) return;
76+
77+
const { id: jobId, data: job } = activeJob;
78+
const lastTouchedSeconds = job.modifiedDate?.seconds || job.addedDate?.seconds;
79+
if (!lastTouchedSeconds) continue;
80+
81+
const ageMs = Date.now() - lastTouchedSeconds * 1000;
82+
if (ageMs < staleThresholdMs) continue;
83+
84+
const hasBuilds = await CiBuilds.hasAnyBuildsForJob(jobId);
85+
if (!hasBuilds) continue; // no builds at all — handled by requeueActiveJobsWithoutBuilds
86+
87+
const hasStartedBuilds = await CiBuilds.hasAnyStartedBuildsForJob(jobId);
88+
if (hasStartedBuilds) continue; // at least one build is still running
89+
90+
this.buildsProcessed += 1;
91+
await CiJobs.markFailureForJob(jobId);
92+
await Discord.sendAlert(
93+
`[Cleaner] Transitioned ${job.status} job "${jobId}" to failed: all its builds have settled with no started builds remaining.`,
94+
);
95+
}
96+
}
97+
6398
/**
6499
* Automatically recover maxed-out failed builds for the latest repo version.
65100
* If the image is already on DockerHub, mark it as published.
@@ -161,6 +196,7 @@ export class Cleaner {
161196
await CiBuilds.markBuildAsFailed(buildId, {
162197
reason: `[ManualCleanup] Build never reported back and image not found on DockerHub.`,
163198
});
199+
await CiJobs.markFailureForJob(jobId);
164200
continue;
165201
}
166202

@@ -315,6 +351,7 @@ export class Cleaner {
315351
await CiBuilds.markBuildAsFailed(buildId, {
316352
reason: markAsFailedMessage,
317353
});
354+
await CiJobs.markFailureForJob(jobId);
318355

319356
continue;
320357
}

functions/src/model/ciBuilds.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ export class CiBuilds {
126126
return snapshot.docs.length > 0;
127127
};
128128

129+
public static hasAnyStartedBuildsForJob = async (jobId: string): Promise<boolean> => {
130+
const snapshot = await db
131+
.collection(CiBuilds.collection)
132+
.where('relatedJobId', '==', jobId)
133+
.where('status', '==', 'started')
134+
.limit(1)
135+
.get();
136+
137+
return snapshot.docs.length > 0;
138+
};
139+
129140
/**
130141
* Registers a new build or handles duplicate dispatches gracefully.
131142
* Returns the existing status if the build is already in progress or published,

0 commit comments

Comments
 (0)