diff --git a/README.md b/README.md index 25da8cd..f645d42 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ An explicit token always wins over the OIDC exchange. ## How it works -Each run has three phases: install, build, and deploy. Your workflow owns checkout and the toolchain. The action runs your install and build commands exactly as configured, and it never inspects your repository to decide how to build. The deploy phase hands your built app to the [Prisma Composer](https://github.com/prisma/composer) CLI, running it under Bun. Bun must be on the runner PATH — add `oven-sh/setup-bun@v2` before this action. The generated Prisma deploy workflow includes that step automatically. +Each run has three phases: install, build, and deploy. Your workflow owns checkout and the toolchain. The action runs your install and build commands exactly as configured, and it never inspects your repository to decide how to build. A repository with no build script — one that runs its source directly — sets `build-command: none` to skip the build phase. The deploy phase hands your built app to the [Prisma Composer](https://github.com/prisma/composer) CLI, running it under Bun. Bun must be on the runner PATH — add `oven-sh/setup-bun@v2` before this action. The generated Prisma deploy workflow includes that step automatically. Deploy targets follow your branches: @@ -88,7 +88,7 @@ The credential resolves in order: an explicit `PRISMA_SERVICE_TOKEN` from the en | Input | Default | Description | | --- | --- | --- | -| `build-command` | `npm run build` | Your build command, run verbatim. It runs in both modes, because destroy evaluates the built app to know what to remove. | +| `build-command` | `npm run build` | Your build command, run verbatim. It runs in both modes, because destroy evaluates the built app to know what to remove. The exact value `none` skips the build phase in both modes, for repositories with no build script; an empty value keeps the default. | | `install-command` | auto | Detected from the lockfile: `npm ci` for `package-lock.json`, `bun install --frozen-lockfile` for a bun lockfile. Set this to override. pnpm and yarn are not supported yet. | | `module` | `module.ts` | Path to your app's Composer module. | | `mode` | `deploy` | `deploy` or `destroy`. | diff --git a/action.yml b/action.yml index 97d5853..9202f6f 100644 --- a/action.yml +++ b/action.yml @@ -2,7 +2,7 @@ name: Deploy to Prisma Cloud description: Build and deploy your app to Prisma Cloud from GitHub Actions inputs: build-command: - description: Your build command, run verbatim in both modes + description: Your build command, run verbatim in both modes; the exact value "none" skips the build phase default: npm run build install-command: description: Empty auto-detects from the lockfile (npm ci or bun install --frozen-lockfile) diff --git a/build.mjs b/build.mjs new file mode 100644 index 0000000..6df701c --- /dev/null +++ b/build.mjs @@ -0,0 +1,14 @@ +/** + * Returns the build command to run, or null to skip the build phase. + * + * Only the exact value "none" skips — for repos with no build script whose + * source runs directly. An empty input falls back to the default, so a blank + * value or a typo can never silently skip a build. + * + * @param {string} rawInput - The trimmed build-command input ("" when unset). + * @returns {string | null} + */ +export function selectBuildCommand(rawInput) { + if (rawInput === "none") return null; + return rawInput || "npm run build"; +} diff --git a/main.mjs b/main.mjs index 251b2a9..ad1765a 100644 --- a/main.mjs +++ b/main.mjs @@ -4,6 +4,7 @@ import { spawnSync } from "node:child_process"; import { appendFileSync, existsSync, readFileSync, writeSync } from "node:fs"; import { join, resolve } from "node:path"; +import { selectBuildCommand } from "./build.mjs"; import { selectComposerCommand } from "./composer.mjs"; import { resolveCredential } from "./credentials.mjs"; import { deployedUrlFromOutput } from "./deployment.mjs"; @@ -230,8 +231,16 @@ await runPhase("install", installCommand); // same stack program as deploy, which packages the built artifacts — so the // app must be built first" (its error text on 0.6.0). We wanted destroy to // skip the build so teardown never depends on the default branch's build -// health; composer does not allow that today. -await runPhase("build", input("build-command") || "npm run build"); +// health; composer does not allow that today. `build-command: none` is +// different: the app has no build step at all, so both modes skip the phase. +const buildCommand = selectBuildCommand(input("build-command")); +if (buildCommand === null) { + // Install already reported the server-side "build" phase (mapPhase sends + // install as "build"), so the skip needs no report update of its own. + log("build: skipped (build-command: none)"); +} else { + await runPhase("build", buildCommand); +} // The stage reaches the argv array straight from the environment; it is // never interpolated into a shell string. diff --git a/tests/build.test.mjs b/tests/build.test.mjs new file mode 100644 index 0000000..2e2cd73 --- /dev/null +++ b/tests/build.test.mjs @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { selectBuildCommand } from "../build.mjs"; + +test('the exact value "none" skips the build', () => { + assert.equal(selectBuildCommand("none"), null); +}); + +test("an empty input falls back to the default build command", () => { + assert.equal(selectBuildCommand(""), "npm run build"); +}); + +test("any other command runs verbatim", () => { + assert.equal(selectBuildCommand("bun run build"), "bun run build"); +}); + +test('a near-miss like "None" runs verbatim instead of skipping', () => { + assert.equal(selectBuildCommand("None"), "None"); +}); + +test('a command containing "none" runs verbatim', () => { + assert.equal(selectBuildCommand("npm run none"), "npm run none"); +});