Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions backend/src/config/paths.ts
Original file line number Diff line number Diff line change
@@ -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');
}
71 changes: 68 additions & 3 deletions backend/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
11 changes: 8 additions & 3 deletions backend/src/services/runnerManager.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions backend/tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => ({
Expand All @@ -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: [],
Expand Down