From 4900183b15deb8ab1e141ff07f9b983a9128f542 Mon Sep 17 00:00:00 2001 From: David Poll Date: Sun, 22 Feb 2026 01:40:36 -0800 Subject: [PATCH] Fix runtime provisioning regressions Avoid false amd64 mismatch failures by relying on Docker platform pinning at container create time, skip reconciler orphan cleanup for provisioning runners, and tolerate missing archive cleanup during concurrent runner lifecycle events. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- backend/src/services/dockerRunner.ts | 46 ++------------------------- backend/src/services/reconciler.ts | 9 ++++++ backend/src/services/runnerManager.ts | 7 +++- backend/tests/reconciler.test.ts | 13 ++++++++ 4 files changed, 30 insertions(+), 45 deletions(-) diff --git a/backend/src/services/dockerRunner.ts b/backend/src/services/dockerRunner.ts index 2aeb50d..19e4c9c 100644 --- a/backend/src/services/dockerRunner.ts +++ b/backend/src/services/dockerRunner.ts @@ -89,33 +89,6 @@ export async function pullRunnerImage( ): Promise { const d = initDocker(); const platform = `linux/${architecture}`; - - const splitImageRef = (ref: string): { repo: string; tag: string } => { - const lastColon = ref.lastIndexOf(':'); - const lastSlash = ref.lastIndexOf('/'); - if (lastColon > lastSlash) { - return { repo: ref.slice(0, lastColon), tag: ref.slice(lastColon + 1) }; - } - return { repo: ref, tag: 'latest' }; - }; - - const { repo, tag: originalTag } = splitImageRef(RUNNER_IMAGE); - const archTag = `${originalTag}-${architecture}`; - const platformTag = `${repo}:${archTag}`; - - // Check if we already have the platform-specific tag with correct architecture - try { - const existingImage = await d.getImage(platformTag).inspect(); - const imageArch = normalizeImageArchitecture(existingImage.Architecture); - - if (imageArch === architecture) { - console.log(`Image ${platformTag} already exists with correct architecture (${existingImage.Architecture})`); - return platformTag; - } - console.log(`Image ${platformTag} exists but has wrong architecture (${existingImage.Architecture}), re-pulling...`); - } catch { - // Image doesn't exist, need to pull - } console.log(`Pulling image ${RUNNER_IMAGE} for ${platform}...`); @@ -140,23 +113,8 @@ export async function pullRunnerImage( }); }); }); - - // Verify the pulled image has the correct architecture - const pulledImage = await d.getImage(RUNNER_IMAGE).inspect(); - console.log(`Pulled image architecture: ${pulledImage.Architecture}`); - assertImageArchitecture(RUNNER_IMAGE, pulledImage.Architecture, architecture); - - // Tag the pulled image with architecture-specific tag - console.log(`Tagging image as ${platformTag}...`); - const image = d.getImage(RUNNER_IMAGE); - await image.tag({ repo, tag: archTag }); - - // Verify the tagged image - const taggedImage = await d.getImage(platformTag).inspect(); - console.log(`Tagged image ${platformTag} architecture: ${taggedImage.Architecture}`); - assertImageArchitecture(platformTag, taggedImage.Architecture, architecture); - - return platformTag; + + return RUNNER_IMAGE; } /** diff --git a/backend/src/services/reconciler.ts b/backend/src/services/reconciler.ts index efb84a5..2518ed2 100644 --- a/backend/src/services/reconciler.ts +++ b/backend/src/services/reconciler.ts @@ -55,6 +55,10 @@ const getAllEnabledPools = db.prepare('SELECT * FROM runner_pools WHERE enabled const deleteRunner = db.prepare('DELETE FROM runners WHERE id = ?'); const updateRunnerStatus = db.prepare('UPDATE runners SET status = ?, updated_at = datetime(\'now\') WHERE id = ?'); +export function shouldSkipGitHubExistenceCheck(status: string): boolean { + return status === 'pending' || status === 'configuring'; +} + /** * Start the periodic reconciliation service */ @@ -157,6 +161,11 @@ async function reconcileRunnersInternal(): Promise { for (const runner of localRunners) { stats.checked++; + // Newly provisioning runners are expected to not exist in GitHub yet. + if (shouldSkipGitHubExistenceCheck(runner.status)) { + continue; + } + // Check if runner exists in GitHub (by ID or name) const existsInGitHub = (runner.github_runner_id && ghRunnerIds.has(runner.github_runner_id)) || diff --git a/backend/src/services/runnerManager.ts b/backend/src/services/runnerManager.ts index 6d7bbbd..f533675 100644 --- a/backend/src/services/runnerManager.ts +++ b/backend/src/services/runnerManager.ts @@ -441,7 +441,12 @@ export async function downloadRunner( } // Cleanup archive - await fs.unlink(archivePath); + await fs.unlink(archivePath).catch((error: unknown) => { + const code = (error as NodeJS.ErrnoException).code; + if (code !== 'ENOENT') { + throw error; + } + }); // Update runner directory in database updateRunnerDir.run(runnerDir, runnerId); diff --git a/backend/tests/reconciler.test.ts b/backend/tests/reconciler.test.ts index 5b507f1..7b42df6 100644 --- a/backend/tests/reconciler.test.ts +++ b/backend/tests/reconciler.test.ts @@ -7,6 +7,7 @@ import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { db } from '../src/db/index.js'; +import { shouldSkipGitHubExistenceCheck } from '../src/services/reconciler.js'; // Test directory for runner cleanup tests const TEST_RUNNERS_DIR = path.join(os.tmpdir(), 'action-packer-test-runners'); @@ -16,6 +17,18 @@ let cleanupOrphanedDirectories: () => Promise; let withTimeout: (promise: Promise, ms: number, operation: string) => Promise; describe('Reconciler', () => { + describe('shouldSkipGitHubExistenceCheck', () => { + it('skips pending and configuring runners', () => { + expect(shouldSkipGitHubExistenceCheck('pending')).toBe(true); + expect(shouldSkipGitHubExistenceCheck('configuring')).toBe(true); + }); + + it('does not skip online/offline runners', () => { + expect(shouldSkipGitHubExistenceCheck('online')).toBe(false); + expect(shouldSkipGitHubExistenceCheck('offline')).toBe(false); + }); + }); + describe('withTimeout', () => { beforeEach(async () => { // Dynamically import to get fresh module