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
40 changes: 33 additions & 7 deletions packages/cli/src/cmd/build/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Logger } from '@agentuity/core';
import { spawn } from 'bun';
import { mkdir, mkdtemp, readdir, realpath, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { dirname, join } from 'node:path';
import { ErrorCode } from '../../errors';
import * as tui from '../../tui';

Expand All @@ -21,6 +21,27 @@ export interface CIBuildOptions {
pullRequestUrl?: string;
logsUrl?: string;
skipDnsValidation?: boolean;
skipTypeCheck?: boolean;
}

export async function hasProjectDependenciesInstalled(projectDir: string): Promise<boolean> {
let dir = projectDir;
for (;;) {
const bunStore = join(dir, 'node_modules', '.bun');
const lockfile = join(dir, 'bun.lock');
if (
(await stat(bunStore).catch(() => null))?.isDirectory() &&
(await Bun.file(lockfile).exists())
) {
return true;
}

const parent = dirname(dir);
if (parent === dir) {
return false;
}
dir = parent;
}
}

async function runCommand(cmd: string[], cwd: string): Promise<number> {
Expand Down Expand Up @@ -127,6 +148,7 @@ export function buildDeployArgs(opts: CIBuildOptions): string[] {
if (opts.pullRequestUrl) args.push('--pull-request-url', opts.pullRequestUrl);
if (opts.logsUrl) args.push('--logs-url', opts.logsUrl);
if (opts.skipDnsValidation) args.push('--skip-dns-validation');
if (opts.skipTypeCheck) args.push('--skip-type-check');

return args;
}
Expand Down Expand Up @@ -210,12 +232,16 @@ export async function runCIBuild(opts: CIBuildOptions, _logger: Logger): Promise
await Bun.write(join(projectDir, '.env'), `AGENTUITY_SDK_KEY=${sdkKey}\n`);
}

tui.info('3️⃣ Installing your project dependencies...');
const installExit = await runCommand(['bun', 'install'], projectDir);
if (installExit !== 0) {
tui.error(`Dependency installation failed (exit ${installExit})`);
pendingExitCode = installExit;
return;
if (await hasProjectDependenciesInstalled(projectDir)) {
tui.info('3️⃣ Using existing project dependencies (skipping install)...');
} else {
tui.info('3️⃣ Installing your project dependencies...');
const installExit = await runCommand(['bun', 'install'], projectDir);
if (installExit !== 0) {
tui.error(`Dependency installation failed (exit ${installExit})`);
pendingExitCode = installExit;
return;
}
}

const packageJsonPath = join(projectDir, 'package.json');
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/cmd/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const command = createCommand({
pullRequestUrl: opts.pullRequestUrl,
logsUrl: opts.logsUrl,
skipDnsValidation: opts.skipDnsValidation ?? true,
skipTypeCheck: opts.skipTypeCheck,
},
ctx.logger
);
Expand Down
35 changes: 19 additions & 16 deletions packages/cli/src/cmd/cloud/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ export const deploySubcommand = createSubcommand({
childArgs.push(`--pull-request-number=${opts.pullRequestNumber}`);
if (opts.pullRequestUrl) childArgs.push(`--pull-request-url=${opts.pullRequestUrl}`);
if (opts.skipDnsValidation) childArgs.push('--skip-dns-validation');
if (opts.skipTypeCheck) childArgs.push('--skip-type-check');

const result = await runForkedDeploy({
projectDir,
Expand Down Expand Up @@ -650,23 +651,25 @@ export const deploySubcommand = createSubcommand({
let capturedOutput: string[] = [];
const rootDir = resolve(projectDir);

// Run typecheck with collector for error reporting
const endTypecheckDiagnostic = collector.startDiagnostic('typecheck');
const started = Date.now();
const typeResult = await typecheck(rootDir, { collector });
endTypecheckDiagnostic();

if (typeResult.success) {
capturedOutput.push(
tui.muted(`✓ Typechecked in ${Date.now() - started}ms`)
);
} else {
// Errors already added to collector by typecheck()
// Write report before returning error
if (opts.reportFile) {
await collector.forceWrite();
if (!opts.skipTypeCheck) {
// Run typecheck with collector for error reporting
const endTypecheckDiagnostic = collector.startDiagnostic('typecheck');
const started = Date.now();
const typeResult = await typecheck(rootDir, { collector });
endTypecheckDiagnostic();

if (typeResult.success) {
capturedOutput.push(
tui.muted(`✓ Typechecked in ${Date.now() - started}ms`)
);
} else {
// Errors already added to collector by typecheck()
// Write report before returning error
if (opts.reportFile) {
await collector.forceWrite();
}
return stepError('Typecheck failed\n\n' + typeResult.output);
}
return stepError('Typecheck failed\n\n' + typeResult.output);
}
try {
const bundleResult = await viteBundle({
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,7 @@ export const DeployOptionsSchema = zod
.boolean()
.optional()
.describe('Skip custom domain DNS validation before deploying'),
skipTypeCheck: zod.boolean().optional().describe('Skip TypeScript validation during deploy'),
})
.merge(GitOptionsSchema);

Expand Down
37 changes: 35 additions & 2 deletions packages/cli/test/cmd/build/ci.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
import { describe, expect, test } from 'bun:test';
import { Buffer } from 'node:buffer';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { buildDeployArgs, downloadSource, sourceDownloadHeaders } from '../../../src/cmd/build/ci';
import {
buildDeployArgs,
downloadSource,
hasProjectDependenciesInstalled,
sourceDownloadHeaders,
} from '../../../src/cmd/build/ci';

describe('build ci', () => {
test('passes skip DNS validation to nested deploy', () => {
const args = buildDeployArgs({ skipDnsValidation: true });
expect(args).toContain('--skip-dns-validation');
});

test('passes skip typecheck to nested deploy', () => {
const args = buildDeployArgs({ skipTypeCheck: true });
expect(args).toContain('--skip-type-check');
});

test('detects existing dependencies from monorepo root', async () => {
const dir = await mkdtemp(join(tmpdir(), 'agentuity-ci-deps-test-'));
try {
await mkdir(join(dir, 'node_modules', '.bun'), { recursive: true });
await writeFile(join(dir, 'bun.lock'), '');
const appDir = join(dir, 'apps', 'web');
await mkdir(appDir, { recursive: true });

expect(await hasProjectDependenciesInstalled(appDir)).toBe(true);
} finally {
await rm(dir, { recursive: true, force: true });
}
});

test('requires install when dependencies are missing', async () => {
const dir = await mkdtemp(join(tmpdir(), 'agentuity-ci-deps-test-'));
try {
expect(await hasProjectDependenciesInstalled(dir)).toBe(false);
} finally {
await rm(dir, { recursive: true, force: true });
}
});

test('adds GitHub archive token only for GitHub archive hosts', () => {
const previous = process.env.GITHUB_ARCHIVE_TOKEN;
process.env.GITHUB_ARCHIVE_TOKEN = 'ghs_test_token';
Expand Down
Loading