From c2a9336519379642d6823c8253208eeef44847ae Mon Sep 17 00:00:00 2001 From: David Poll Date: Sun, 11 Jan 2026 12:03:05 -0800 Subject: [PATCH 1/6] feat: update database and runner directory structure for consistency and migration support --- backend/src/db/schema.ts | 36 ++++++++++++++++++++++++--- backend/src/services/runnerManager.ts | 5 +++- backend/tests/setup.ts | 11 ++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index cfb8025..b1656aa 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -6,13 +6,43 @@ import Database, { type Database as DatabaseType } from 'better-sqlite3'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; +import os from 'os'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// Database file location - store in data directory -const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', '..', 'data'); -const DB_PATH = path.join(DATA_DIR, 'action-packer.db'); +// Database file location +// +// Important: runners default to ~/.action-packer/runners (see runnerManager). +// To avoid multi-instance corruption (different DB + shared runner dirs), the +// default DB location should live under the same base directory. +// +// Operators can override with DATA_DIR (or ACTION_PACKER_HOME). +const ACTION_PACKER_HOME = + process.env.ACTION_PACKER_HOME || path.join(os.homedir(), '.action-packer'); +const DEFAULT_DATA_DIR = path.join(ACTION_PACKER_HOME, 'data'); + +// Legacy default (repo-local). Kept for one-time migration. +const LEGACY_DATA_DIR = path.join(__dirname, '..', '..', 'data'); +const LEGACY_DB_PATH = path.join(LEGACY_DATA_DIR, 'action-packer.db'); + +let DATA_DIR = process.env.DATA_DIR || DEFAULT_DATA_DIR; +let DB_PATH = path.join(DATA_DIR, 'action-packer.db'); + +// If DATA_DIR is not explicitly set, migrate the legacy DB into the new default +// location the first time we run. +if (!process.env.DATA_DIR) { + const newDbPath = path.join(DEFAULT_DATA_DIR, 'action-packer.db'); + if (!fs.existsSync(newDbPath) && fs.existsSync(LEGACY_DB_PATH)) { + fs.mkdirSync(DEFAULT_DATA_DIR, { recursive: true }); + fs.copyFileSync(LEGACY_DB_PATH, newDbPath); + // Log to aid debugging during upgrades. + console.log(`[db] Migrated legacy database to ${newDbPath}`); + } + + DATA_DIR = DEFAULT_DATA_DIR; + DB_PATH = newDbPath; +} // Ensure data directory exists if (!fs.existsSync(DATA_DIR)) { diff --git a/backend/src/services/runnerManager.ts b/backend/src/services/runnerManager.ts index 0223445..60e57c6 100644 --- a/backend/src/services/runnerManager.ts +++ b/backend/src/services/runnerManager.ts @@ -18,7 +18,10 @@ import { type GitHubRunnerDownload } from './github.js'; import { createClientFromCredentialId, resolveCredentialById } from './credentialResolver.js'; // Runner storage directory -export const RUNNERS_DIR = process.env.RUNNERS_DIR || path.join(os.homedir(), '.action-packer', 'runners'); +// Keep defaults consistent with DB default location (see db/schema.ts). +export const ACTION_PACKER_HOME = + process.env.ACTION_PACKER_HOME || path.join(os.homedir(), '.action-packer'); +export const RUNNERS_DIR = process.env.RUNNERS_DIR || path.join(ACTION_PACKER_HOME, 'runners'); /** * Get the cache directory paths for a specific runner. diff --git a/backend/tests/setup.ts b/backend/tests/setup.ts index 075da4c..5a23a22 100644 --- a/backend/tests/setup.ts +++ b/backend/tests/setup.ts @@ -2,12 +2,23 @@ * Test setup and utilities */ +import os from 'node:os'; +import path from 'node:path'; +import fs from 'node:fs/promises'; import { vi } from 'vitest'; // Mock environment for tests process.env.ENCRYPTION_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; process.env.NODE_ENV = 'test'; +// Keep tests isolated from any developer/host Action Packer state. +process.env.ACTION_PACKER_HOME = path.join(os.tmpdir(), `action-packer-test-home-${process.pid}`); +process.env.DATA_DIR = path.join(process.env.ACTION_PACKER_HOME, 'data'); + +// Best-effort cleanup from previous runs (same PID reuse is rare, but cheap to handle). +await fs.rm(process.env.ACTION_PACKER_HOME, { recursive: true, force: true }).catch(() => {}); +await fs.mkdir(process.env.DATA_DIR, { recursive: true }); + // Mock GitHub client vi.mock('../src/services/github.js', () => ({ createGitHubClient: vi.fn(() => ({ From cfc3c90a80b75e9ffd68dc3a2dc0ed5427db2b4b Mon Sep 17 00:00:00 2001 From: David Poll Date: Sun, 11 Jan 2026 21:14:48 -0800 Subject: [PATCH 2/6] Harden DB migration and dedupe ACTION_PACKER_HOME --- backend/src/config/paths.ts | 14 +++++++ backend/src/db/schema.ts | 53 +++++++++++++++++++++------ backend/src/services/runnerManager.ts | 6 +-- 3 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 backend/src/config/paths.ts diff --git a/backend/src/config/paths.ts b/backend/src/config/paths.ts new file mode 100644 index 0000000..bf1a6fc --- /dev/null +++ b/backend/src/config/paths.ts @@ -0,0 +1,14 @@ +import os from 'node:os'; +import path from 'node:path'; + +export function getActionPackerHome(): string { + return process.env.ACTION_PACKER_HOME || path.join(os.homedir(), '.action-packer'); +} + +export function getDefaultDataDir(actionPackerHome: string = getActionPackerHome()): string { + return path.join(actionPackerHome, 'data'); +} + +export function getDefaultRunnersDir(actionPackerHome: string = getActionPackerHome()): string { + return path.join(actionPackerHome, 'runners'); +} diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index b1656aa..8b3b596 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -6,7 +6,7 @@ import Database, { type Database as DatabaseType } from 'better-sqlite3'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; -import os from 'os'; +import { getActionPackerHome, getDefaultDataDir } from '../config/paths.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -18,30 +18,61 @@ const __dirname = path.dirname(__filename); // default DB location should live under the same base directory. // // Operators can override with DATA_DIR (or ACTION_PACKER_HOME). -const ACTION_PACKER_HOME = - process.env.ACTION_PACKER_HOME || path.join(os.homedir(), '.action-packer'); -const DEFAULT_DATA_DIR = path.join(ACTION_PACKER_HOME, 'data'); +const ACTION_PACKER_HOME = getActionPackerHome(); +const DEFAULT_DATA_DIR = getDefaultDataDir(ACTION_PACKER_HOME); // Legacy default (repo-local). Kept for one-time migration. const LEGACY_DATA_DIR = path.join(__dirname, '..', '..', 'data'); const LEGACY_DB_PATH = path.join(LEGACY_DATA_DIR, 'action-packer.db'); -let DATA_DIR = process.env.DATA_DIR || DEFAULT_DATA_DIR; -let DB_PATH = path.join(DATA_DIR, 'action-packer.db'); +let DATA_DIR: string; +let DB_PATH: string; // If DATA_DIR is not explicitly set, migrate the legacy DB into the new default // location the first time we run. if (!process.env.DATA_DIR) { const newDbPath = path.join(DEFAULT_DATA_DIR, 'action-packer.db'); - if (!fs.existsSync(newDbPath) && fs.existsSync(LEGACY_DB_PATH)) { - fs.mkdirSync(DEFAULT_DATA_DIR, { recursive: true }); - fs.copyFileSync(LEGACY_DB_PATH, newDbPath); - // Log to aid debugging during upgrades. - console.log(`[db] Migrated legacy database to ${newDbPath}`); + const shouldMigrate = !fs.existsSync(newDbPath) && fs.existsSync(LEGACY_DB_PATH); + + if (shouldMigrate) { + // Best-effort: be robust to multiple processes starting at once. + // Copy into a temporary file then atomically rename into place. + const tmpDbPath = `${newDbPath}.tmp-${process.pid}-${Date.now()}`; + + try { + fs.mkdirSync(DEFAULT_DATA_DIR, { recursive: true }); + fs.copyFileSync(LEGACY_DB_PATH, tmpDbPath); + + try { + fs.renameSync(tmpDbPath, newDbPath); + console.log(`[db] Migrated legacy database to ${newDbPath}`); + } catch (error) { + // Another process likely won the race. Remove our temp file. + try { + fs.unlinkSync(tmpDbPath); + } catch { + // ignore + } + + // If the destination still doesn't exist, rethrow so we don't silently + // proceed with a missing DB. + if (!fs.existsSync(newDbPath)) { + throw error; + } + } + } catch (error) { + console.error( + `[db] Failed to migrate legacy database from ${LEGACY_DB_PATH} to ${newDbPath}:`, + error + ); + } } DATA_DIR = DEFAULT_DATA_DIR; DB_PATH = newDbPath; +} else { + DATA_DIR = process.env.DATA_DIR; + DB_PATH = path.join(DATA_DIR, 'action-packer.db'); } // Ensure data directory exists diff --git a/backend/src/services/runnerManager.ts b/backend/src/services/runnerManager.ts index 60e57c6..971a281 100644 --- a/backend/src/services/runnerManager.ts +++ b/backend/src/services/runnerManager.ts @@ -16,12 +16,12 @@ import { v4 as uuidv4 } from 'uuid'; import { db, type RunnerRow } from '../db/index.js'; import { type GitHubRunnerDownload } from './github.js'; import { createClientFromCredentialId, resolveCredentialById } from './credentialResolver.js'; +import { getActionPackerHome, getDefaultRunnersDir } from '../config/paths.js'; // Runner storage directory // Keep defaults consistent with DB default location (see db/schema.ts). -export const ACTION_PACKER_HOME = - process.env.ACTION_PACKER_HOME || path.join(os.homedir(), '.action-packer'); -export const RUNNERS_DIR = process.env.RUNNERS_DIR || path.join(ACTION_PACKER_HOME, 'runners'); +export const ACTION_PACKER_HOME = getActionPackerHome(); +export const RUNNERS_DIR = process.env.RUNNERS_DIR || getDefaultRunnersDir(ACTION_PACKER_HOME); /** * Get the cache directory paths for a specific runner. From 312723563c484e6ce84b4e8d60d0d1e04aafb931 Mon Sep 17 00:00:00 2001 From: David Poll Date: Sun, 11 Jan 2026 21:21:54 -0800 Subject: [PATCH 3/6] Fall back to legacy DB path if migration fails --- backend/src/db/schema.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 8b3b596..fe5dab2 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -65,11 +65,17 @@ if (!process.env.DATA_DIR) { `[db] Failed to migrate legacy database from ${LEGACY_DB_PATH} to ${newDbPath}:`, error ); + // Fall back to legacy location so we don't start with a missing DB. + DATA_DIR = LEGACY_DATA_DIR; + DB_PATH = LEGACY_DB_PATH; } } - DATA_DIR = DEFAULT_DATA_DIR; - DB_PATH = newDbPath; + // Only update to new paths if we didn't fall back above. + if (!DATA_DIR) { + DATA_DIR = DEFAULT_DATA_DIR; + DB_PATH = newDbPath; + } } else { DATA_DIR = process.env.DATA_DIR; DB_PATH = path.join(DATA_DIR, 'action-packer.db'); From af3b5a466ca664e4a1f059d1b04ca4a43e298d77 Mon Sep 17 00:00:00 2001 From: David Poll Date: Sun, 11 Jan 2026 21:32:07 -0800 Subject: [PATCH 4/6] Fix TS2454: refactor path resolution to use helper function --- backend/src/db/schema.ts | 28 +++++++++++++-------------- backend/src/services/runnerManager.ts | 1 - 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index fe5dab2..54c11ca 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -25,12 +25,16 @@ const DEFAULT_DATA_DIR = getDefaultDataDir(ACTION_PACKER_HOME); const LEGACY_DATA_DIR = path.join(__dirname, '..', '..', 'data'); const LEGACY_DB_PATH = path.join(LEGACY_DATA_DIR, 'action-packer.db'); -let DATA_DIR: string; -let DB_PATH: string; +// Resolve DATA_DIR and DB_PATH, handling legacy migration if needed. +function resolvePaths(): { dataDir: string; dbPath: string } { + // If DATA_DIR is explicitly set, use it directly. + if (process.env.DATA_DIR) { + return { + dataDir: process.env.DATA_DIR, + dbPath: path.join(process.env.DATA_DIR, 'action-packer.db'), + }; + } -// If DATA_DIR is not explicitly set, migrate the legacy DB into the new default -// location the first time we run. -if (!process.env.DATA_DIR) { const newDbPath = path.join(DEFAULT_DATA_DIR, 'action-packer.db'); const shouldMigrate = !fs.existsSync(newDbPath) && fs.existsSync(LEGACY_DB_PATH); @@ -66,21 +70,15 @@ if (!process.env.DATA_DIR) { error ); // Fall back to legacy location so we don't start with a missing DB. - DATA_DIR = LEGACY_DATA_DIR; - DB_PATH = LEGACY_DB_PATH; + return { dataDir: LEGACY_DATA_DIR, dbPath: LEGACY_DB_PATH }; } } - // Only update to new paths if we didn't fall back above. - if (!DATA_DIR) { - DATA_DIR = DEFAULT_DATA_DIR; - DB_PATH = newDbPath; - } -} else { - DATA_DIR = process.env.DATA_DIR; - DB_PATH = path.join(DATA_DIR, 'action-packer.db'); + return { dataDir: DEFAULT_DATA_DIR, dbPath: newDbPath }; } +const { dataDir: DATA_DIR, dbPath: DB_PATH } = resolvePaths(); + // Ensure data directory exists if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); diff --git a/backend/src/services/runnerManager.ts b/backend/src/services/runnerManager.ts index 971a281..7b92eff 100644 --- a/backend/src/services/runnerManager.ts +++ b/backend/src/services/runnerManager.ts @@ -6,7 +6,6 @@ import { spawn, ChildProcess } from 'child_process'; import path from 'path'; import fs from 'fs/promises'; -import os from 'os'; import https from 'https'; import { createWriteStream } from 'fs'; import { pipeline } from 'stream/promises'; From a78268fe7fd14759ff52b2d3fa92208544a225e9 Mon Sep 17 00:00:00 2001 From: David Poll Date: Sun, 11 Jan 2026 21:40:24 -0800 Subject: [PATCH 5/6] Improve runnerManager docs; force CI rebuild --- backend/src/services/runnerManager.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/services/runnerManager.ts b/backend/src/services/runnerManager.ts index 7b92eff..ad4785b 100644 --- a/backend/src/services/runnerManager.ts +++ b/backend/src/services/runnerManager.ts @@ -1,9 +1,11 @@ /** * Native runner manager service - * Handles downloading, configuring, and managing GitHub Actions runners + * + * Handles downloading, configuring, and managing GitHub Actions runners. + * Uses shared path configuration from config/paths.ts. */ -import { spawn, ChildProcess } from 'child_process'; +import { spawn, type ChildProcess } from 'child_process'; import path from 'path'; import fs from 'fs/promises'; import https from 'https'; From ef2f202d51fc68b2612ad636956e9b739365cd98 Mon Sep 17 00:00:00 2001 From: David Poll Date: Sun, 11 Jan 2026 23:23:05 -0800 Subject: [PATCH 6/6] Fix missing os import and add getRunnerDownloads mock - Add back os import in runnerManager.ts (needed for os.homedir() in cleanupGlobalBuildCaches) - Add getRunnerDownloads mock to test setup (required by runner creation tests) --- backend/src/services/runnerManager.ts | 1 + backend/tests/setup.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/backend/src/services/runnerManager.ts b/backend/src/services/runnerManager.ts index ad4785b..6d7bbbd 100644 --- a/backend/src/services/runnerManager.ts +++ b/backend/src/services/runnerManager.ts @@ -8,6 +8,7 @@ import { spawn, type ChildProcess } from 'child_process'; import path from 'path'; import fs from 'fs/promises'; +import os from 'os'; import https from 'https'; import { createWriteStream } from 'fs'; import { pipeline } from 'stream/promises'; diff --git a/backend/tests/setup.ts b/backend/tests/setup.ts index 5a23a22..225d2cc 100644 --- a/backend/tests/setup.ts +++ b/backend/tests/setup.ts @@ -43,6 +43,15 @@ vi.mock('../src/services/github.js', () => ({ }), deleteRunner: vi.fn().mockResolvedValue(true), setRunnerLabels: vi.fn().mockResolvedValue([]), + getRunnerDownloads: vi.fn().mockResolvedValue([ + { + os: 'osx', + architecture: 'arm64', + download_url: 'https://github.com/actions/runner/releases/download/v2.300.0/actions-runner-osx-arm64-2.300.0.tar.gz', + filename: 'actions-runner-osx-arm64-2.300.0.tar.gz', + sha256_checksum: 'abc123', + }, + ]), listRepositories: vi.fn().mockResolvedValue({ total_count: 0, repositories: [],