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
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- uses: oven-sh/setup-bun@v2
- uses: prisma/cloud-deploy-action@v1
with:
build-command: npm run build
Expand All @@ -46,6 +48,8 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- uses: oven-sh/setup-bun@v2
- uses: prisma/cloud-deploy-action@v1
with:
mode: destroy
Expand All @@ -70,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.
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.

Deploy targets follow your branches:

Expand All @@ -89,7 +93,7 @@ The credential resolves in order: an explicit `PRISMA_SERVICE_TOKEN` from the en
| `module` | `module.ts` | Path to your app's Composer module. |
| `mode` | `deploy` | `deploy` or `destroy`. |
| `stage` | derived | Empty derives the stage from the branch: the default branch deploys to production, any other branch name becomes the stage. `destroy` requires a resolved stage. |
| `composer-version` | `0.7.0` | The Composer CLI version the action fetches via npx when the repo has no local bin. From 0.7.0 the bin ships in `@prisma/composer-cli`; set to a `0.6.x` value only if you need the old package. |
| `composer-version` | `0.7.0` | The Composer CLI version the action fetches via bunx when the repo has no local bin. From 0.7.0 the bin ships in `@prisma/composer-cli`; set to a `0.6.x` value only if you need the old package. |
| `working-directory` | `.` | Where install, build, and deploy run. |
| `api-url` | `https://api.prisma.io` | Prisma API base URL for the OIDC credential exchange. |

Expand All @@ -110,9 +114,12 @@ On a successful deploy, the action also reports the deployed preview URL (`deplo

When a run has no credential, no `[report-stub]` log lines appear; the run is silent on reporting.

## Requirements

Bun must be on the runner PATH. Add `oven-sh/setup-bun@v2` before this action step. The generated Prisma deploy workflow adds this step for every project.

## Known limitations

- Keep the workflow on Node 22. The Composer CLI has not been verified against Node 24, even though the action itself runs on the runner's Node 24.
- Install detection covers npm and bun lockfiles. Repositories using pnpm or yarn need an explicit `install-command`, and deploys are not tested against them yet.
- Workflow runs triggered from forks receive no OIDC token from GitHub, so they skip deploying unless a `PRISMA_SERVICE_TOKEN` secret is provided.
- The deployed preview URL is read from Composer's human deploy output, because released Composer (0.6.0) does not expose it as data. When Composer emits the deploy result in a machine-readable form — a `--json` result carrying each deployed service's public URL — the action should read the URL from there rather than from the printed report.
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ inputs:
description: Empty derives the stage from the branch (default branch deploys to production, any other branch name becomes the stage)
default: ""
composer-version:
description: The @prisma/composer-cli version the action fetches via npx when the repo has no local bin (0.7.0+); use @prisma/composer for 0.6.x
description: The @prisma/composer-cli version the action fetches via bunx when the repo has no local bin (0.7.0+); use @prisma/composer for 0.6.x
default: "0.7.0"
working-directory:
description: Where install, build, and deploy run
Expand Down
18 changes: 18 additions & 0 deletions composer.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Returns [cmd, leadArgs, logLabel] for running the Composer CLI under Bun.
*
* @param {string} localBin - Absolute path to the local prisma-composer binary.
* @param {string} composerVersion - @prisma/composer-cli version to fetch via bunx when the local bin is absent.
* @param {boolean} binExists - Whether the local bin file exists.
* @returns {[string, string[], string]}
*/
export function selectComposerCommand(localBin, composerVersion, binExists) {
if (binExists) {
return ["bun", [localBin], "composer=local bin (bun)"];
}
return [
"bunx",
["--bun", "-p", `@prisma/composer-cli@${composerVersion}`, "prisma-composer"],
`composer=${composerVersion} (bunx fallback)`,
];
}
25 changes: 19 additions & 6 deletions main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { selectComposerCommand } from "./composer.mjs";
import { resolveCredential } from "./credentials.mjs";
import { deployedUrlFromOutput } from "./deployment.mjs";
import { guardReport, makeReporter, mapPhase } from "./report.mjs";
Expand Down Expand Up @@ -136,6 +137,16 @@ if (mode === "destroy" && !stage) {
}
log(stage ? `${mode} target: --stage ${stage}` : `${mode} target: production (default branch, no --stage)`);

// Bun is required: composer runs under Bun, not Node. The generated Prisma
// deploy workflow adds `oven-sh/setup-bun@v2` for every project.
const bunVersionCheck = spawnSync("bun", ["--version"], { stdio: "pipe" });
if (bunVersionCheck.error?.code === "ENOENT") {
failEarly(
"config",
"bun not found on PATH: add `- uses: oven-sh/setup-bun@v2` before this action step; the generated Prisma deploy workflow includes this automatically",
);
}

const installCommand = (() => {
const explicit = input("install-command");
if (explicit) return explicit;
Expand Down Expand Up @@ -228,13 +239,15 @@ await runPhase("build", input("build-command") || "npm run build");
// The CLI bin is named prisma-composer. From 0.7.0 it ships inside
// @prisma/composer-cli (previously @prisma/composer); there is no npm package
// named prisma-composer, so `npx prisma-composer@v` 404s. The repo's own
// install already provides the bin, so prefer the local bin; fall back to
// fetching the pinned package only when the repo does not carry it.
// install already provides the bin, so prefer the local bin under Bun; fall
// back to bunx fetching the pinned package when the repo does not carry it.
const localBin = join(workdir, "node_modules", ".bin", "prisma-composer");
const [composerCmd, composerLead] = existsSync(localBin)
? [localBin, []]
: ["npx", [`--package=@prisma/composer-cli@${composerVersion}`, "prisma-composer"]];
log(composerCmd === localBin ? "composer=local bin" : `composer=${composerVersion} (npx fallback)`);
const [composerCmd, composerLead, composerLabel] = selectComposerCommand(
localBin,
composerVersion,
existsSync(localBin),
);
log(composerLabel);
const composerArgs = [
...composerLead,
...(mode === "deploy"
Expand Down
40 changes: 40 additions & 0 deletions tests/composer.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { selectComposerCommand } from "../composer.mjs";

test("uses bun with the local bin when it exists", () => {
const [cmd, lead, label] = selectComposerCommand(
"/work/node_modules/.bin/prisma-composer",
"0.9.0",
true,
);
assert.equal(cmd, "bun");
assert.deepEqual(lead, ["/work/node_modules/.bin/prisma-composer"]);
assert.equal(label, "composer=local bin (bun)");
});

test("falls back to bunx with --bun when the local bin is absent", () => {
const [cmd, lead, label] = selectComposerCommand(
"/work/node_modules/.bin/prisma-composer",
"0.9.0",
false,
);
assert.equal(cmd, "bunx");
assert.deepEqual(lead, [
"--bun",
"-p",
"@prisma/composer-cli@0.9.0",
"prisma-composer",
]);
assert.equal(label, "composer=0.9.0 (bunx fallback)");
});

test("bunx fallback log label includes the exact version", () => {
const [, , label] = selectComposerCommand("/any", "0.7.5", false);
assert.equal(label, "composer=0.7.5 (bunx fallback)");
});

test("local bin label is always the same string regardless of version", () => {
const [, , label] = selectComposerCommand("/any", "0.7.5", true);
assert.equal(label, "composer=local bin (bun)");
});
Loading