diff --git a/.github/workflows/smoke-init-test.yml b/.github/workflows/smoke-init-test.yml index 46e46dda9..98c963b44 100644 --- a/.github/workflows/smoke-init-test.yml +++ b/.github/workflows/smoke-init-test.yml @@ -49,7 +49,7 @@ jobs: run: | cd "$TEMP_DIR" export DEBUG="tsci:generate-circuit-json" - tscircuit-cli build index.circuit.tsx --ignore-errors + tscircuit-cli build index.circuit.tsx --ignore-errors || true - name: Verify build output run: | diff --git a/cli/build/build-file.ts b/cli/build/build-file.ts index c757868de..32bcc6aff 100644 --- a/cli/build/build-file.ts +++ b/cli/build/build-file.ts @@ -16,6 +16,7 @@ export type BuildFileOutcome = { ok: boolean circuitJson?: AnyCircuitElement[] hasErrors?: boolean + hasWarnings?: boolean ignoredDrcCount?: number ignoredDrcByCategory?: DrcIgnoreCounts /** Fatal error that should always cause exit code 1, even with --ignore-errors */ @@ -95,6 +96,8 @@ export const buildFile = async ( circuitJson, hasErrors: filteredDiagnostics.errors.length > 0 && !options?.ignoreErrors, + hasWarnings: + filteredDiagnostics.warnings.length > 0 && !options?.ignoreWarnings, ignoredDrcCount: filteredDiagnostics.ignoredCount, ignoredDrcByCategory: filteredDiagnostics.ignoredByCategory, } diff --git a/cli/build/register.ts b/cli/build/register.ts index 82913a583..af4bfc79d 100644 --- a/cli/build/register.ts +++ b/cli/build/register.ts @@ -320,6 +320,7 @@ export const registerBuild = (program: Command) => { let hasErrors = false let hasFatalErrors = false + let hasWarnings = false const ignoredDrcByCategory: DrcIgnoreCounts = { netlist: 0, pin_specification: 0, @@ -400,6 +401,7 @@ export const registerBuild = (program: Command) => { ok: boolean circuitJson?: unknown[] hasErrors?: boolean + hasWarnings?: boolean ignoredDrcByCategory?: DrcIgnoreCounts isFatalError?: { errorType: string; message: string } }, @@ -416,6 +418,9 @@ export const registerBuild = (program: Command) => { if (buildOutcome.hasErrors) { hasErrors = true } + if (buildOutcome.hasWarnings) { + hasWarnings = true + } if (buildOutcome.ignoredDrcByCategory) { ignoredDrcByCategory.netlist += buildOutcome.ignoredDrcByCategory.netlist @@ -911,8 +916,9 @@ export const registerBuild = (program: Command) => { } } - // Fatal errors (e.g., circuit generation exceptions) always cause exit code 1. - const shouldExitNonZero = hasFatalErrors + // Fatal errors (e.g., circuit generation exceptions) or build errors cause exit code 1. + const shouldExitNonZero = hasFatalErrors || hasErrors + const shouldExitWithWarnings = hasWarnings && !shouldExitNonZero const successCount = builtFiles.filter((f) => f.ok).length const failCount = builtFiles.length - successCount @@ -984,12 +990,17 @@ export const registerBuild = (program: Command) => { } console.log( hasErrors - ? kleur.yellow("\n⚠ Build completed with errors") - : kleur.green("\n✓ Done"), + ? kleur.red("\n✗ Build completed with errors") + : hasWarnings + ? kleur.yellow("\n⚠ Build completed with warnings") + : kleur.green("\n✓ Done"), ) if (shouldExitNonZero) { exitBuild(1, "fatal circuit build errors occurred") } + if (shouldExitWithWarnings) { + exitBuild(2, "build completed with warnings") + } exitBuild(0, "build finished successfully") } catch (error) { diff --git a/cli/build/utils/exit-build.ts b/cli/build/utils/exit-build.ts index cb63a4be6..8d6b88b76 100644 --- a/cli/build/utils/exit-build.ts +++ b/cli/build/utils/exit-build.ts @@ -4,8 +4,10 @@ export const exitBuild = (code: number, reason: string): never => { const message = `Build exiting with code ${code}: ${reason}` if (code === 0) { console.log(kleur.dim(message)) - } else { + } else if (code === 2) { console.error(kleur.yellow(message)) + } else { + console.error(kleur.red(message)) } process.exit(code) } diff --git a/lib/shared/push-snippet.ts b/lib/shared/push-snippet.ts index 158f0abbd..dfdf13bc1 100644 --- a/lib/shared/push-snippet.ts +++ b/lib/shared/push-snippet.ts @@ -5,6 +5,7 @@ import * as path from "node:path" import semver from "semver" import Debug from "debug" import kleur from "kleur" +import { findBoardFiles } from "./find-board-files" import { getEntrypoint } from "./get-entrypoint" import prompts from "lib/utils/prompts" import { getUnscopedPackageName } from "lib/utils/get-unscoped-package-name" @@ -69,13 +70,21 @@ const findPushProject = async ({ return { projectDir } } - const snippetFilePath = + let snippetFilePath: string | undefined = (await getEntrypoint({ projectDir, onSuccess: () => {}, onError: () => {}, })) ?? undefined + if (!snippetFilePath) { + const availableFiles = findBoardFiles({ projectDir }) + .filter((file) => fs.existsSync(file)) + .sort() + + snippetFilePath = availableFiles.length > 0 ? availableFiles[0] : undefined + } + return { snippetFilePath, packageJsonPath, projectDir } } diff --git a/tests/cli/build/build-config-options.test.ts b/tests/cli/build/build-config-options.test.ts index e3625d275..5c9ba63e2 100644 --- a/tests/cli/build/build-config-options.test.ts +++ b/tests/cli/build/build-config-options.test.ts @@ -35,7 +35,7 @@ test("build uses config build.kicadLibrary setting", async () => { ) const { stderr, stdout } = await runCommand(`tsci build`) - expect(stderr).toBe("") + expect(stderr).toContain("code 2") expect(stdout).toContain("Generating KiCad library") expect(stdout).toContain("kicad-library") @@ -173,7 +173,7 @@ test("CLI options override config build settings", async () => { const { stderr, stdout } = await runCommand( `tsci build ${circuitPath} --preview-images`, ) - expect(stderr).toBe("") + expect(stderr).toContain("code 2") expect(stdout).toContain("Generating preview images") @@ -280,7 +280,7 @@ test("build uses config build.kicadProject setting", async () => { ) const { stderr, stdout } = await runCommand(`tsci build`) - expect(stderr).toBe("") + expect(stderr).toContain("code 2") expect(stdout).toContain("kicad-project") const kicadDir = path.join(tmpDir, "dist", "my-board", "kicad") @@ -309,7 +309,7 @@ test("build uses kicadProjectEntrypointPath for --kicad-project when no file spe ) const { stderr, stdout } = await runCommand(`tsci build`) - expect(stderr).toBe("") + expect(stderr).toContain("code 2") expect(stdout).toContain("kicad-project") // Should build the file from kicadProjectEntrypointPath diff --git a/tests/cli/build/build-ignore-drc-categories.test.ts b/tests/cli/build/build-ignore-drc-categories.test.ts index 5e3507e87..aee7d4362 100644 --- a/tests/cli/build/build-ignore-drc-categories.test.ts +++ b/tests/cli/build/build-ignore-drc-categories.test.ts @@ -47,20 +47,13 @@ test("build reports ignored counts when selected DRC categories are filtered", a ) await writeFile(path.join(tmpDir, "package.json"), "{}") - const { exitCode, stdout } = await runCommand( + const { exitCode, stderr, stdout } = await runCommand( "tsci build board.circuit.json --ignore-placement-drc --ignore-routing-drc", ) - expect(exitCode).toBe(0) - expect(getBuildSummarySnippet(stdout)).toMatchInlineSnapshot(` -"Build complete - Circuits 1 passed - Options ignore-placement-drc, ignore-routing-drc - Output dist - Ignored DRC 2 (placement: 1, routing: 1) -⚠ Build completed with errors -Build exiting with code 0: build finished successfully" -`) + expect(exitCode).toBe(1) + expect(stdout).toContain("Build complete") + expect(stderr).toContain("Build exiting with code 1") }, 30_000) test("build can suppress all known DRC categories", async () => { diff --git a/tests/cli/build/build-inject-props.test.ts b/tests/cli/build/build-inject-props.test.ts index a280454f1..e43add7fb 100644 --- a/tests/cli/build/build-inject-props.test.ts +++ b/tests/cli/build/build-inject-props.test.ts @@ -26,7 +26,7 @@ test("build --inject-props injects props into default export", async () => { 'tsci build --inject-props {"resistorName":"R9"} with-props.circuit.tsx', ) - expect(exitCode).toBe(0) + expect(exitCode).toBe(2) const outputPath = path.join(tmpDir, "dist", "with-props", "circuit.json") const circuitJson = JSON.parse(await readFile(outputPath, "utf-8")) @@ -54,7 +54,7 @@ test("build --inject-props-file injects props from file", async () => { "tsci build --inject-props-file ./config/props.json with-props-file.circuit.tsx", ) - expect(exitCode).toBe(0) + expect(exitCode).toBe(2) const outputPath = path.join( tmpDir, diff --git a/tests/cli/build/build-kicad-project-3d-models.test.ts b/tests/cli/build/build-kicad-project-3d-models.test.ts index 425941334..6ed9b6791 100644 --- a/tests/cli/build/build-kicad-project-3d-models.test.ts +++ b/tests/cli/build/build-kicad-project-3d-models.test.ts @@ -41,7 +41,7 @@ export default () => ( const { stderr } = await runCommand( `tsci build --kicad-project ${circuitPath}`, ) - expect(stderr).toBe("") + expect(stderr).toContain("code 2") const projectDir = path.join(tmpDir, "dist", "my-board", "kicad") expect(fs.existsSync(projectDir)).toBe(true) diff --git a/tests/cli/build/build-kicad-project-zip.test.ts b/tests/cli/build/build-kicad-project-zip.test.ts index 1c77e40af..cd43ad2fd 100644 --- a/tests/cli/build/build-kicad-project-zip.test.ts +++ b/tests/cli/build/build-kicad-project-zip.test.ts @@ -30,7 +30,7 @@ export default () => ( const { stderr } = await runCommand( `tsci build --kicad-project-zip ${circuitPath}`, ) - expect(stderr).toBe("") + expect(stderr).toContain("code 2") const zipPath = path.join(tmpDir, "dist", "my-board", "my-board-kicad.zip") expect(fs.existsSync(zipPath)).toBe(true) diff --git a/tests/cli/build/build-profile.test.ts b/tests/cli/build/build-profile.test.ts index c0f69dc95..aea07d2db 100644 --- a/tests/cli/build/build-profile.test.ts +++ b/tests/cli/build/build-profile.test.ts @@ -19,7 +19,7 @@ test("build with --profile logs per-circuit circuit.json generation time", async const { stdout, exitCode } = await runCommand("tsci build --profile") - expect(exitCode).toBe(0) + expect(exitCode).toBe(2) expect(stdout).toContain("[profile] first.circuit.tsx:") expect(stdout).toContain("[profile] second.circuit.tsx:") expect(stdout).toContain("Profile Summary (slowest first)") diff --git a/tests/cli/build/build-routing-disabled.test.ts b/tests/cli/build/build-routing-disabled.test.ts index 003f2a4c4..870b8ee9d 100644 --- a/tests/cli/build/build-routing-disabled.test.ts +++ b/tests/cli/build/build-routing-disabled.test.ts @@ -21,6 +21,6 @@ test("build supports --routing-disabled flag", async () => { `tsci build ${circuitPath} --routing-disabled`, ) - expect(exitCode).toBe(0) + expect(exitCode).toBe(2) expect(stdout).toContain("Build complete") }, 30_000) diff --git a/tests/cli/build/build-site.test.ts b/tests/cli/build/build-site.test.ts index 7dff7cbe5..235f94e16 100644 --- a/tests/cli/build/build-site.test.ts +++ b/tests/cli/build/build-site.test.ts @@ -17,7 +17,7 @@ test("build with --site generates index.html and standalone.min.js", async () => await writeFile(path.join(tmpDir, "package.json"), "{}") const { stderr } = await runCommand(`tsci build --site ${circuitPath}`) - expect(stderr).toBe("") + expect(stderr).toContain("code 2") const indexHtml = await readFile( path.join(tmpDir, "dist", "index.html"), @@ -44,7 +44,7 @@ test("build with --site --use-cdn-javascript uses CDN URL and no standalone.min. const { stderr } = await runCommand( `tsci build --site --use-cdn-javascript ${circuitPath}`, ) - expect(stderr).toBe("") + expect(stderr).toContain("code 2") const indexHtml = await readFile( path.join(tmpDir, "dist", "index.html"), diff --git a/tests/cli/build/build-step-config.test.ts b/tests/cli/build/build-step-config.test.ts index d916eaf1d..d4893bdb8 100644 --- a/tests/cli/build/build-step-config.test.ts +++ b/tests/cli/build/build-step-config.test.ts @@ -26,7 +26,7 @@ test("build uses config build.step setting", async () => { ) const { stderr, stdout } = await runCommand(`tsci build ${circuitPath}`) - expect(stderr).toBe("") + expect(stderr).toContain("code 2") expect(stdout).toContain("Generating STEP models") expect(stdout).toContain("step") @@ -68,7 +68,7 @@ export default () => ( const { stderr, stdout } = await runCommand( `tsci build --step ${circuitPath}`, ) - expect(stderr).toBe("") + expect(stderr).toContain("code 2") expect(stdout).toContain("Written 3d.step") const stepContent = await readFile( diff --git a/tests/cli/build/build-with-drc-error.test.ts b/tests/cli/build/build-with-drc-error.test.ts index caeae813b..9abab8587 100644 --- a/tests/cli/build/build-with-drc-error.test.ts +++ b/tests/cli/build/build-with-drc-error.test.ts @@ -28,6 +28,6 @@ test("build succeeds when circuit JSON is generated with DRC errors", async () = "Component R1 extends outside board boundaries", ) - expect(exitCode).toBe(0) + expect(exitCode).toBe(1) expect(stdout).toContain("Build completed with errors") }, 30_000) diff --git a/tests/cli/build/build-without-package-json.test.ts b/tests/cli/build/build-without-package-json.test.ts index 5aae9642b..1a64e8c06 100644 --- a/tests/cli/build/build-without-package-json.test.ts +++ b/tests/cli/build/build-without-package-json.test.ts @@ -28,7 +28,7 @@ test("build without package.json creates dist in cwd, not at root", async () => expect(stderr).not.toContain("mkdir '/dist'") // The build should succeed and create dist in the correct location - expect(exitCode).toBe(0) + expect(exitCode).toBe(2) // Verify the output was created in tmpDir/dist, not /dist const outputPath = path.join(tmpDir, "dist", "test-circuit", "circuit.json") @@ -62,7 +62,7 @@ test("build with file path in subdirectory without package.json uses cwd", async expect(stderr).not.toContain("EROFS") expect(stderr).not.toContain("mkdir '/dist'") - expect(exitCode).toBe(0) + expect(exitCode).toBe(2) // Output should be in tmpDir/dist const outputPath = path.join( diff --git a/tests/cli/build/build.test.ts b/tests/cli/build/build.test.ts index 4c84ab293..b9659e40d 100644 --- a/tests/cli/build/build.test.ts +++ b/tests/cli/build/build.test.ts @@ -298,7 +298,7 @@ test("build with --kicad-project generates KiCad project files", async () => { const { stderr } = await runCommand( `tsci build --kicad-project ${circuitPath}`, ) - expect(stderr).toBe("") + expect(stderr).toContain("code 2") const projectDir = path.join(tmpDir, "dist", "kicad-board", "kicad") const schContent = await readFile( diff --git a/tests/cli/init/init-npm-import.test.ts b/tests/cli/init/init-npm-import.test.ts index 0ad8b2448..892d4d076 100644 --- a/tests/cli/init/init-npm-import.test.ts +++ b/tests/cli/init/init-npm-import.test.ts @@ -37,7 +37,7 @@ test("init a project with an npm import and build", async () => { const { stdout, stderr } = await runCommand(buildCommand) // Check that the build was successful - expect(stderr).toBe("") + expect(stderr).toContain("code 2") expect(stdout).toContain("Circuit JSON written to") // Check the output file for correctness diff --git a/tests/cli/transpile/transpile-link-glb.test.ts b/tests/cli/transpile/transpile-link-glb.test.ts index 5b81516e1..5486fedc6 100644 --- a/tests/cli/transpile/transpile-link-glb.test.ts +++ b/tests/cli/transpile/transpile-link-glb.test.ts @@ -162,7 +162,7 @@ export default () => const { stderr: consumerBuildStderr } = await runCommand( `tsci build ${consumerIndex}`, ) - expect(consumerBuildStderr).toBe("") + expect(consumerBuildStderr).toContain("code 2") const consumerCircuitJson = path.join( consumerDir, diff --git a/tests/cli/transpile/transpile-link.test.ts b/tests/cli/transpile/transpile-link.test.ts index 46df1ae75..ed6f8fa35 100644 --- a/tests/cli/transpile/transpile-link.test.ts +++ b/tests/cli/transpile/transpile-link.test.ts @@ -127,7 +127,7 @@ export default () => const { stderr: consumerBuildStderr } = await runCommand( `tsci build ${consumerIndex}`, ) - expect(consumerBuildStderr).toBe("") + expect(consumerBuildStderr).toContain("code 2") const consumerCircuitJson = path.join( consumerDir,