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 cfb8025..54c11ca 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -6,13 +6,78 @@ import Database, { type Database as DatabaseType } from 'better-sqlite3'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; +import { getActionPackerHome, getDefaultDataDir } from '../config/paths.js'; 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 = 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'); + +// 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'), + }; + } + + const newDbPath = path.join(DEFAULT_DATA_DIR, 'action-packer.db'); + 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 + ); + // Fall back to legacy location so we don't start with a missing DB. + return { dataDir: LEGACY_DATA_DIR, dbPath: LEGACY_DB_PATH }; + } + } + + return { dataDir: DEFAULT_DATA_DIR, dbPath: newDbPath }; +} + +const { dataDir: DATA_DIR, dbPath: DB_PATH } = resolvePaths(); // 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..6d7bbbd 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 os from 'os'; @@ -16,9 +18,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 -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 = getActionPackerHome(); +export const RUNNERS_DIR = process.env.RUNNERS_DIR || getDefaultRunnersDir(ACTION_PACKER_HOME); /** * Get the cache directory paths for a specific runner. diff --git a/backend/tests/setup.ts b/backend/tests/setup.ts index 075da4c..225d2cc 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(() => ({ @@ -32,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: [],