fix(ci): resolve flaky api-command module resolution and migrate to self-hosted runner

Root cause: Bun's test runner fails to resolve exports from directory
index.ts files during parallel test execution. The api-command/index.ts
imports 8 subcommand modules — if any fail to load during concurrent
test runs, the entire module fails with "Export not found."

Fix: extract createApiCommandHandler into handler.ts (lightweight, only
imports named-command-router). Test imports from handler.ts directly,
bypassing the heavy index.ts that loads all subcommand modules.

Also:
- Remove stale api-command.ts barrel file
- Use explicit /index import paths in source
- Migrate CI validate to self-hosted runner with clean artifact step
- Disable Bun cache (no-cache: true) on self-hosted runner
This commit is contained in:
Tam Nhu Tran
2026-03-26 18:12:32 -04:00
parent 57019c1105
commit 41a31c8a4f
7 changed files with 62 additions and 57 deletions
+5 -1
View File
@@ -6,16 +6,20 @@ on:
jobs:
validate:
runs-on: ubuntu-latest
runs-on: [self-hosted, linux, x64]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Clean stale artifacts
run: rm -rf node_modules ui/node_modules dist
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3.9'
no-cache: true
- name: Setup Node.js
uses: actions/setup-node@v4
-1
View File
@@ -1 +0,0 @@
export * from './api-command/index';
+51
View File
@@ -0,0 +1,51 @@
import { dispatchNamedCommand, type NamedCommandRoute } from '../named-command-router';
type ApiCommandHandler = (args: string[]) => Promise<void>;
type ApiCommandHelpHandler = () => Promise<void>;
type ApiCommandUnknownHandler = (command: string) => Promise<void>;
export interface ApiCommandDependencies {
help: ApiCommandHelpHandler;
unknown: ApiCommandUnknownHandler;
create: ApiCommandHandler;
list: ApiCommandHandler;
discover: ApiCommandHandler;
copy: ApiCommandHandler;
export: ApiCommandHandler;
import: ApiCommandHandler;
remove: ApiCommandHandler;
}
function createApiCommandRoutes(
dependencies: ApiCommandDependencies
): readonly NamedCommandRoute[] {
return [
{ name: 'create', handle: dependencies.create },
{ name: 'list', handle: dependencies.list },
{ name: 'discover', handle: dependencies.discover },
{ name: 'copy', handle: dependencies.copy },
{ name: 'export', handle: dependencies.export },
{ name: 'import', handle: dependencies.import },
{ name: 'remove', aliases: ['delete', 'rm'], handle: dependencies.remove },
];
}
/**
* Factory for building an api-command handler with injectable dependencies.
* Extracted from index.ts so tests can import without loading all subcommand modules.
*/
export function createApiCommandHandler(
dependencies: ApiCommandDependencies
): (args: string[]) => Promise<void> {
const routes = createApiCommandRoutes(dependencies);
return async (args: string[]) => {
await dispatchNamedCommand({
args,
routes,
onHelp: dependencies.help,
onUnknown: dependencies.unknown,
allowEmptyHelp: true,
});
};
}
+3 -52
View File
@@ -1,31 +1,16 @@
import { dispatchNamedCommand, type NamedCommandRoute } from '../named-command-router';
import { handleApiCopyCommand } from './copy-command';
import { handleApiCreateCommand } from './create-command';
import { handleApiDiscoverCommand } from './discover-command';
import { handleApiExportCommand } from './export-command';
import { createApiCommandHandler, type ApiCommandDependencies } from './handler';
import { showApiCommandHelp, showUnknownApiCommand } from './help';
import { handleApiImportCommand } from './import-command';
import { handleApiListCommand } from './list-command';
import { handleApiRemoveCommand } from './remove-command';
export { createApiCommandHandler, type ApiCommandDependencies } from './handler';
export { parseApiCommandArgs } from './shared';
type ApiCommandHandler = (args: string[]) => Promise<void>;
type ApiCommandHelpHandler = () => Promise<void>;
type ApiCommandUnknownHandler = (command: string) => Promise<void>;
export interface ApiCommandDependencies {
help: ApiCommandHelpHandler;
unknown: ApiCommandUnknownHandler;
create: ApiCommandHandler;
list: ApiCommandHandler;
discover: ApiCommandHandler;
copy: ApiCommandHandler;
export: ApiCommandHandler;
import: ApiCommandHandler;
remove: ApiCommandHandler;
}
const DEFAULT_API_COMMAND_DEPENDENCIES: ApiCommandDependencies = {
help: showApiCommandHelp,
unknown: showUnknownApiCommand,
@@ -38,40 +23,6 @@ const DEFAULT_API_COMMAND_DEPENDENCIES: ApiCommandDependencies = {
remove: handleApiRemoveCommand,
};
function createApiCommandRoutes(
dependencies: ApiCommandDependencies
): readonly NamedCommandRoute[] {
return [
{ name: 'create', handle: dependencies.create },
{ name: 'list', handle: dependencies.list },
{ name: 'discover', handle: dependencies.discover },
{ name: 'copy', handle: dependencies.copy },
{ name: 'export', handle: dependencies.export },
{ name: 'import', handle: dependencies.import },
{ name: 'remove', aliases: ['delete', 'rm'], handle: dependencies.remove },
];
}
export function createApiCommandHandler(
overrides: Partial<ApiCommandDependencies> = {}
): (args: string[]) => Promise<void> {
const dependencies: ApiCommandDependencies = {
...DEFAULT_API_COMMAND_DEPENDENCIES,
...overrides,
};
const routes = createApiCommandRoutes(dependencies);
return async (args: string[]) => {
await dispatchNamedCommand({
args,
routes,
onHelp: dependencies.help,
onUnknown: dependencies.unknown,
allowEmptyHelp: true,
});
};
}
export async function handleApiCommand(args: string[]): Promise<void> {
await createApiCommandHandler()(args);
await createApiCommandHandler(DEFAULT_API_COMMAND_DEPENDENCIES)(args);
}
+1 -1
View File
@@ -2,7 +2,7 @@
* Commands module barrel export
*/
export { handleApiCommand } from './api-command';
export { handleApiCommand } from './api-command/index';
export { handleCleanupCommand } from './cleanup-command';
export { handleCliproxyCommand } from './cliproxy-command';
export { handleConfigCommand } from './config-command';
+1 -1
View File
@@ -118,7 +118,7 @@ const ROOT_COMMAND_ROUTES: readonly NamedCommandRoute[] = [
{
name: 'api',
handle: async (args) => {
const { handleApiCommand } = await import('./api-command');
const { handleApiCommand } = await import('./api-command/index');
await handleApiCommand(args);
},
},
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it } from 'bun:test';
import { createApiCommandHandler } from '../../../src/commands/api-command';
import { createApiCommandHandler } from '../../../src/commands/api-command/handler';
let calls: string[] = [];