From 7826d58e8f4842f135df05a479518db4ffaa0d94 Mon Sep 17 00:00:00 2001 From: Dawid Harat Date: Thu, 2 Apr 2026 14:31:30 +0200 Subject: [PATCH 1/3] chore(tools): disable useSortedKeys in biome --- tools/biome.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/biome.json b/tools/biome.json index 6db43cf..17cd0ee 100644 --- a/tools/biome.json +++ b/tools/biome.json @@ -5,7 +5,7 @@ "source": { "organizeImports": "on", "useSortedAttributes": "on", - "useSortedKeys": "on", + "useSortedKeys": "off", "useSortedProperties": "on" } }, From 2a3461e6aa3e5fd0eb31a04aee7483b8b6131049 Mon Sep 17 00:00:00 2001 From: Dawid Harat Date: Thu, 2 Apr 2026 14:32:00 +0200 Subject: [PATCH 2/3] chore: lint --- packages/hydra/src/cli.ts | 10 ++++- packages/hydra/src/commands/create.ts | 10 +++-- packages/hydra/src/commands/index.ts | 2 +- packages/hydra/src/commands/init.ts | 38 ++++++++----------- .../hydra/src/commands/profile/default.ts | 10 ++--- packages/hydra/src/commands/profile/list.ts | 2 +- packages/hydra/src/commands/profile/remove.ts | 15 ++++---- packages/hydra/src/commands/remove.ts | 7 +++- packages/hydra/src/commands/start.ts | 7 +++- packages/hydra/src/commands/status.ts | 4 +- packages/hydra/src/commands/stop.ts | 7 +++- packages/hydra/src/constants.ts | 4 +- .../src/providers/GitHubRunnerProvider.ts | 22 ++++------- packages/hydra/src/providers/types.ts | 13 +------ packages/hydra/src/types.ts | 6 +-- packages/hydra/src/utils.ts | 13 +++---- packages/sisyphus/src/commands/version.ts | 1 - 17 files changed, 77 insertions(+), 94 deletions(-) diff --git a/packages/hydra/src/cli.ts b/packages/hydra/src/cli.ts index ce3f8ed..d86ad88 100644 --- a/packages/hydra/src/cli.ts +++ b/packages/hydra/src/cli.ts @@ -1,6 +1,14 @@ import { AbstractCLI, ConfigManager } from "@r5n/cli-core"; import { version } from "../package.json"; -import { CreateCommand, InitCommand, ProfileCommand, RemoveCommand, StartCommand, StatusCommand, StopCommand } from "./commands"; +import { + CreateCommand, + InitCommand, + ProfileCommand, + RemoveCommand, + StartCommand, + StatusCommand, + StopCommand, +} from "./commands"; import { CLI_BIN, DEFAULT_CONFIG, HYDRA_CONFIG_FILE } from "./constants"; import type { HydraConfig } from "./types"; diff --git a/packages/hydra/src/commands/create.ts b/packages/hydra/src/commands/create.ts index f069bdd..9d97687 100644 --- a/packages/hydra/src/commands/create.ts +++ b/packages/hydra/src/commands/create.ts @@ -1,5 +1,5 @@ import { join } from "node:path"; -import { Exit, color, log, positionals, spinner, text } from "@r5n/cli-core"; +import { color, Exit, log, positionals, spinner, text } from "@r5n/cli-core"; import { BaseCommand, type Ctx } from "../base-command"; import { createProvider } from "../providers"; import type { RunnerEntry } from "../types"; @@ -24,7 +24,9 @@ export class CreateCommand extends BaseCommand { const count = ctx.interactive ? await this.promptCount(profile.numberOfMachines) - : (ctx.positionals.count ? Number.parseInt(ctx.positionals.count, 10) : profile.numberOfMachines); + : ctx.positionals.count + ? Number.parseInt(ctx.positionals.count, 10) + : profile.numberOfMachines; if (!count || count < 1) { throw new Exit("Runner count must be at least 1"); @@ -73,7 +75,9 @@ export class CreateCommand extends BaseCommand { const isDefault = profileName === ctx.config.get("defaultProfile"); const profileSuffix = isDefault ? "" : ` ${profileName}`; - log.info(`${color.green("Created")} ${toCreate} runner(s) for "${profileName}". Run ${color.green(`hydra start${profileSuffix}`)} to start them.`); + log.info( + `${color.green("Created")} ${toCreate} runner(s) for "${profileName}". Run ${color.green(`hydra start${profileSuffix}`)} to start them.`, + ); } private async promptCount(defaultCount: number): Promise { diff --git a/packages/hydra/src/commands/index.ts b/packages/hydra/src/commands/index.ts index 9720220..d97292c 100644 --- a/packages/hydra/src/commands/index.ts +++ b/packages/hydra/src/commands/index.ts @@ -1,7 +1,7 @@ export { CreateCommand } from "./create"; export { InitCommand } from "./init"; -export { StatusCommand } from "./status"; export { ProfileCommand } from "./profile"; export { RemoveCommand } from "./remove"; export { StartCommand } from "./start"; +export { StatusCommand } from "./status"; export { StopCommand } from "./stop"; diff --git a/packages/hydra/src/commands/init.ts b/packages/hydra/src/commands/init.ts index 8a31c3c..0d7eafd 100644 --- a/packages/hydra/src/commands/init.ts +++ b/packages/hydra/src/commands/init.ts @@ -1,13 +1,11 @@ import { existsSync } from "node:fs"; import { mkdir } from "node:fs/promises"; -import { Cancel, Exit, args, color, confirm, group, note, positionals, text } from "@r5n/cli-core"; +import { args, Cancel, color, confirm, Exit, group, note, positionals, text } from "@r5n/cli-core"; import { BaseCommand, type Ctx } from "../base-command"; import { CLI_BIN, DEFAULT_PROFILE, RUNNERS_DIR, SHARED_DIR } from "../constants"; import type { Profile } from "../types"; -const initPositionals = positionals({ - url: { description: "GitHub repository or organization URL" }, -}); +const initPositionals = positionals({ url: { description: "GitHub repository or organization URL" } }); const initArgs = args({ force: { alias: "f", default: false, description: "Overwrite existing config", type: "boolean" }, @@ -27,9 +25,7 @@ export class InitCommand extends BaseCommand { prompts = true; async execute(ctx: InitCtx) { - const profileName = ctx.interactive - ? await this.promptProfileName(ctx) - : (ctx.args.profile ?? DEFAULT_PROFILE); + const profileName = ctx.interactive ? await this.promptProfileName(ctx) : (ctx.args.profile ?? DEFAULT_PROFILE); const isNewInit = !ctx.config.exists(); @@ -38,7 +34,10 @@ export class InitCommand extends BaseCommand { if (!ctx.interactive) { throw new Exit(`Profile "${profileName}" already exists. Use --force to overwrite.`); } - const overwrite = await confirm({ initialValue: false, message: `Profile "${profileName}" already exists. Overwrite?` }); + const overwrite = await confirm({ + initialValue: false, + message: `Profile "${profileName}" already exists. Overwrite?`, + }); if (!overwrite) return; } @@ -70,10 +69,7 @@ export class InitCommand extends BaseCommand { } private async promptProfileName(ctx: InitCtx): Promise { - return text({ - initialValue: ctx.args.profile ?? DEFAULT_PROFILE, - message: "Profile name", - }); + return text({ initialValue: ctx.args.profile ?? DEFAULT_PROFILE, message: "Profile name" }); } private async ensureDirectories() { @@ -102,12 +98,7 @@ export class InitCommand extends BaseCommand { return undefined; }, }), - name: () => - text({ - initialValue: "runner", - message: "Base name for runners", - placeholder: "runner", - }), + name: () => text({ initialValue: "runner", message: "Base name for runners", placeholder: "runner" }), runners: () => text({ initialValue: "1", @@ -121,12 +112,13 @@ export class InitCommand extends BaseCommand { }, }), labels: () => - text({ - message: "Additional labels (comma-separated, optional)", - placeholder: "self-hosted,macOS,ARM64", - }), + text({ message: "Additional labels (comma-separated, optional)", placeholder: "self-hosted,macOS,ARM64" }), + }, + { + onCancel: () => { + throw new Cancel(); + }, }, - { onCancel: () => { throw new Cancel(); } }, ); return { diff --git a/packages/hydra/src/commands/profile/default.ts b/packages/hydra/src/commands/profile/default.ts index d8e403e..1991285 100644 --- a/packages/hydra/src/commands/profile/default.ts +++ b/packages/hydra/src/commands/profile/default.ts @@ -1,10 +1,8 @@ -import { Exit, color, log, positionals } from "@r5n/cli-core"; +import { color, Exit, log, positionals } from "@r5n/cli-core"; import { BaseCommand, type Ctx } from "../../base-command"; import { resolveProfile, selectProfile } from "../../utils"; -const defaultPositionals = positionals({ - name: { description: "Profile name to set as default" }, -}); +const defaultPositionals = positionals({ name: { description: "Profile name to set as default" } }); type DefaultCtx = Ctx, typeof defaultPositionals>; @@ -14,9 +12,7 @@ export class ProfileDefaultCommand extends BaseCommand { positionals = defaultPositionals; async execute(ctx: DefaultCtx) { - const profileName = ctx.interactive - ? await selectProfile(ctx.config) - : ctx.positionals.name; + const profileName = ctx.interactive ? await selectProfile(ctx.config) : ctx.positionals.name; if (!profileName) { throw new Exit("Profile name is required", "Usage: hydra profile default "); diff --git a/packages/hydra/src/commands/profile/list.ts b/packages/hydra/src/commands/profile/list.ts index 070d5f2..f326951 100644 --- a/packages/hydra/src/commands/profile/list.ts +++ b/packages/hydra/src/commands/profile/list.ts @@ -1,4 +1,4 @@ -import { Exit, color, log, note } from "@r5n/cli-core"; +import { color, Exit, log, note } from "@r5n/cli-core"; import { BaseCommand, type Ctx } from "../../base-command"; import { createProvider } from "../../providers"; import type { Profile } from "../../types"; diff --git a/packages/hydra/src/commands/profile/remove.ts b/packages/hydra/src/commands/profile/remove.ts index 8126f38..deba70d 100644 --- a/packages/hydra/src/commands/profile/remove.ts +++ b/packages/hydra/src/commands/profile/remove.ts @@ -1,14 +1,12 @@ import { rm } from "node:fs/promises"; import { join } from "node:path"; -import { Exit, color, confirm, log, positionals, spinner } from "@r5n/cli-core"; +import { color, confirm, Exit, log, positionals, spinner } from "@r5n/cli-core"; import { BaseCommand, type Ctx } from "../../base-command"; import { createProvider } from "../../providers"; import type { Profile } from "../../types"; import { resolveProfile, selectProfile } from "../../utils"; -const removePositionals = positionals({ - name: { description: "Profile name to remove" }, -}); +const removePositionals = positionals({ name: { description: "Profile name to remove" } }); type RemoveCtx = Ctx, typeof removePositionals>; @@ -18,9 +16,7 @@ export class ProfileRemoveCommand extends BaseCommand { positionals = removePositionals; async execute(ctx: RemoveCtx) { - const profileName = ctx.interactive - ? await selectProfile(ctx.config) - : ctx.positionals.name; + const profileName = ctx.interactive ? await selectProfile(ctx.config) : ctx.positionals.name; if (!profileName) { throw new Exit("Profile name is required", "Usage: hydra profile remove "); @@ -38,7 +34,10 @@ export class ProfileRemoveCommand extends BaseCommand { }); if (!proceed) return; - await this.removeRunners(profile, profileRunners.map((e) => e.id)); + await this.removeRunners( + profile, + profileRunners.map((e) => e.id), + ); const remainingEntries = entries.filter((e) => e.profile !== profileName); ctx.config.set("runners", remainingEntries); } diff --git a/packages/hydra/src/commands/remove.ts b/packages/hydra/src/commands/remove.ts index 6191e9e..98db16d 100644 --- a/packages/hydra/src/commands/remove.ts +++ b/packages/hydra/src/commands/remove.ts @@ -1,6 +1,6 @@ import { rm } from "node:fs/promises"; import { join } from "node:path"; -import { Exit, color, log, multiselect, positionals, spinner } from "@r5n/cli-core"; +import { color, Exit, log, multiselect, positionals, spinner } from "@r5n/cli-core"; import { BaseCommand, type Ctx } from "../base-command"; import { createProvider } from "../providers"; import { resolveProfile, resolveRunnerIds, selectProfile } from "../utils"; @@ -30,7 +30,10 @@ export class RemoveCommand extends BaseCommand { const ids = ctx.interactive ? await this.promptRunnerSelection(profileEntries.map((e) => e.id)) - : resolveRunnerIds(ctx.positionals.ids ?? [], profileEntries.map((e) => e.id)); + : resolveRunnerIds( + ctx.positionals.ids ?? [], + profileEntries.map((e) => e.id), + ); const provider = createProvider(profile); const s = spinner(); diff --git a/packages/hydra/src/commands/start.ts b/packages/hydra/src/commands/start.ts index 5453754..5c0ca75 100644 --- a/packages/hydra/src/commands/start.ts +++ b/packages/hydra/src/commands/start.ts @@ -1,4 +1,4 @@ -import { Exit, color, log, multiselect, positionals, spinner } from "@r5n/cli-core"; +import { color, Exit, log, multiselect, positionals, spinner } from "@r5n/cli-core"; import { BaseCommand, type Ctx } from "../base-command"; import { createProvider } from "../providers"; import { resolveProfile, resolveRunnerIds, selectProfile } from "../utils"; @@ -39,7 +39,10 @@ export class StartCommand extends BaseCommand { const ids = ctx.interactive ? await this.promptRunnerSelection(stoppedIds) - : resolveRunnerIds(ctx.positionals.ids ?? [], profileEntries.map((e) => e.id)); + : resolveRunnerIds( + ctx.positionals.ids ?? [], + profileEntries.map((e) => e.id), + ); let started = 0; for (const id of ids) { diff --git a/packages/hydra/src/commands/status.ts b/packages/hydra/src/commands/status.ts index 39f2b5e..86185e9 100644 --- a/packages/hydra/src/commands/status.ts +++ b/packages/hydra/src/commands/status.ts @@ -1,7 +1,7 @@ -import { Exit, color, log } from "@r5n/cli-core"; +import { color, Exit, log } from "@r5n/cli-core"; import { BaseCommand, type Ctx } from "../base-command"; -import { createProvider } from "../providers"; import type { RunnerInfo } from "../providers"; +import { createProvider } from "../providers"; import type { Profile } from "../types"; type ListCtx = Ctx; diff --git a/packages/hydra/src/commands/stop.ts b/packages/hydra/src/commands/stop.ts index 472ea5a..cd97e69 100644 --- a/packages/hydra/src/commands/stop.ts +++ b/packages/hydra/src/commands/stop.ts @@ -1,4 +1,4 @@ -import { Exit, color, log, multiselect, positionals, spinner } from "@r5n/cli-core"; +import { color, Exit, log, multiselect, positionals, spinner } from "@r5n/cli-core"; import { BaseCommand, type Ctx } from "../base-command"; import { createProvider } from "../providers"; import { resolveProfile, resolveRunnerIds, selectProfile } from "../utils"; @@ -39,7 +39,10 @@ export class StopCommand extends BaseCommand { const ids = ctx.interactive ? await this.promptRunnerSelection(activeIds) - : resolveRunnerIds(ctx.positionals.ids ?? [], profileEntries.map((e) => e.id)); + : resolveRunnerIds( + ctx.positionals.ids ?? [], + profileEntries.map((e) => e.id), + ); let stopped = 0; for (const id of ids) { diff --git a/packages/hydra/src/constants.ts b/packages/hydra/src/constants.ts index 42dcd06..32d492e 100644 --- a/packages/hydra/src/constants.ts +++ b/packages/hydra/src/constants.ts @@ -9,6 +9,4 @@ export const RUNNERS_DIR = `${HYDRA_DIR}/runners`; export const DEFAULT_PROFILE = "default"; -export const DEFAULT_CONFIG: HydraConfig = { - profiles: {}, -}; +export const DEFAULT_CONFIG: HydraConfig = { profiles: {} }; diff --git a/packages/hydra/src/providers/GitHubRunnerProvider.ts b/packages/hydra/src/providers/GitHubRunnerProvider.ts index 94a892f..0a7cadc 100644 --- a/packages/hydra/src/providers/GitHubRunnerProvider.ts +++ b/packages/hydra/src/providers/GitHubRunnerProvider.ts @@ -1,5 +1,5 @@ import { existsSync } from "node:fs"; -import { cp, link, mkdir, readFile, readdir, symlink, unlink, writeFile } from "node:fs/promises"; +import { cp, link, mkdir, readdir, readFile, symlink, unlink, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import { SHARED_DIR } from "../constants"; import type { Profile } from "../types"; @@ -69,11 +69,7 @@ export class GitHubRunnerProvider implements RunnerProvider { const existingPid = await this.readPidFile(runnerDir); if (existingPid && this.isProcessRunning(existingPid)) continue; - const proc = Bun.spawn(["bash", "./run.sh"], { - cwd: resolve(runnerDir), - stderr: "pipe", - stdout: "pipe", - }); + const proc = Bun.spawn(["bash", "./run.sh"], { cwd: resolve(runnerDir), stderr: "pipe", stdout: "pipe" }); proc.unref(); await this.writePidFile(runnerDir, proc.pid); } @@ -117,13 +113,7 @@ export class GitHubRunnerProvider implements RunnerProvider { if (isRunning) status = "running"; else if (existsSync(join(runnerDir, ".runner"))) status = "registered"; - return { - directory: runnerDir, - id, - name, - pid: isRunning ? pid : undefined, - status, - }; + return { directory: runnerDir, id, name, pid: isRunning ? pid : undefined, status }; } private async ensureDownloaded(): Promise { @@ -166,7 +156,8 @@ export class GitHubRunnerProvider implements RunnerProvider { private async fetchRegistrationToken(): Promise { const { owner, repo } = this.parseUrl(this.profile.url); - const result = await Bun.$`gh api repos/${owner}/${repo}/actions/runners/registration-token --method POST --jq .token`.quiet(); + const result = + await Bun.$`gh api repos/${owner}/${repo}/actions/runners/registration-token --method POST --jq .token`.quiet(); const token = result.stdout.toString().trim(); if (!token) { @@ -183,7 +174,8 @@ export class GitHubRunnerProvider implements RunnerProvider { if (!url) throw new Error("Cannot determine GitHub URL from runner config"); const { owner, repo } = this.parseUrl(url); - const result = await Bun.$`gh api repos/${owner}/${repo}/actions/runners/remove-token --method POST --jq .token`.quiet(); + const result = + await Bun.$`gh api repos/${owner}/${repo}/actions/runners/remove-token --method POST --jq .token`.quiet(); return result.stdout.toString().trim(); } diff --git a/packages/hydra/src/providers/types.ts b/packages/hydra/src/providers/types.ts index 16243d6..773a441 100644 --- a/packages/hydra/src/providers/types.ts +++ b/packages/hydra/src/providers/types.ts @@ -1,17 +1,8 @@ export type RunnerStatus = "running" | "stopped" | "registered" | "unknown"; -export type RunnerInfo = { - id: string; - name: string; - directory: string; - status: RunnerStatus; - pid?: number; -}; +export type RunnerInfo = { id: string; name: string; directory: string; status: RunnerStatus; pid?: number }; -export type DownloadResult = { - version: string; - path: string; -}; +export type DownloadResult = { version: string; path: string }; export interface RunnerProvider { download(): Promise; diff --git a/packages/hydra/src/types.ts b/packages/hydra/src/types.ts index b52c8b8..85d8cb4 100644 --- a/packages/hydra/src/types.ts +++ b/packages/hydra/src/types.ts @@ -25,8 +25,4 @@ export type RunnerEntry = { createdAt: string; }; -export type HydraConfig = { - defaultProfile?: string; - profiles: Record; - runners?: RunnerEntry[]; -}; +export type HydraConfig = { defaultProfile?: string; profiles: Record; runners?: RunnerEntry[] }; diff --git a/packages/hydra/src/utils.ts b/packages/hydra/src/utils.ts index 800b897..4a19f87 100644 --- a/packages/hydra/src/utils.ts +++ b/packages/hydra/src/utils.ts @@ -1,5 +1,5 @@ -import { Exit, select } from "@r5n/cli-core"; import type { ConfigManager } from "@r5n/cli-core"; +import { Exit, select } from "@r5n/cli-core"; import { DEFAULT_PROFILE } from "./constants"; import type { HydraConfig, Profile } from "./types"; @@ -15,7 +15,10 @@ export function resolveRunnerIds(input: string[], available: string[]): string[] return input; } -export function resolveProfile(config: ConfigManager, positionalProfile?: string): { name: string; profile: Profile } { +export function resolveProfile( + config: ConfigManager, + positionalProfile?: string, +): { name: string; profile: Profile } { const profiles = config.get("profiles"); if (Object.keys(profiles).length === 0) { @@ -44,10 +47,6 @@ export async function selectProfile(config: ConfigManager): Promise return select({ message: "Select profile", - options: names.map((name) => ({ - label: name, - value: name, - hint: profiles[name]?.url, - })), + options: names.map((name) => ({ label: name, value: name, hint: profiles[name]?.url })), }); } diff --git a/packages/sisyphus/src/commands/version.ts b/packages/sisyphus/src/commands/version.ts index 01779df..cb76c5e 100644 --- a/packages/sisyphus/src/commands/version.ts +++ b/packages/sisyphus/src/commands/version.ts @@ -5,7 +5,6 @@ import { BUMP_COLORS, BumpType, nonEmpty, type Package, type StoneData } from ". import { CommitAnalyzer, StoneManager, WorkspaceScanner } from "../services"; import { findDependencyPackages } from "../utils"; -// biome-ignore assist/source/useSortedKeys: message must come first const versionPositionals = positionals({ message: { description: "Stone message (commit message)" }, description: { description: "Stone description (optional details)" }, From 1c6b0fc1dbb2f06e83703d8049e3add3490305da Mon Sep 17 00:00:00 2001 From: Dawid Harat Date: Sat, 4 Apr 2026 02:25:23 +0200 Subject: [PATCH 3/3] feat(hydra): add `update` command to upgrade runner binaries --- packages/hydra/src/cli.ts | 2 + packages/hydra/src/commands/index.ts | 1 + packages/hydra/src/commands/update.ts | 66 +++++++++++++++++++ .../src/providers/GitHubRunnerProvider.ts | 42 +++++++++++- packages/hydra/src/providers/types.ts | 2 + 5 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 packages/hydra/src/commands/update.ts diff --git a/packages/hydra/src/cli.ts b/packages/hydra/src/cli.ts index d86ad88..4157126 100644 --- a/packages/hydra/src/cli.ts +++ b/packages/hydra/src/cli.ts @@ -8,6 +8,7 @@ import { StartCommand, StatusCommand, StopCommand, + UpdateCommand, } from "./commands"; import { CLI_BIN, DEFAULT_CONFIG, HYDRA_CONFIG_FILE } from "./constants"; import type { HydraConfig } from "./types"; @@ -33,6 +34,7 @@ class HydraCLI extends AbstractCLI { new StopCommand(), new StatusCommand(), new RemoveCommand(), + new UpdateCommand(), new ProfileCommand(), ]); } diff --git a/packages/hydra/src/commands/index.ts b/packages/hydra/src/commands/index.ts index d97292c..9a5125d 100644 --- a/packages/hydra/src/commands/index.ts +++ b/packages/hydra/src/commands/index.ts @@ -5,3 +5,4 @@ export { RemoveCommand } from "./remove"; export { StartCommand } from "./start"; export { StatusCommand } from "./status"; export { StopCommand } from "./stop"; +export { UpdateCommand } from "./update"; diff --git a/packages/hydra/src/commands/update.ts b/packages/hydra/src/commands/update.ts new file mode 100644 index 0000000..e2e64d4 --- /dev/null +++ b/packages/hydra/src/commands/update.ts @@ -0,0 +1,66 @@ +import { color, Exit, log, spinner } from "@r5n/cli-core"; +import { BaseCommand, type Ctx } from "../base-command"; +import { createProvider } from "../providers"; +import type { Profile } from "../types"; + +type UpdateCtx = Ctx; + +export class UpdateCommand extends BaseCommand { + name = "update"; + description = "Update runners to the latest version"; + + async execute(ctx: UpdateCtx) { + const profiles = ctx.config.get("profiles"); + const entries = ctx.config.get("runners") ?? []; + + if (entries.length === 0) { + throw new Exit("No runners found", "Run hydra create to provision runners"); + } + + const s = spinner(); + const byProfile = Map.groupBy(entries, (e) => e.profile); + let currentVersion: string | null = null; + let latestVersion = ""; + let versionChecked = false; + + for (const [profileName, profileEntries] of byProfile) { + const profile = profiles[profileName] as Profile | undefined; + const firstRunner = profileEntries[0]; + if (!profile || !firstRunner) continue; + + const provider = createProvider(profile); + + if (!versionChecked) { + versionChecked = true; + s.start("Checking for updates..."); + const result = await provider.download(); + latestVersion = result.version; + currentVersion = await provider.currentVersion(firstRunner.id); + s.stop(`Current: v${currentVersion ?? "unknown"} | Latest: v${latestVersion}`); + + if (currentVersion === latestVersion) { + log.info(`Already on latest version ${color.dim(`(v${latestVersion})`)}`); + return; + } + } + + const statuses = await provider.list(); + const runningIds = new Set(statuses.filter((r) => r.status === "running").map((r) => r.id)); + + for (const entry of profileEntries) { + const wasRunning = runningIds.has(entry.id); + + s.start(`Updating ${entry.id}...`); + + if (wasRunning) await provider.stop([entry.id]); + await provider.update([entry.id]); + if (wasRunning) await provider.start([entry.id]); + + const suffix = wasRunning ? ` ${color.dim("(restarted)")}` : ""; + s.stop(`${color.green("✓")} ${entry.id}${suffix}`); + } + } + + log.info(`${color.green("Updated")} ${entries.length} runner(s) from v${currentVersion} to v${latestVersion}`); + } +} diff --git a/packages/hydra/src/providers/GitHubRunnerProvider.ts b/packages/hydra/src/providers/GitHubRunnerProvider.ts index 0a7cadc..0d7d35f 100644 --- a/packages/hydra/src/providers/GitHubRunnerProvider.ts +++ b/packages/hydra/src/providers/GitHubRunnerProvider.ts @@ -1,5 +1,5 @@ import { existsSync } from "node:fs"; -import { cp, link, mkdir, readdir, readFile, symlink, unlink, writeFile } from "node:fs/promises"; +import { cp, link, mkdir, readdir, readFile, readlink, rm, symlink, unlink, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import { SHARED_DIR } from "../constants"; import type { Profile } from "../types"; @@ -104,6 +104,21 @@ export class GitHubRunnerProvider implements RunnerProvider { return runners; } + async currentVersion(id: string): Promise { + const runnerDir = join(this.profile.directory, id); + return this.detectVersion(runnerDir); + } + + async update(ids: string[]): Promise { + const sharedPath = await this.ensureDownloaded(); + + for (const id of ids) { + const runnerDir = join(this.profile.directory, id); + await this.removeRunnerBinaries(runnerDir); + await this.setupRunnerDir(sharedPath, runnerDir); + } + } + private async getRunnerStatus(runnerDir: string, id: string): Promise { const name = await this.readRunnerName(runnerDir); const pid = await this.readPidFile(runnerDir); @@ -240,6 +255,31 @@ export class GitHubRunnerProvider implements RunnerProvider { } } + private async detectVersion(runnerDir: string): Promise { + try { + const target = await readlink(join(runnerDir, "externals")); + const match = target.match(/github\/([^/]+)/); + return match?.[1] ?? null; + } catch { + return null; + } + } + + private async removeRunnerBinaries(runnerDir: string) { + for (const dir of HARDLINK_DIRS) { + await rm(join(runnerDir, dir), { force: true, recursive: true }); + } + for (const dir of SYMLINK_DIRS) { + await rm(join(runnerDir, dir), { force: true }); + } + for (const pattern of ["*.sh", "*.sh.template"]) { + const files = await Array.fromAsync(new Bun.Glob(pattern).scan(runnerDir)); + for (const file of files) { + await rm(join(runnerDir, file), { force: true }); + } + } + } + private stripBom(content: string) { return content.replace(/^\uFEFF/, ""); } diff --git a/packages/hydra/src/providers/types.ts b/packages/hydra/src/providers/types.ts index 773a441..2bc6003 100644 --- a/packages/hydra/src/providers/types.ts +++ b/packages/hydra/src/providers/types.ts @@ -11,4 +11,6 @@ export interface RunnerProvider { start(ids: string[]): Promise; stop(ids: string[]): Promise; list(): Promise; + currentVersion(id: string): Promise; + update(ids: string[]): Promise; }