From 24a72235863e940057214567cb5220f9fb2583a0 Mon Sep 17 00:00:00 2001 From: Luan van der Westhuizen Date: Wed, 27 May 2026 13:53:53 +0200 Subject: [PATCH 1/4] chore(cli): simplify release publishing --- .github/workflows/prepare-cli-release.yml | 168 ------------------ .github/workflows/preview-cli-package.yml | 57 ++++++ .github/workflows/publish-cli.yml | 150 ++++++++++++++-- CONTRIBUTING.md | 14 +- README.md | 47 ++--- SECURITY.md | 11 ++ .../0001-preview-package-and-publishing.md | 44 +++-- .../adrs/0002-workflow-command-model.md | 2 +- docs/architecture/adrs/README.md | 2 +- docs/architecture/overview.md | 4 +- docs/architecture/package-structure.md | 2 +- docs/product/command-spec.md | 16 +- docs/product/resource-model.md | 8 +- docs/reference/glossary.md | 13 +- examples/hello-world/README.md | 4 +- examples/hello-world/server.ts | 2 +- examples/next-smoke/README.md | 2 +- examples/next-smoke/app/layout.tsx | 2 +- package.json | 1 + packages/cli/README.md | 8 +- packages/cli/package.json | 4 +- packages/cli/tests/publish-prep.test.ts | 55 +++++- pnpm-lock.yaml | 9 + scripts/prepare-cli-publish.mjs | 50 +++++- 24 files changed, 407 insertions(+), 268 deletions(-) delete mode 100644 .github/workflows/prepare-cli-release.yml create mode 100644 .github/workflows/preview-cli-package.yml create mode 100644 SECURITY.md diff --git a/.github/workflows/prepare-cli-release.yml b/.github/workflows/prepare-cli-release.yml deleted file mode 100644 index ab8439a..0000000 --- a/.github/workflows/prepare-cli-release.yml +++ /dev/null @@ -1,168 +0,0 @@ -name: Prepare CLI Release - -on: - workflow_dispatch: - inputs: - release_type: - description: Version strategy for the release PR - required: true - type: choice - default: alpha - options: - - alpha - - custom - custom_version: - description: Exact version when release_type is custom, for example 3.0.0-alpha.2 - required: false - type: string - dry_run: - description: Validate the release PR without creating a branch or pull request - required: false - type: boolean - default: false - -concurrency: - group: prepare-cli-release-main - cancel-in-progress: false - -jobs: - prepare: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - name: Ensure workflow runs from main - run: | - if [ "${GITHUB_REF}" != "refs/heads/main" ]; then - echo "This workflow only prepares releases from main." - exit 1 - fi - - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - uses: pnpm/action-setup@v5 - with: - version: 10.30.0 - - - uses: actions/setup-node@v6 - with: - node-version: 24 - registry-url: https://registry.npmjs.org - cache: pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Configure git author - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - - name: Resolve release PR version - id: cli_version - env: - RELEASE_TYPE: ${{ inputs.release_type }} - CUSTOM_VERSION: ${{ inputs.custom_version }} - run: | - if [ "${RELEASE_TYPE}" = "custom" ] && [ -z "${CUSTOM_VERSION}" ]; then - echo "custom_version is required when release_type=custom." - exit 1 - fi - - if [ "${RELEASE_TYPE}" = "alpha" ]; then - npm --prefix packages/cli version prerelease --preid alpha --no-git-tag-version >/dev/null - elif [ "${RELEASE_TYPE}" = "custom" ]; then - npm --prefix packages/cli version "${CUSTOM_VERSION}" --no-git-tag-version >/dev/null - else - echo "Unsupported release_type: ${RELEASE_TYPE}" - exit 1 - fi - - VERSION="$(node -e "process.stdout.write(JSON.parse(require('fs').readFileSync('packages/cli/package.json', 'utf8')).version)")" - printf 'version=%s\n' "${VERSION}" >> "$GITHUB_OUTPUT" - - - name: Fail if version already exists on npm - run: | - PACKAGE='@prisma/cli' - VERSION='${{ steps.cli_version.outputs.version }}' - if npm view "${PACKAGE}@${VERSION}" version >/dev/null 2>&1; then - echo "${PACKAGE}@${VERSION} already exists on npm." - exit 1 - fi - - - name: Fail if release tag already exists - run: | - VERSION='${{ steps.cli_version.outputs.version }}' - TAG="cli-v${VERSION}" - if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then - echo "Release tag ${TAG} already exists." - exit 1 - fi - - - name: Run focused CLI tests - run: pnpm --filter @prisma/cli test - - - name: Build CLI package - run: pnpm --filter @prisma/cli build - - - name: Prepare staged publish package - run: node scripts/prepare-cli-publish.mjs .publish/cli - - - name: Ensure release PR still targets the latest main - if: ${{ !inputs.dry_run }} - run: | - git fetch origin main - if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then - echo "main moved while the release PR was being prepared. Rerun the workflow from the latest main." - exit 1 - fi - - - name: Create release pull request - if: ${{ !inputs.dry_run }} - env: - GH_TOKEN: ${{ github.token }} - VERSION: ${{ steps.cli_version.outputs.version }} - run: | - BRANCH="chore/release-cli-${VERSION}" - TITLE="chore(cli): release @prisma/cli v${VERSION}" - - if git ls-remote --exit-code --heads origin "refs/heads/${BRANCH}" >/dev/null 2>&1; then - echo "Release branch ${BRANCH} already exists." - exit 1 - fi - - git checkout -b "${BRANCH}" - git add packages/cli/package.json - if git diff --cached --quiet; then - echo "No CLI version change was produced." - exit 1 - fi - - git commit -m "${TITLE}" - git push origin "HEAD:${BRANCH}" - gh pr create \ - --base main \ - --head "${BRANCH}" \ - --title "${TITLE}" \ - --body "Bumps \`@prisma/cli\` to \`${VERSION}\` for the next preview release." - - - name: Summarize release preparation - run: | - VERSION='${{ steps.cli_version.outputs.version }}' - { - echo "## Prepare CLI Release" - echo - echo "- Version: \`${VERSION}\`" - echo "- Dry run: \`${{ inputs.dry_run }}\`" - if [ '${{ inputs.dry_run }}' = 'true' ]; then - echo "- Branch: skipped" - echo "- Pull request: skipped" - else - echo "- Branch: \`chore/release-cli-${VERSION}\`" - echo "- Pull request: created" - fi - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/preview-cli-package.yml b/.github/workflows/preview-cli-package.yml new file mode 100644 index 0000000..0fb967c --- /dev/null +++ b/.github/workflows/preview-cli-package.yml @@ -0,0 +1,57 @@ +name: Preview CLI Package + +on: + pull_request: + types: + - opened + - synchronize + - reopened + +concurrency: + group: preview-cli-package-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + preview: + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: pnpm/action-setup@v5 + with: + version: 10.30.0 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Resolve preview version + id: cli_version + run: | + HEAD_SHA='${{ github.event.pull_request.head.sha }}' + SHORT_SHA="${HEAD_SHA::12}" + VERSION="3.0.0-pr.${{ github.event.pull_request.number }}.sha${SHORT_SHA}" + printf 'version=%s\n' "${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Run focused CLI tests + run: pnpm --filter @prisma/cli test + + - name: Build CLI package + run: pnpm --filter @prisma/cli build + + - name: Prepare staged preview package + run: node scripts/prepare-cli-publish.mjs .publish/cli --version '${{ steps.cli_version.outputs.version }}' + + - name: Publish installable PR preview + run: pnpm exec pkg-pr-new publish --bin --comment=update .publish/cli diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 825f028..ceb5f6a 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -1,20 +1,130 @@ -name: Release CLI +name: Publish CLI on: + push: + branches: + - main workflow_dispatch: inputs: dry_run: - description: Validate the release without publishing, tagging, or pushing + description: Validate the next official beta release without publishing or tagging required: false type: boolean default: false concurrency: - group: release-cli-main + group: publish-cli-${{ github.event_name }} cancel-in-progress: false jobs: - release: + publish-dev: + if: ${{ github.event_name == 'push' }} + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v5 + + - uses: pnpm/action-setup@v5 + with: + version: 10.30.0 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Resolve dev version + id: cli_version + run: | + SHORT_SHA="${GITHUB_SHA::12}" + VERSION="3.0.0-dev.${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}.sha${SHORT_SHA}" + printf 'version=%s\n' "${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Fail if version already exists on npm + run: | + PACKAGE='@prisma/cli' + VERSION='${{ steps.cli_version.outputs.version }}' + if npm view "${PACKAGE}@${VERSION}" version >/dev/null 2>&1; then + echo "${PACKAGE}@${VERSION} already exists on npm." + exit 1 + fi + + - name: Run focused CLI tests + run: pnpm --filter @prisma/cli test + + - name: Build CLI package + run: pnpm --filter @prisma/cli build + + - name: Prepare staged publish package + run: node scripts/prepare-cli-publish.mjs .publish/cli --version '${{ steps.cli_version.outputs.version }}' + + - name: Audit staged package contents + working-directory: .publish/cli + run: | + PACK_JSON="$(npm pack --dry-run --json)" + PACK_JSON="${PACK_JSON}" node -e " + const pack = JSON.parse(process.env.PACK_JSON)[0] + const files = pack.files.map((file) => file.path).sort() + const forbidden = files.filter((file) => + file.startsWith('src/') || + file.startsWith('tests/') || + file.startsWith('fixtures/') || + file.startsWith('docs/') || + file.startsWith('.prisma/') || + file.startsWith('.publish/') + ) + if (forbidden.length) { + console.error('Forbidden files in npm package:', forbidden.join(', ')) + process.exit(1) + } + for (const required of ['dist/cli.js', 'README.md', 'LICENSE', 'package.json']) { + if (!files.includes(required)) { + console.error('Missing required package file:', required) + process.exit(1) + } + } + " + + - name: Smoke test staged tarball install + run: | + TARBALL="$(cd .publish/cli && npm pack --silent)" + TMPDIR="$(mktemp -d)" + cat > "${TMPDIR}/package.json" <<'EOF' + { + "name": "cli-publish-smoke", + "private": true + } + EOF + pnpm add -D "${PWD}/.publish/cli/${TARBALL}" --dir "${TMPDIR}" + ( + cd "${TMPDIR}" + pnpm prisma-cli --help + pnpm prisma-cli auth whoami --json + ) + + - name: Publish dev package to npm + working-directory: .publish/cli + run: npm publish --access public --tag dev --provenance + + - name: Summarize dev publish + run: | + VERSION='${{ steps.cli_version.outputs.version }}' + { + echo "## Publish CLI Dev" + echo + echo "- npm package: \`@prisma/cli@${VERSION}\`" + echo "- npm dist-tag: \`dev\`" + } >> "$GITHUB_STEP_SUMMARY" + + publish-official: + if: ${{ github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest permissions: contents: write @@ -24,7 +134,7 @@ jobs: - name: Ensure workflow runs from main run: | if [ "${GITHUB_REF}" != "refs/heads/main" ]; then - echo "This workflow only releases from main." + echo "This workflow only publishes official releases from main." exit 1 fi @@ -45,10 +155,24 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Resolve CLI version + - name: Resolve next beta version id: cli_version run: | - VERSION="$(node -e "process.stdout.write(JSON.parse(require('fs').readFileSync('packages/cli/package.json', 'utf8')).version)")" + PACKAGE='@prisma/cli' + LATEST="$(npm view "${PACKAGE}" dist-tags.latest --silent 2>/dev/null || true)" + + if [[ -z "${LATEST}" || "${LATEST}" =~ ^2\. ]]; then + VERSION="3.0.0-beta.0" + elif [[ "${LATEST}" =~ ^3\.0\.0-beta\.([0-9]+)$ ]]; then + NEXT_NUMBER="$((${BASH_REMATCH[1]} + 1))" + VERSION="3.0.0-beta.${NEXT_NUMBER}" + else + echo "Cannot compute the next beta from npm latest (${LATEST})." + echo "Expected no latest, a 2.x legacy latest, or 3.0.0-beta.N." + exit 1 + fi + + printf 'latest=%s\n' "${LATEST}" >> "$GITHUB_OUTPUT" printf 'version=%s\n' "${VERSION}" >> "$GITHUB_OUTPUT" - name: Fail if version already exists on npm @@ -76,7 +200,7 @@ jobs: run: pnpm --filter @prisma/cli build - name: Prepare staged publish package - run: node scripts/prepare-cli-publish.mjs .publish/cli + run: node scripts/prepare-cli-publish.mjs .publish/cli --version '${{ steps.cli_version.outputs.version }}' - name: Audit staged package contents working-directory: .publish/cli @@ -131,10 +255,10 @@ jobs: exit 1 fi - - name: Publish to npm + - name: Publish official package to npm if: ${{ !inputs.dry_run }} working-directory: .publish/cli - run: npm publish --access public --tag preview --provenance + run: npm publish --access public --tag latest --provenance - name: Create and push release tag if: ${{ !inputs.dry_run }} @@ -147,9 +271,11 @@ jobs: - name: Summarize release run: | VERSION='${{ steps.cli_version.outputs.version }}' + LATEST='${{ steps.cli_version.outputs.latest }}' { - echo "## Release CLI" + echo "## Publish CLI Official" echo + echo "- Previous npm latest: \`${LATEST:-none}\`" echo "- Version: \`${VERSION}\`" echo "- Dry run: \`${{ inputs.dry_run }}\`" if [ '${{ inputs.dry_run }}' = 'true' ]; then @@ -157,7 +283,7 @@ jobs: echo "- Tag/push: skipped" else echo "- npm package: \`@prisma/cli@${VERSION}\`" - echo "- npm dist-tag: \`preview\`" + echo "- npm dist-tag: \`latest\`" echo "- git tag: \`cli-v${VERSION}\`" fi } >> "$GITHUB_STEP_SUMMARY" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 789561e..8cdbb31 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,15 +58,19 @@ The CLI must preserve the unified command model: - Keep the canonical command shape as `prisma `. - Keep stdout for machine-readable data and stderr for human-oriented status. -## Package Preview +## Package Channels -Preview releases use `@prisma/cli` and expose the `prisma-cli` binary. The -release workflow is configured to publish prerelease versions under the -`preview` dist-tag until the CLI is ready for broader use. +Official beta releases use `@prisma/cli` and expose the `prisma-cli` binary. +The `latest` dist-tag points at the latest manually published beta. + +The `dev` dist-tag points at the latest successful `main` build. Trusted +same-repo pull requests receive pkg.pr.new preview comments for testing exact +unmerged commits. Fork pull requests do not publish preview packages +automatically. Do not publish from a local checkout unless the release owner has explicitly asked you to do so. Release publishing is intended to happen through the -configured GitHub Actions workflow. +configured `Publish CLI` GitHub Actions workflow. ## Pull Requests diff --git a/README.md b/README.md index 8e72cce..ddd46c1 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ # Prisma CLI -Preview of the unified Prisma CLI. +Beta of the unified Prisma CLI. This repository contains the unified Prisma command-line experience. The current implementation focuses on app deployment workflows while preserving the long-term command model for Prisma projects, branches, schemas, databases, and apps. -Preview releases use `@prisma/cli` under the `preview` dist-tag during early -development. The package exposes a `prisma-cli` binary so it can coexist with -the existing `prisma` executable. +Official beta releases use the primary `@prisma/cli` package line. The package +exposes a `prisma-cli` binary so it can coexist with the existing `prisma` +executable. ## Install ```bash -pnpm add -D @prisma/cli@preview +pnpm add -D @prisma/cli pnpm prisma-cli --help ``` @@ -95,7 +95,7 @@ The canonical command shape is: prisma ``` -The preview package includes app build, run, deploy, environment-variable, +The beta package includes app build, run, deploy, environment-variable, deployment inspection, promotion, rollback, and removal commands. The product model intentionally keeps room for future schema, database, and migration workflows without introducing product-specific namespaces. @@ -120,7 +120,7 @@ See `ARCHITECTURE.md` for the short architecture entrypoint. ## Community -Issues and feedback are welcome while the CLI is in public preview. Pull +Issues and feedback are welcome while the CLI is in public beta. Pull requests should be tied to an existing issue or maintainer agreement so product behavior, docs, and tests stay aligned. @@ -139,22 +139,29 @@ inside an example only when you want to run manual end-to-end checks. ## Publishing -Publishing is intentionally manual and gated through GitHub Actions. The -workflow publishes the version that is already merged in -`packages/cli/package.json`; it does not bump versions or push commits to -`main`. +Publishing happens through the `Publish CLI` GitHub Actions workflow. Do not +publish from a local checkout unless the release owner explicitly asks you to do +so. -The prerelease line uses `3.0.0-alpha.N`. The release workflow is configured to -publish to the `preview` dist-tag. Do not publish from a local checkout unless -the release owner explicitly asks you to do so. +The committed `packages/cli/package.json` version is a development placeholder. +Release versions are injected by CI when the staged npm package is prepared. -For a preview release: +Release channels: -1. Run the `Prepare CLI Release` GitHub Actions workflow with `dry_run: true`. -2. Run the same workflow with `dry_run: false` to open a version bump PR. -3. Merge the PR to `main`. -4. Run the `Release CLI` GitHub Actions workflow with `dry_run: true`. -5. Run the same workflow with `dry_run: false`. +- `@prisma/cli` / `latest`: official beta releases. Run `Publish CLI` manually; + it computes the next `3.0.0-beta.N`, publishes with the `latest` dist-tag, and + creates `cli-v`. +- `@prisma/cli@dev`: latest successful `main` build. Every push to `main` + publishes a unique dev version with the `dev` dist-tag. +- PR preview packages: trusted same-repo pull requests get an installable + pkg.pr.new comment for the exact commit. Fork PRs do not publish preview + packages automatically. + +For an official beta release: + +1. Run `Publish CLI` with `dry_run: true`. +2. Check the computed version and package checks. +3. Run `Publish CLI` with `dry_run: false`. If a release workflow fails after the npm publish step, check npm before rerunning. The package version may already be published even if tag creation diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..934a9b7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +Please do not report security-sensitive issues in public GitHub issues. + +If you believe you found a vulnerability in the Prisma CLI, report it through +Prisma's security process: + +- Email: security@prisma.io +- Include affected versions, reproduction steps, and impact where possible. + +We will coordinate disclosure and remediation through the security channel. diff --git a/docs/architecture/adrs/0001-preview-package-and-publishing.md b/docs/architecture/adrs/0001-preview-package-and-publishing.md index c810c06..e9344aa 100644 --- a/docs/architecture/adrs/0001-preview-package-and-publishing.md +++ b/docs/architecture/adrs/0001-preview-package-and-publishing.md @@ -1,4 +1,4 @@ -# ADR 0001 - Preview Package And Publishing +# ADR 0001 - Package Channels And Publishing ## Status @@ -6,40 +6,46 @@ Accepted ## Context -The Prisma CLI preview needs a public package identity that can coexist with the -existing Prisma CLI while development moves quickly. Contributors also need to -understand how release preparation works without assuming local publishing is -expected. +The new Prisma CLI needs the primary `@prisma/cli` package identity while the +team is still iterating through beta. Contributors also need clear test channels +for integrated `main` builds and unmerged trusted pull requests without assuming +local publishing is expected. ## Decision -Preview releases use the `@prisma/cli` package name and the `preview` npm +Official beta releases use the `@prisma/cli` package name and the `latest` npm dist-tag. The package exposes a `prisma-cli` binary so it can coexist with the existing `prisma` executable. -Release preparation is staged through the repository scripts and manual GitHub -Actions workflows. Version bumps are merged through pull requests before -publishing. The prepare workflow opens the version bump PR, and the publish -workflow reads the already-merged version from `packages/cli/package.json`, -publishes it, and creates the matching release tag. The publish workflow must -not push release commits directly to `main`. +The committed `packages/cli/package.json` version is a development placeholder. +Release versions are injected into the staged package by CI: -The publish workflow is prepared for npm trusted publishing with provenance and -publishes with: +- Manual official releases compute the next `3.0.0-beta.N`, publish to + `latest`, and create `cli-v`. +- Pushes to `main` publish unique dev builds to the `dev` dist-tag. +- Trusted same-repo pull requests publish installable pkg.pr.new previews for + the exact commit. Fork pull requests do not publish preview packages + automatically. + +The publish workflow is prepared for npm trusted publishing with provenance. +Official releases publish with: ```bash -npm publish --access public --tag preview --provenance +npm publish --access public --tag latest --provenance ``` Local development should build and stage the package, but should not publish it. ## Consequences -- Public docs should refer to `@prisma/cli@preview` for preview package usage. +- Public docs should refer to `@prisma/cli` for official beta package usage. +- Team testing can use `@prisma/cli@dev` for latest integrated `main` or PR + preview comments for exact unmerged commits. - Project scripts may map `prisma` to `prisma-cli` when testing the future command shape locally. - The npm package should contain only the staged package files: built `dist`, package README, license, and package manifest. -- Publishing remains manual and gated until the release owner runs the workflow. -- Preview version changes must be reviewed and merged before publishing so - repository rules that require pull requests are respected. +- Official publishing remains manual and gated until a maintainer runs the + workflow. +- Release version bumps are not committed through pull requests; npm versions + and `cli-v` tags are the release record. diff --git a/docs/architecture/adrs/0002-workflow-command-model.md b/docs/architecture/adrs/0002-workflow-command-model.md index 4731d61..853450f 100644 --- a/docs/architecture/adrs/0002-workflow-command-model.md +++ b/docs/architecture/adrs/0002-workflow-command-model.md @@ -18,7 +18,7 @@ Commands are grouped by developer workflow. The canonical command shape is: prisma ``` -The preview implementation includes only these groups: +The beta implementation includes only these groups: - `auth` - `project` diff --git a/docs/architecture/adrs/README.md b/docs/architecture/adrs/README.md index d74133e..c32c957 100644 --- a/docs/architecture/adrs/README.md +++ b/docs/architecture/adrs/README.md @@ -11,7 +11,7 @@ long-term architecture boundaries. | ADR | Status | Decision | | --- | --- | --- | -| [0001](0001-preview-package-and-publishing.md) | Accepted | Use `@prisma/cli` with the `preview` dist-tag and `prisma-cli` binary for public preview releases. | +| [0001](0001-preview-package-and-publishing.md) | Accepted | Use `@prisma/cli` latest for official beta releases, `dev` for integrated main builds, and PR previews for trusted unmerged work. | | [0002](0002-workflow-command-model.md) | Accepted | Group commands by developer workflow using `prisma `. | | [0003](0003-structured-output-and-errors.md) | Accepted | Treat structured output and stable error codes as public contracts. | diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 3d62d9a..eb2b036 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -59,9 +59,9 @@ Local state boundaries are also explicit: - Active branch and app selection are local CLI state. - Secret values must not be printed in human output or structured output. -## Public Preview Constraints +## Public Beta Constraints -The preview package should remain small and predictable: +The beta package should remain small and predictable: - The implemented command groups are `auth`, `project`, `branch`, and `app`. - The CLI must not introduce product-specific namespaces. diff --git a/docs/architecture/package-structure.md b/docs/architecture/package-structure.md index 77b7efe..564f137 100644 --- a/docs/architecture/package-structure.md +++ b/docs/architecture/package-structure.md @@ -2,7 +2,7 @@ The repository currently contains one publishable package: -- `packages/cli`: the public Prisma CLI preview package +- `packages/cli`: the public Prisma CLI beta package The root workspace owns shared scripts, docs, release preparation, and examples. diff --git a/docs/product/command-spec.md b/docs/product/command-spec.md index 57ccaa7..5346143 100644 --- a/docs/product/command-spec.md +++ b/docs/product/command-spec.md @@ -1,13 +1,13 @@ -# Prisma CLI Preview Command Spec +# Prisma CLI Beta Command Spec ## Purpose -This document defines the public preview command surface. It is the source of +This document defines the public beta command surface. It is the source of truth for command names, target resolution, and structured behavior. ## Scope -The preview package includes these command groups: +The beta package includes these command groups: - `auth` - `project` (includes `project env` subgroup) @@ -15,7 +15,7 @@ The preview package includes these command groups: - `branch` - `app` -The preview package also includes one top-level utility command: +The beta package also includes one top-level utility command: - `version` @@ -24,7 +24,7 @@ The preview package also includes one top-level utility command: The Git repository connection slice uses the `git` group. It does not add a provider-specific `GitHub` group. -Out of scope for the current preview: +Out of scope for the current beta: - `init` - `schema` @@ -193,7 +193,7 @@ In `--json`, `result` uses this shape: { "cli": { "name": "prisma-cli", - "version": "3.0.0-alpha.3" + "version": "3.0.0-beta.0" }, "node": { "version": "v24.14.1" @@ -208,7 +208,7 @@ In `--json`, `result` uses this shape: Rules: -- `cli.name` is the published package's `bin` name (`prisma-cli` in the current preview). +- `cli.name` is the published package's `bin` name (`prisma-cli` in the current beta). - `cli.version` is the published package version. - `node.version` mirrors `process.version` exactly, including the leading `v`. - `os.platform` and `os.arch` mirror `process.platform` and `process.arch`. @@ -466,7 +466,7 @@ Purpose: Behavior: - detects supported project shapes when `--build-type auto` is used -- supports Bun, Next.js, Nuxt, Astro, and TanStack Start app builds in the preview package +- supports Bun, Next.js, Nuxt, Astro, and TanStack Start app builds in the beta package - fails with `USAGE_ERROR` when framework detection is ambiguous Examples: diff --git a/docs/product/resource-model.md b/docs/product/resource-model.md index 280482b..2b38fca 100644 --- a/docs/product/resource-model.md +++ b/docs/product/resource-model.md @@ -103,7 +103,7 @@ Rules: - the app name is registered within the resolved project - the runtime app service is scoped by branch in the platform model - the app may be selected or created as part of app deployment workflows -- app selection is local CLI state when needed for the preview package +- app selection is local CLI state when needed for the beta package ### Deployment @@ -134,7 +134,7 @@ It matters because: ### Environment Variables Environment variables are deploy-time inputs, not top-level CLI resources in the -preview command model. +beta command model. Rules: @@ -150,13 +150,13 @@ top-level target-context group is `branch`, not `env`. ### Schema and Database -`schema` and `database` are out of scope for the current preview package, but +`schema` and `database` are out of scope for the current beta package, but they remain part of the long-term hierarchy. - `schema` stays a local code artifact - `database` stays a branch-bound resource -The preview package must not redefine project or branch in a way that makes +The beta package must not redefine project or branch in a way that makes future schema, database, and migration workflows awkward. ## Relationships diff --git a/docs/reference/glossary.md b/docs/reference/glossary.md index 478a477..5098529 100644 --- a/docs/reference/glossary.md +++ b/docs/reference/glossary.md @@ -17,14 +17,16 @@ output, and implementation. | App | Deployable runtime workload for a project branch. | [Resource model](../product/resource-model.md) | | Deployment | One build-and-release instance of an app. | [Resource model](../product/resource-model.md) | | Source revision | Code state a deployment was built from. | [Resource model](../product/resource-model.md) | -| Schema | Local data model in the codebase. Out of scope for the current preview package. | [Resource model](../product/resource-model.md) | -| Database | Branch-bound data store. Out of scope for the current preview package. | [Resource model](../product/resource-model.md) | +| Schema | Local data model in the codebase. Out of scope for the current beta package. | [Resource model](../product/resource-model.md) | +| Database | Branch-bound data store. Out of scope for the current beta package. | [Resource model](../product/resource-model.md) | | Command group | First command segment after `prisma`, such as `auth` or `app`. | [Command spec](../product/command-spec.md) | | Action | Operation inside a command group, such as `deploy` or `whoami`. | [Command spec](../product/command-spec.md) | | Structured output | Explicit `--json` output intended for automation. | [Output conventions](../product/output-conventions.md) | | Human output | Status, prompts, summaries, and decoration intended for terminal users. | [Output conventions](../product/output-conventions.md) | | Error code | Stable machine-readable failure code. | [Error conventions](../product/error-conventions.md) | -| Preview package | Public prerelease package line for `@prisma/cli` on the `preview` dist-tag. | [ADR 0001](../architecture/adrs/0001-preview-package-and-publishing.md) | +| Beta package | Public prerelease package line for `@prisma/cli` on the `latest` dist-tag. | [ADR 0001](../architecture/adrs/0001-preview-package-and-publishing.md) | +| Dev package | Latest successful `main` build of `@prisma/cli` on the `dev` dist-tag. | [ADR 0001](../architecture/adrs/0001-preview-package-and-publishing.md) | +| PR preview package | Installable pkg.pr.new package for a trusted same-repo pull request commit. | [ADR 0001](../architecture/adrs/0001-preview-package-and-publishing.md) | ## Terminology Alignment @@ -34,7 +36,8 @@ output, and implementation. | App | Project | App and project have different lifecycle and selection rules. | | Branch | Legacy target wording | Branch is the project-scoped isolation boundary; `env` is reserved for environment variables. | | Preview branch | Legacy preview target wording | Preview branch is the documented product term. | -| `@prisma/cli@preview` | Other prerelease dist-tags | The preview dist-tag avoids confusion with unrelated package names. | -| `prisma-cli` binary | `prisma` binary | The preview package binary coexists with the existing Prisma CLI. | +| `@prisma/cli` | `@prisma/cli@preview` | The primary package line now resolves to the official beta CLI. | +| `@prisma/cli@dev` | Branch-specific npm tags | The dev dist-tag means latest integrated `main`; PR previews cover exact unmerged commits. | +| `prisma-cli` binary | `prisma` binary | The beta package binary coexists with the existing Prisma CLI. | | Structured output | Agent output | JSON output is for all automation, not only agents. | | Error code | Error message string | Automation should branch on stable codes, not prose. | diff --git a/examples/hello-world/README.md b/examples/hello-world/README.md index d70dc74..5e1bac0 100644 --- a/examples/hello-world/README.md +++ b/examples/hello-world/README.md @@ -32,7 +32,7 @@ Fresh external scaffold: mkdir my-bun-app cd my-bun-app bun init --yes -pnpm add -D @prisma/cli@preview +pnpm add -D @prisma/cli ``` Then replace `index.ts` with a `Bun.serve(...)` server and run: @@ -53,7 +53,7 @@ What this validates: - first deploy can carry deploy-time environment variables like `DATABASE_URL` - second deploy reuses saved local app selection from `.prisma/cli/state.json` - `app list-env` shows variable names without exposing values -- the preview build flow can package and deploy a simple Bun server +- the beta build flow can package and deploy a simple Bun server Local files intentionally ignored in this example: diff --git a/examples/hello-world/server.ts b/examples/hello-world/server.ts index b0b1368..d977a74 100644 --- a/examples/hello-world/server.ts +++ b/examples/hello-world/server.ts @@ -12,7 +12,7 @@ const server = Bun.serve({ }); } - return new Response("Hello from the Prisma CLI preview!\n", { + return new Response("Hello from the Prisma CLI beta!\n", { headers: { "content-type": "text/plain; charset=utf-8", }, diff --git a/examples/next-smoke/README.md b/examples/next-smoke/README.md index 4601d73..c9bf248 100644 --- a/examples/next-smoke/README.md +++ b/examples/next-smoke/README.md @@ -4,7 +4,7 @@ Manual smoke app for exercising the local source Prisma CLI from inside this rep This example is intentionally not part of the root pnpm workspace. Install it only when you want to run manual end-to-end checks. -This example already sets `output: "standalone"` in `next.config.ts`, which is required for Next.js deploys in the current preview. +This example already sets `output: "standalone"` in `next.config.ts`, which is required for Next.js deploys in the current beta. ## Manual Flow diff --git a/examples/next-smoke/app/layout.tsx b/examples/next-smoke/app/layout.tsx index 765dcca..372f487 100644 --- a/examples/next-smoke/app/layout.tsx +++ b/examples/next-smoke/app/layout.tsx @@ -3,7 +3,7 @@ import "./globals.css"; export const metadata: Metadata = { title: "Next Smoke", - description: "Manual smoke app for the local Prisma CLI preview", + description: "Manual smoke app for the local Prisma CLI beta", }; export default function RootLayout({ diff --git a/package.json b/package.json index a0fb745..201a4fc 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "prisma": "tsx packages/cli/src/bin.ts" }, "devDependencies": { + "pkg-pr-new": "^0.0.75", "tsx": "^4.19.2" } } diff --git a/packages/cli/README.md b/packages/cli/README.md index c000c53..b8efcbd 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,11 +1,11 @@ -# Prisma CLI Preview +# Prisma CLI Beta -Preview npm package for the unified Prisma CLI. +Beta npm package for the unified Prisma CLI. Install: ```bash -pnpm add -D @prisma/cli@preview +pnpm add -D @prisma/cli ``` Run: @@ -22,6 +22,6 @@ executable. Notes: -- This is a preview package and may change quickly. +- This is a beta package and may change quickly. - `prisma.config.ts` stores linked project context for this CLI. - Environment variable values passed with `--env` are not printed back to the terminal. diff --git a/packages/cli/package.json b/packages/cli/package.json index b4afdf0..b2de0fb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@prisma/cli", - "version": "3.0.0-alpha.14", - "description": "Preview of the unified Prisma CLI.", + "version": "3.0.0-development", + "description": "Beta of the unified Prisma CLI.", "type": "module", "bin": { "prisma-cli": "./dist/cli.js" diff --git a/packages/cli/tests/publish-prep.test.ts b/packages/cli/tests/publish-prep.test.ts index 6e3c8c8..435ae83 100644 --- a/packages/cli/tests/publish-prep.test.ts +++ b/packages/cli/tests/publish-prep.test.ts @@ -1,11 +1,15 @@ -import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises"; +import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { createTempCwd } from "./helpers"; import { stageCliPublishPackage } from "../../../scripts/prepare-cli-publish.mjs"; +function createTempCwd(): Promise { + return mkdtemp(path.join(os.tmpdir(), "prisma-cli-")); +} + describe("prepare cli publish", () => { it("stages a public package manifest for @prisma/cli", async () => { const cwd = await createTempCwd(); @@ -20,8 +24,8 @@ describe("prepare cli publish", () => { { name: "@prisma/cli", private: true, - version: "3.0.0-alpha.0", - description: "Preview of the unified Prisma CLI.", + version: "3.0.0-development", + description: "Beta of the unified Prisma CLI.", type: "module", engines: { node: ">=20", @@ -55,8 +59,8 @@ describe("prepare cli publish", () => { expect(stagedPath).toBe(outputDir); expect(manifest).toEqual({ name: "@prisma/cli", - version: "3.0.0-alpha.0", - description: "Preview of the unified Prisma CLI.", + version: "3.0.0-development", + description: "Beta of the unified Prisma CLI.", type: "module", bin: { "prisma-cli": "./dist/cli.js", @@ -86,6 +90,41 @@ describe("prepare cli publish", () => { expect(manifest).not.toHaveProperty("private"); }); + it("uses an injected publish version when staging the package", async () => { + const cwd = await createTempCwd(); + const sourceDir = path.join(cwd, "source"); + const outputDir = path.join(cwd, "staged"); + + await mkdir(path.join(sourceDir, "dist"), { recursive: true }); + await writeFile(path.join(cwd, "LICENSE"), "Apache-2.0\n", "utf8"); + await writeFile( + path.join(sourceDir, "package.json"), + JSON.stringify( + { + name: "@prisma/cli", + version: "3.0.0-development", + description: "Beta of the unified Prisma CLI.", + type: "module", + dependencies: {}, + }, + null, + 2, + ), + "utf8", + ); + await writeFile(path.join(sourceDir, "README.md"), "# Test package\n", "utf8"); + await writeFile(path.join(sourceDir, "dist/cli.js"), "#!/usr/bin/env node\nconsole.log('ok')\n", "utf8"); + + const stagedPath = await stageCliPublishPackage({ + sourceDir, + outputDir, + publishVersion: "3.0.0-beta.0", + }); + const manifest = JSON.parse(await readFile(path.join(stagedPath, "package.json"), "utf8")); + + expect(manifest.version).toBe("3.0.0-beta.0"); + }); + it("stages only npm package files", async () => { const cwd = await createTempCwd(); const sourceDir = path.join(cwd, "source"); @@ -101,8 +140,8 @@ describe("prepare cli publish", () => { JSON.stringify( { name: "@prisma/cli", - version: "3.0.0-alpha.0", - description: "Preview of the unified Prisma CLI.", + version: "3.0.0-development", + description: "Beta of the unified Prisma CLI.", type: "module", dependencies: {}, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 465cde4..bc8c2eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + pkg-pr-new: + specifier: ^0.0.75 + version: 0.0.75 tsx: specifier: ^4.19.2 version: 4.21.0 @@ -883,6 +886,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pkg-pr-new@0.0.75: + resolution: {integrity: sha512-u9mdErTewKSMsr+ceCt8VcNuNP0ro5AXiPXhUVApuEyqr2Zlvt+DdCFBcm+yGWN8mhOdZJ27meIDbnoZgfzpOw==} + hasBin: true + pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} @@ -1771,6 +1778,8 @@ snapshots: picomatch@4.0.4: {} + pkg-pr-new@0.0.75: {} + pkg-types@2.3.0: dependencies: confbox: 0.2.4 diff --git a/scripts/prepare-cli-publish.mjs b/scripts/prepare-cli-publish.mjs index babc8d6..fa1813b 100644 --- a/scripts/prepare-cli-publish.mjs +++ b/scripts/prepare-cli-publish.mjs @@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url"; export async function stageCliPublishPackage(options = {}) { const sourceDir = options.sourceDir ?? path.join(getRepoRoot(), "packages/cli"); const outputDir = options.outputDir ?? path.join(getRepoRoot(), ".publish/cli"); + const publishVersion = options.publishVersion; await ensureBuildArtifacts(sourceDir); @@ -23,7 +24,7 @@ export async function stageCliPublishPackage(options = {}) { const publishManifest = { name: sourceManifest.name, - version: sourceManifest.version, + version: publishVersion ?? sourceManifest.version, description: sourceManifest.description, type: sourceManifest.type, bin: { @@ -74,13 +75,56 @@ function removeUndefinedFields(value) { } async function main() { - const outputDir = process.argv[2]; + const { outputDir, publishVersion } = parseCliArgs(process.argv.slice(2)); const stagedPath = await stageCliPublishPackage( - outputDir ? { outputDir: path.resolve(outputDir) } : {}, + { + ...(outputDir ? { outputDir: path.resolve(outputDir) } : {}), + ...(publishVersion ? { publishVersion } : {}), + }, ); process.stdout.write(`${stagedPath}\n`); } +function parseCliArgs(args) { + let outputDir; + let publishVersion = process.env.CLI_PUBLISH_VERSION; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + + if (arg === "--version") { + const nextArg = args[index + 1]; + if (!nextArg || nextArg.startsWith("--")) { + throw new Error("--version requires a value."); + } + publishVersion = nextArg; + index += 1; + continue; + } + + if (arg?.startsWith("--version=")) { + publishVersion = arg.slice("--version=".length); + continue; + } + + if (arg?.startsWith("--")) { + throw new Error(`Unknown option: ${arg}`); + } + + if (outputDir) { + throw new Error(`Unexpected argument: ${arg}`); + } + + outputDir = arg; + } + + if (publishVersion !== undefined && publishVersion.trim() === "") { + throw new Error("Publish version cannot be empty."); + } + + return { outputDir, publishVersion }; +} + if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { main().catch((error) => { const message = error instanceof Error ? error.stack ?? error.message : String(error); From f1110dd704a9382c429ba7b9e4ba0239fcf8662a Mon Sep 17 00:00:00 2001 From: Luan van der Westhuizen Date: Wed, 27 May 2026 14:05:58 +0200 Subject: [PATCH 2/4] ci(cli): make PR preview publishing best-effort --- .github/workflows/preview-cli-package.yml | 17 +++++++++++++++++ CONTRIBUTING.md | 3 ++- README.md | 3 ++- .../adrs/0001-preview-package-and-publishing.md | 3 ++- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/preview-cli-package.yml b/.github/workflows/preview-cli-package.yml index 0fb967c..83f8b14 100644 --- a/.github/workflows/preview-cli-package.yml +++ b/.github/workflows/preview-cli-package.yml @@ -54,4 +54,21 @@ jobs: run: node scripts/prepare-cli-publish.mjs .publish/cli --version '${{ steps.cli_version.outputs.version }}' - name: Publish installable PR preview + id: publish_preview + continue-on-error: true run: pnpm exec pkg-pr-new publish --bin --comment=update .publish/cli + + - name: Summarize PR preview publish + if: ${{ always() }} + run: | + { + echo "## Preview CLI Package" + echo + echo "- Version: \`${{ steps.cli_version.outputs.version }}\`" + echo "- Package checks: passed" + echo "- pkg.pr.new publish: \`${{ steps.publish_preview.outcome }}\`" + if [ '${{ steps.publish_preview.outcome }}' != 'success' ]; then + echo + echo "pkg.pr.new publishing is best-effort. If this failed, confirm the pkg.pr.new GitHub App is installed for this repository and rerun the workflow." + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8cdbb31..2efff09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,8 @@ The `latest` dist-tag points at the latest manually published beta. The `dev` dist-tag points at the latest successful `main` build. Trusted same-repo pull requests receive pkg.pr.new preview comments for testing exact unmerged commits. Fork pull requests do not publish preview packages -automatically. +automatically. Preview publishing is best-effort and requires the pkg.pr.new +GitHub App to be installed for this repository. Do not publish from a local checkout unless the release owner has explicitly asked you to do so. Release publishing is intended to happen through the diff --git a/README.md b/README.md index ddd46c1..4e712c7 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,8 @@ Release channels: publishes a unique dev version with the `dev` dist-tag. - PR preview packages: trusted same-repo pull requests get an installable pkg.pr.new comment for the exact commit. Fork PRs do not publish preview - packages automatically. + packages automatically. Preview publishing is best-effort and requires the + pkg.pr.new GitHub App to be installed for this repository. For an official beta release: diff --git a/docs/architecture/adrs/0001-preview-package-and-publishing.md b/docs/architecture/adrs/0001-preview-package-and-publishing.md index e9344aa..dbf19c5 100644 --- a/docs/architecture/adrs/0001-preview-package-and-publishing.md +++ b/docs/architecture/adrs/0001-preview-package-and-publishing.md @@ -25,7 +25,8 @@ Release versions are injected into the staged package by CI: - Pushes to `main` publish unique dev builds to the `dev` dist-tag. - Trusted same-repo pull requests publish installable pkg.pr.new previews for the exact commit. Fork pull requests do not publish preview packages - automatically. + automatically. Preview publishing is best-effort because it depends on the + pkg.pr.new GitHub App being installed for the repository. The publish workflow is prepared for npm trusted publishing with provenance. Official releases publish with: From 5dde917f1326e23ae1d15028ba867eaac78a53b4 Mon Sep 17 00:00:00 2001 From: Luan van der Westhuizen Date: Wed, 27 May 2026 14:29:02 +0200 Subject: [PATCH 3/4] ci(cli): test release version resolution --- .github/workflows/preview-cli-package.yml | 11 +- .github/workflows/publish-cli.yml | 22 +--- CONTRIBUTING.md | 4 +- README.md | 4 +- .../0001-preview-package-and-publishing.md | 4 +- .../cli/tests/resolve-cli-version.test.ts | 59 +++++++++ scripts/resolve-cli-version.mjs | 120 ++++++++++++++++++ 7 files changed, 198 insertions(+), 26 deletions(-) create mode 100644 packages/cli/tests/resolve-cli-version.test.ts create mode 100644 scripts/resolve-cli-version.mjs diff --git a/.github/workflows/preview-cli-package.yml b/.github/workflows/preview-cli-package.yml index 83f8b14..c58177a 100644 --- a/.github/workflows/preview-cli-package.yml +++ b/.github/workflows/preview-cli-package.yml @@ -39,10 +39,9 @@ jobs: - name: Resolve preview version id: cli_version run: | - HEAD_SHA='${{ github.event.pull_request.head.sha }}' - SHORT_SHA="${HEAD_SHA::12}" - VERSION="3.0.0-pr.${{ github.event.pull_request.number }}.sha${SHORT_SHA}" - printf 'version=%s\n' "${VERSION}" >> "$GITHUB_OUTPUT" + node scripts/resolve-cli-version.mjs pr \ + --sha '${{ github.event.pull_request.head.sha }}' \ + --pr-number '${{ github.event.pull_request.number }}' >> "$GITHUB_OUTPUT" - name: Run focused CLI tests run: pnpm --filter @prisma/cli test @@ -55,7 +54,7 @@ jobs: - name: Publish installable PR preview id: publish_preview - continue-on-error: true + continue-on-error: ${{ vars.CLI_PR_PREVIEW_REQUIRED != 'true' }} run: pnpm exec pkg-pr-new publish --bin --comment=update .publish/cli - name: Summarize PR preview publish @@ -69,6 +68,6 @@ jobs: echo "- pkg.pr.new publish: \`${{ steps.publish_preview.outcome }}\`" if [ '${{ steps.publish_preview.outcome }}' != 'success' ]; then echo - echo "pkg.pr.new publishing is best-effort. If this failed, confirm the pkg.pr.new GitHub App is installed for this repository and rerun the workflow." + echo "pkg.pr.new publishing is best-effort while \`CLI_PR_PREVIEW_REQUIRED\` is not \`true\`. If this failed, confirm the pkg.pr.new GitHub App is installed for this repository and rerun the workflow." fi } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index ceb5f6a..aef41fb 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -43,9 +43,10 @@ jobs: - name: Resolve dev version id: cli_version run: | - SHORT_SHA="${GITHUB_SHA::12}" - VERSION="3.0.0-dev.${GITHUB_RUN_NUMBER}.${GITHUB_RUN_ATTEMPT}.sha${SHORT_SHA}" - printf 'version=%s\n' "${VERSION}" >> "$GITHUB_OUTPUT" + node scripts/resolve-cli-version.mjs dev \ + --sha "${GITHUB_SHA}" \ + --run-number "${GITHUB_RUN_NUMBER}" \ + --run-attempt "${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT" - name: Fail if version already exists on npm run: | @@ -160,20 +161,7 @@ jobs: run: | PACKAGE='@prisma/cli' LATEST="$(npm view "${PACKAGE}" dist-tags.latest --silent 2>/dev/null || true)" - - if [[ -z "${LATEST}" || "${LATEST}" =~ ^2\. ]]; then - VERSION="3.0.0-beta.0" - elif [[ "${LATEST}" =~ ^3\.0\.0-beta\.([0-9]+)$ ]]; then - NEXT_NUMBER="$((${BASH_REMATCH[1]} + 1))" - VERSION="3.0.0-beta.${NEXT_NUMBER}" - else - echo "Cannot compute the next beta from npm latest (${LATEST})." - echo "Expected no latest, a 2.x legacy latest, or 3.0.0-beta.N." - exit 1 - fi - - printf 'latest=%s\n' "${LATEST}" >> "$GITHUB_OUTPUT" - printf 'version=%s\n' "${VERSION}" >> "$GITHUB_OUTPUT" + node scripts/resolve-cli-version.mjs next-beta --latest "${LATEST}" >> "$GITHUB_OUTPUT" - name: Fail if version already exists on npm run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2efff09..9fdcac7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -67,7 +67,9 @@ The `dev` dist-tag points at the latest successful `main` build. Trusted same-repo pull requests receive pkg.pr.new preview comments for testing exact unmerged commits. Fork pull requests do not publish preview packages automatically. Preview publishing is best-effort and requires the pkg.pr.new -GitHub App to be installed for this repository. +GitHub App to be installed for this repository. Once that app is installed, set +the repository variable `CLI_PR_PREVIEW_REQUIRED=true` to make preview +publishing failures block CI. Do not publish from a local checkout unless the release owner has explicitly asked you to do so. Release publishing is intended to happen through the diff --git a/README.md b/README.md index 4e712c7..88b23f3 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,9 @@ Release channels: - PR preview packages: trusted same-repo pull requests get an installable pkg.pr.new comment for the exact commit. Fork PRs do not publish preview packages automatically. Preview publishing is best-effort and requires the - pkg.pr.new GitHub App to be installed for this repository. + pkg.pr.new GitHub App to be installed for this repository. Once that app is + installed, set the repository variable `CLI_PR_PREVIEW_REQUIRED=true` to make + preview publishing failures block CI. For an official beta release: diff --git a/docs/architecture/adrs/0001-preview-package-and-publishing.md b/docs/architecture/adrs/0001-preview-package-and-publishing.md index dbf19c5..cd85cd7 100644 --- a/docs/architecture/adrs/0001-preview-package-and-publishing.md +++ b/docs/architecture/adrs/0001-preview-package-and-publishing.md @@ -26,7 +26,9 @@ Release versions are injected into the staged package by CI: - Trusted same-repo pull requests publish installable pkg.pr.new previews for the exact commit. Fork pull requests do not publish preview packages automatically. Preview publishing is best-effort because it depends on the - pkg.pr.new GitHub App being installed for the repository. + pkg.pr.new GitHub App being installed for the repository. After that app is + installed, set the repository variable `CLI_PR_PREVIEW_REQUIRED=true` to make + preview publishing failures block CI. The publish workflow is prepared for npm trusted publishing with provenance. Official releases publish with: diff --git a/packages/cli/tests/resolve-cli-version.test.ts b/packages/cli/tests/resolve-cli-version.test.ts new file mode 100644 index 0000000..e4ae51e --- /dev/null +++ b/packages/cli/tests/resolve-cli-version.test.ts @@ -0,0 +1,59 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { describe, expect, it } from "vitest"; + +import { + resolveDevVersion, + resolveNextBetaVersion, + resolvePrVersion, +} from "../../../scripts/resolve-cli-version.mjs"; + +const execFileAsync = promisify(execFile); +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../.."); +const scriptPath = path.join(repoRoot, "scripts/resolve-cli-version.mjs"); + +describe("resolve cli version", () => { + it("computes the first beta when npm latest is missing or still legacy 2.x", () => { + expect(resolveNextBetaVersion("")).toBe("3.0.0-beta.0"); + expect(resolveNextBetaVersion("2.20.1")).toBe("3.0.0-beta.0"); + }); + + it("increments the beta number from the current npm latest", () => { + expect(resolveNextBetaVersion("3.0.0-beta.0")).toBe("3.0.0-beta.1"); + }); + + it("fails when npm latest is outside the supported beta line", () => { + expect(() => resolveNextBetaVersion("3.0.0")).toThrow( + "Cannot compute the next beta from npm latest (3.0.0).", + ); + }); + + it("computes a unique dev build version", () => { + expect(resolveDevVersion({ + runNumber: "123", + runAttempt: "2", + sha: "abcdef1234567890", + })).toBe("3.0.0-dev.123.2.shaabcdef123456"); + }); + + it("computes an exact PR preview version", () => { + expect(resolvePrVersion({ + prNumber: "43", + sha: "f1110dd704a9382c429b", + })).toBe("3.0.0-pr.43.shaf1110dd704a9"); + }); + + it("prints GitHub output lines for the next beta command", async () => { + const { stdout } = await execFileAsync(process.execPath, [ + scriptPath, + "next-beta", + "--latest", + "3.0.0-beta.0", + ]); + + expect(stdout).toBe("latest=3.0.0-beta.0\nversion=3.0.0-beta.1\n"); + }); +}); diff --git a/scripts/resolve-cli-version.mjs b/scripts/resolve-cli-version.mjs new file mode 100644 index 0000000..74acdef --- /dev/null +++ b/scripts/resolve-cli-version.mjs @@ -0,0 +1,120 @@ +#!/usr/bin/env node + +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +export const CLI_RELEASE_BASE_VERSION = "3.0.0"; + +export function resolveDevVersion(options) { + const runNumber = requireValue(options.runNumber, "runNumber"); + const runAttempt = requireValue(options.runAttempt, "runAttempt"); + const sha = shortSha(requireValue(options.sha, "sha")); + + return `${CLI_RELEASE_BASE_VERSION}-dev.${runNumber}.${runAttempt}.sha${sha}`; +} + +export function resolvePrVersion(options) { + const prNumber = requireValue(options.prNumber, "prNumber"); + const sha = shortSha(requireValue(options.sha, "sha")); + + return `${CLI_RELEASE_BASE_VERSION}-pr.${prNumber}.sha${sha}`; +} + +export function resolveNextBetaVersion(latest) { + const normalizedLatest = (latest ?? "").trim(); + + if (!normalizedLatest || normalizedLatest.startsWith("2.")) { + return `${CLI_RELEASE_BASE_VERSION}-beta.0`; + } + + const betaMatch = normalizedLatest.match(/^3\.0\.0-beta\.(\d+)$/); + if (betaMatch) { + const nextNumber = Number(betaMatch[1]) + 1; + return `${CLI_RELEASE_BASE_VERSION}-beta.${nextNumber}`; + } + + throw new Error( + `Cannot compute the next beta from npm latest (${normalizedLatest}). Expected no latest, a 2.x legacy latest, or ${CLI_RELEASE_BASE_VERSION}-beta.N.`, + ); +} + +function shortSha(sha) { + return sha.slice(0, 12); +} + +function requireValue(value, name) { + if (value === undefined || value === null || String(value).trim() === "") { + throw new Error(`${name} is required.`); + } + + return String(value); +} + +function parseOptions(args) { + const options = {}; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + + if (!arg.startsWith("--")) { + throw new Error(`Unexpected argument: ${arg}`); + } + + const inlineValueIndex = arg.indexOf("="); + if (inlineValueIndex !== -1) { + options[arg.slice(2, inlineValueIndex)] = arg.slice(inlineValueIndex + 1); + continue; + } + + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) { + throw new Error(`${arg} requires a value.`); + } + + options[arg.slice(2)] = value; + index += 1; + } + + return options; +} + +function main() { + const [command, ...args] = process.argv.slice(2); + const options = parseOptions(args); + + if (command === "dev") { + process.stdout.write(`version=${resolveDevVersion({ + runNumber: options["run-number"], + runAttempt: options["run-attempt"], + sha: options.sha, + })}\n`); + return; + } + + if (command === "pr") { + process.stdout.write(`version=${resolvePrVersion({ + prNumber: options["pr-number"], + sha: options.sha, + })}\n`); + return; + } + + if (command === "next-beta") { + const latest = options.latest ?? ""; + process.stdout.write(`latest=${latest}\n`); + process.stdout.write(`version=${resolveNextBetaVersion(latest)}\n`); + return; + } + + throw new Error("Usage: resolve-cli-version.mjs [options]"); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { + try { + main(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exitCode = 1; + } +} From 225034b518be4ca4e140d26393067ea3b017a016 Mon Sep 17 00:00:00 2001 From: Luan van der Westhuizen Date: Wed, 27 May 2026 14:49:01 +0200 Subject: [PATCH 4/4] ci(cli): address release workflow review feedback --- .github/workflows/preview-cli-package.yml | 19 +++++++++++++------ .github/workflows/publish-cli.yml | 19 ++++++++++++------- .../adrs/0002-workflow-command-model.md | 5 ++++- docs/product/command-spec.md | 1 + 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/.github/workflows/preview-cli-package.yml b/.github/workflows/preview-cli-package.yml index c58177a..2e7c048 100644 --- a/.github/workflows/preview-cli-package.yml +++ b/.github/workflows/preview-cli-package.yml @@ -20,15 +20,16 @@ jobs: pull-requests: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - - uses: pnpm/action-setup@v5 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 with: version: 10.30.0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: 24 cache: pnpm @@ -44,12 +45,15 @@ jobs: --pr-number '${{ github.event.pull_request.number }}' >> "$GITHUB_OUTPUT" - name: Run focused CLI tests + id: cli_tests run: pnpm --filter @prisma/cli test - name: Build CLI package + id: cli_build run: pnpm --filter @prisma/cli build - name: Prepare staged preview package + id: prepare_preview run: node scripts/prepare-cli-publish.mjs .publish/cli --version '${{ steps.cli_version.outputs.version }}' - name: Publish installable PR preview @@ -64,9 +68,12 @@ jobs: echo "## Preview CLI Package" echo echo "- Version: \`${{ steps.cli_version.outputs.version }}\`" - echo "- Package checks: passed" - echo "- pkg.pr.new publish: \`${{ steps.publish_preview.outcome }}\`" - if [ '${{ steps.publish_preview.outcome }}' != 'success' ]; then + echo "- CLI tests: \`${{ steps.cli_tests.outcome || 'skipped' }}\`" + echo "- Build: \`${{ steps.cli_build.outcome || 'skipped' }}\`" + echo "- Package staging: \`${{ steps.prepare_preview.outcome || 'skipped' }}\`" + echo "- pkg.pr.new publish: \`${{ steps.publish_preview.outcome || 'skipped' }}\`" + PUBLISH_OUTCOME="${{ steps.publish_preview.outcome || 'skipped' }}" + if [ "${PUBLISH_OUTCOME}" != "success" ]; then echo echo "pkg.pr.new publishing is best-effort while \`CLI_PR_PREVIEW_REQUIRED\` is not \`true\`. If this failed, confirm the pkg.pr.new GitHub App is installed for this repository and rerun the workflow." fi diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index aef41fb..28f95a3 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -25,13 +25,15 @@ jobs: id-token: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + with: + persist-credentials: false - - uses: pnpm/action-setup@v5 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 with: version: 10.30.0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: 24 registry-url: https://registry.npmjs.org @@ -139,15 +141,16 @@ jobs: exit 1 fi - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd with: fetch-depth: 0 + persist-credentials: false - - uses: pnpm/action-setup@v5 + - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 with: version: 10.30.0 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: 24 registry-url: https://registry.npmjs.org @@ -250,11 +253,13 @@ jobs: - name: Create and push release tag if: ${{ !inputs.dry_run }} + env: + GITHUB_TOKEN: ${{ github.token }} run: | VERSION='${{ steps.cli_version.outputs.version }}' TAG="cli-v${VERSION}" git tag "${TAG}" - git push origin "refs/tags/${TAG}" + git -c http.https://github.com/.extraheader="AUTHORIZATION: bearer ${GITHUB_TOKEN}" push origin "refs/tags/${TAG}" - name: Summarize release run: | diff --git a/docs/architecture/adrs/0002-workflow-command-model.md b/docs/architecture/adrs/0002-workflow-command-model.md index 853450f..a81a975 100644 --- a/docs/architecture/adrs/0002-workflow-command-model.md +++ b/docs/architecture/adrs/0002-workflow-command-model.md @@ -18,10 +18,13 @@ Commands are grouped by developer workflow. The canonical command shape is: prisma ``` -The beta implementation includes only these groups: +The beta implementation includes the command groups defined in +`docs/product/command-spec.md`, which is authoritative for beta command-group +scope. At the time of this ADR, those groups are: - `auth` - `project` +- `git` - `branch` - `app` diff --git a/docs/product/command-spec.md b/docs/product/command-spec.md index 5346143..a14b076 100644 --- a/docs/product/command-spec.md +++ b/docs/product/command-spec.md @@ -4,6 +4,7 @@ This document defines the public beta command surface. It is the source of truth for command names, target resolution, and structured behavior. +This file is authoritative for command group scope during beta. ## Scope