diff --git a/.github/workflows/update-product-versions.yml b/.github/workflows/update-product-versions.yml index 725f223b..639f7cdf 100644 --- a/.github/workflows/update-product-versions.yml +++ b/.github/workflows/update-product-versions.yml @@ -1,10 +1,10 @@ name: Update product versions # When composer or prisma/prisma publish a release of a package this repo -# depends on, this workflow moves the dependency and opens a pull -# request; merging it ships a dev version of the CLI (operator rulings -# 2026-08-13 and 2026-08-17; docs/oss/release-automation.md). Only a real -# CLI release needs a human. +# depends on, this workflow moves the dependency, bumps the CLI to its next +# version and opens a pull request; merging it releases the CLI +# (docs/oss/release-automation.md). A CLI shipped behind a product release +# breaks every project on that release, so nobody has to remember to cut it. # # It follows each product's RELEASE tag, never `dev`. The dev channel # gets its dev builds from publish.yml's stamp instead, so what this @@ -52,6 +52,8 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false + # The release tags, to compare main's pins with the last release. + fetch-depth: 0 - name: Set up pnpm uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 @@ -65,50 +67,53 @@ jobs: id: compare run: node scripts/update-product-versions.mjs --channel release - # `pnpm install --no-frozen-lockfile` scoped to the lockfile - # refresh: the edit touches two manifests, and the pull request must - # carry the matching lockfile or every CI job fails on - # --frozen-lockfile. - - name: Refresh the lockfile + - name: Bump the CLI version + id: bump if: ${{ steps.compare.outputs.changed == 'true' }} run: | corepack enable - pnpm install --lockfile-only --no-frozen-lockfile + pnpm bump-version + echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" - name: Open the pull request with auto-merge if: ${{ steps.compare.outputs.changed == 'true' }} env: GH_TOKEN: ${{ secrets.DEPLOY_GITHUB_TOKEN }} SUMMARY: ${{ steps.compare.outputs.summary }} + VERSION: ${{ steps.bump.outputs.version }} run: | if [ -z "$GH_TOKEN" ]; then echo "::error::DEPLOY_GITHUB_TOKEN is not configured, so this workflow cannot open a pull request that triggers CI. See docs/oss/release-automation.md." exit 1 fi - # An open pull request from an earlier run already carries these - # versions or is about to be superseded by this one. Close it - # rather than stacking a second: two open version updates race - # each other's auto-merge and one lands on a stale lockfile. + # Every open version update or release pull request bumps the same + # version this one does, so only one of them can merge. + COMMENT="Superseded by the automatic release $VERSION, which follows the products' released versions: $SUMMARY." gh pr list --state open --json number,headRefName \ --jq '.[] | select(.headRefName | startswith("product-versions/")) | .number' \ | while read -r number; do [ -n "$number" ] || continue - gh pr close "$number" \ - --comment "Superseded by a newer product version update." \ - --delete-branch + gh pr close "$number" --comment "$COMMENT" --delete-branch + done + gh pr list --state open --json number,title \ + --jq '.[] | select(.title | startswith("Bump to version ")) | .number' \ + | while read -r number; do + [ -n "$number" ] || continue + gh pr close "$number" --comment "$COMMENT" done BRANCH="product-versions/$(date -u +%Y%m%d%H%M%S)" + TITLE="Release $VERSION: follow the products' released versions: $SUMMARY" git config user.name "willbot" git config user.email "w.a.madden+machine@gmail.com" git checkout -b "$BRANCH" - git add packages/cli/package.json packages/prisma/package.json pnpm-lock.yaml + git add -u git commit -s --trailer "Signed-off-by: Will Madden " \ - -m "Follow the products' released versions: $SUMMARY" \ - -m "Opened automatically (update-product-versions.yml); merging ships a dev version of the CLI. A real release stays a human act." + -m "$TITLE" \ + -m "Opened automatically (update-product-versions.yml); merging releases the CLI." git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$BRANCH" PR_URL=$(gh pr create \ - --title "Follow the products' released versions: $SUMMARY" \ - --body "Opened automatically by update-product-versions.yml, following the products' release dist-tags ($SUMMARY). Merging publishes a \`dev\` version of the CLI; the conformance checks on this pull request are what stand between a product release and that dev build." \ + --title "$TITLE" \ + --body "Opened automatically by update-product-versions.yml, following the products' release dist-tags ($SUMMARY). Merging releases \`prisma@$VERSION\` under \`latest\`; the checks on this pull request are what stand between a product release and that CLI release." \ --base main --head "$BRANCH") gh pr merge --auto --squash "$PR_URL" echo "Opened $PR_URL with auto-merge armed." diff --git a/README.md b/README.md index 65681a77..e1b83d3d 100644 --- a/README.md +++ b/README.md @@ -135,10 +135,7 @@ lockstep) is the release source of truth. The workflow publishes Release channels: -- `latest`: releases on the Prisma 8 RC line (`8.0.0-rc.N`). A merged - `chore(release)` PR that bumps the root version (via `pnpm bump-version`) - publishes automatically; `workflow_dispatch` is the manual escape hatch and - the dry-run path. +- `latest`: releases on the Prisma 8 RC line (`8.0.0-rc.N`). A merged PR that bumps the root version (via `pnpm bump-version`) publishes automatically: a maintainer's release PR, or the version-follow PR a product release opens; `workflow_dispatch` is the manual escape hatch and the dry-run path. - `dev`: latest successful `main` build. Every push to `main` that does not change the root version publishes `-dev.N` under the `dev` dist-tag. Commit traceability comes from npm provenance and the GitHub Actions run. diff --git a/docs/oss/release-automation.md b/docs/oss/release-automation.md index fb494655..844561de 100644 --- a/docs/oss/release-automation.md +++ b/docs/oss/release-automation.md @@ -1,6 +1,6 @@ # Release automation across the three repositories -The unified CLI is assembled from three repositories. This page explains how a new product version reaches the CLI without anyone editing a file by hand, what the one shared secret is for, and how to repair the machinery when it stops. +The unified CLI is assembled from three repositories. This page explains how a new product version reaches a released CLI without anyone editing a file or approving a release, what the one shared secret is for, and how to repair the machinery when it stops. | Repository | Publishes | Consumed by the CLI as | | --- | --- | --- | @@ -16,9 +16,10 @@ A **release** of the CLI depends only on **released** versions of the products. 1. The product repository's publish workflow finishes and sends a `repository_dispatch` event of type `product-published` to `prisma/prisma-cli`. 2. [`update-product-versions.yml`](../../.github/workflows/update-product-versions.yml) runs [`scripts/update-product-versions.mjs`](../../scripts/update-product-versions.mjs), which asks the registry for each watched package's version at its **release** dist-tag and rewrites `packages/cli/package.json` and `packages/prisma/package.json` where they differ. Every watched package publishes its releases under `latest`, pre-release versions included: they are all new packages with no pre-8 audience to protect. Holding `latest` back behind `next` matters only for the bare `prisma` name, which this repository publishes and which still serves Prisma 7 there until the deliberate cutover. Each package carries a list of candidate tags rather than one name, most-preferred first, so a product moving where it publishes does not silently freeze the CLI on an old version — and a package that publishes none of its candidates fails the run instead of looking like "nothing to update". -3. If anything changed, the workflow refreshes `pnpm-lock.yaml`, opens a pull request, and arms auto-merge. Any earlier open pull request from this workflow is closed first, so two of them cannot race each other onto a stale lockfile. -4. The pull request runs the full quality and conformance checks. When they pass, it merges, and that push to `main` publishes a `dev` build of the CLI carrying the products' **dev** versions (the dev stamp in [`publish.yml`](../../.github/workflows/publish.yml) rewrites them for that run only). -5. A real CLI release stays a human act: a maintainer cuts the `rc.N` bump, and that release ships the committed release versions. +3. If any pin changed, or `main` holds pins the last release (its `v` tag) did not ship, the workflow bumps the CLI to its next version with `pnpm bump-version` (which also refreshes `pnpm-lock.yaml`), opens a pull request titled `Release : follow the products' released versions: ...`, and arms auto-merge. Every earlier open pull request from this workflow, and every open `Bump to version` release pull request, is closed first: each bumps the same version, so only one can merge. +4. The pull request runs the full quality and conformance checks. When they pass and CodeRabbit approves it (the `main` ruleset requires one approving review), it merges, and that push to `main` changes the root version, so [`publish.yml`](../../.github/workflows/publish.yml) publishes the release under `latest` with the committed product versions, plus the usual `dev` build. + +A product release reaches a released CLI with no person in the loop, because a CLI left on an older product version breaks every project on the newer one: `prisma@8.0.0-rc.29` pinned `@prisma/orm-toolchain` 8.0.0-rc.21 for four days after the ORM's rc.22 shipped, while its release pull request waited for a review. A release for a change in this repository alone is still cut by a maintainer (see [versioning.md](versioning.md)). Everything on `main` ships with either kind of release, so `main` must always be releasable. Any commit that changes a product pin on `main`, by hand or not, is released by the next run. A **daily schedule** runs the same comparison, so a lost dispatch event delays an update by at most a day rather than indefinitely. `workflow_dispatch` runs it on demand. @@ -40,7 +41,7 @@ One fine-grained personal access token, stored under the same secret name in all Only `prisma/prisma-cli` needs to be listed even though the secret lives in three repositories. In this repository the token pushes the branch and opens the pull request. In composer and prisma/prisma it only sends the `repository_dispatch` event *to* this repository, and that endpoint needs Contents write **on the target repository** — the same permission on the same one repository. -Deliberately **not** granted: the Workflows permission. The pull request only ever touches the two manifests and the lockfile, so a change under `.github/workflows/` would be rejected — a useful limit on an unattended token. +Deliberately **not** granted: the Workflows permission. The pull request only ever touches the files `pnpm bump-version` and `update-product-versions.mjs` write, so a change under `.github/workflows/` would be rejected — a useful limit on an unattended token. **When it expires** the workflow fails: an absent secret is reported as an error before anything else runs, and an expired one fails the first `git push` or `gh` call. The catch is *where* it fails — a scheduled run nobody is watching — so the visible symptom is stale versions rather than a red pull request, and the daily backstop cannot recover on its own until the token is replaced. Record the renewal date somewhere you will see it. To replace it, generate a new token with the table above and update the secret in all three repositories; nothing in any repository names the account, so no code changes. @@ -71,7 +72,9 @@ A workflow step that must run should fail when its secret or token is missing, n | No pull request after a product release | Dispatch event not sent, or the token is missing or expired | The daily run will catch it up; check the product repository's notify step and the secret | | The workflow fails with "DEPLOY_GITHUB_TOKEN is not configured" | The secret is absent from this repository | Add it (see above) | | A pull request opens but never merges | Required checks failing | Read the checks — this is the automation working; a product release broke something | -| Two open version-update pull requests | The close-the-previous step failed | Close the older one by hand; they race each other's lockfile | +| Two open version-update pull requests | The close-the-previous step failed | Close the older one by hand; they race each other's version and lockfile | +| Checks pass but the pull request does not merge | The `main` ruleset requires one approving review, which CodeRabbit gives; it commented or was rate-limited instead | Approve the pull request by hand | +| `main` holds product pins no release shipped | A release took the version number before the follow pull request merged | The next run (the daily schedule at the latest) compares `main`'s pins with the last release's tag and opens a release for them; dispatch the workflow to do it now | | A published version never appears on the registry | npm accepted the publish but the version is not resolvable | The publish run polls for it and fails if it never appears; if that fails, re-dispatch the workflow — an already-published version is treated as done, so only the missing one publishes | | A release publish fails on the dev-build check | The committed pins are dev builds — a dev stamp was committed by mistake, or a product has no usable release | Run `node scripts/update-product-versions.mjs --channel release`; if that changes nothing, the product must publish a real release | diff --git a/docs/oss/versioning.md b/docs/oss/versioning.md index d63bfc75..c1ae99ac 100644 --- a/docs/oss/versioning.md +++ b/docs/oss/versioning.md @@ -28,7 +28,7 @@ How the packages published by *other* repositories relate to the engine's versio **A release of the CLI depends only on released versions of the products; a dev build of the CLI depends on their dev builds** (operator ruling 2026-08-17). The two channels are kept apart by two mechanisms: -- The committed manifests hold the products' **released** versions, and [`update-product-versions.yml`](../../.github/workflows/update-product-versions.yml) keeps them current — so `main` always points at released versions, and every version change is a reviewed pull request that CI has tested. A release publish uses those pins exactly as committed, which is what keeps a release reproducible from its tag. Pointing at released versions is not the same as being ready to release: a released version can still be unusable, and what proves the difference is the conformance checks passing on the release channel. +- The committed manifests hold the products' **released** versions, and [`update-product-versions.yml`](../../.github/workflows/update-product-versions.yml) keeps them current — so `main` always points at released versions, and every version change is a pull request that CI has tested. That workflow also releases the CLI, so a product release reaches `latest` without a maintainer ([release-automation.md](release-automation.md)). A release publish uses those pins exactly as committed, which is what keeps a release reproducible from its tag. Pointing at released versions is not the same as being ready to release: a released version can still be unusable, and what proves the difference is the conformance checks passing on the release channel. - A **dev** publish rewrites them to each product's `dev` dist-tag in CI, ephemerally, exactly as it rewrites the CLI's own version. The build and the conformance checks run after that rewrite, in the same run, so a broken product dev build fails the dev publish instead of shipping inside it. This is enforced, not remembered: the conformance checks refuse to publish a release whose dependencies include any `-dev.` version. `prisma@8.0.0-rc.3` shipped two of them — `@prisma/composer@0.6.0-dev.16` and `@prisma/orm-toolchain@8.0.0-rc.1-dev.40` — because nothing looked. @@ -43,7 +43,7 @@ The npm registry exposes the CLI packages under these dist-tags: - **`next`** — kept alive but no longer moved by the publish path (operator ruling 2026-08-25). Removing it would break every `prisma@next` instruction immediately, and an automated sync to `latest` would need a standing token — re-pointing a tag on an already-published version cannot authenticate over OIDC, and npm has no token permission narrower than package write, which is publish-capable. So the operator points it at the current release by hand (`npm dist-tag add prisma@ next`, same for `@prisma/cli` and the engine), accepts that it lags between such moves — a lagging `next` still resolves a complete, working release — and retires it with an announcement once `8.0.0` stable ships. New text should say plain `prisma`. - **`beta`** — reserved for hand-cut previews ahead of significant changes, published by dispatching the workflow with that dist-tag. Routine releases do not use this tag. -- **`dev`** — every routine push to `main` publishes `-dev.` here automatically (operator ruling 2026-08-13, superseding the earlier "no dev channel" ruling). The suffix derives from the workflow run number and is stamped ephemerally in CI, never committed, so release versions remain exactly what a commit says. The channel exists so a product's new version reaches a working CLI without a human: an auto-merging pull request moves the version, runs the full quality and conformance checks, and its merge ships the dev build. Today a daily scheduled run is what notices a product release; the immediate path needs a notification step in each product repository, which neither has yet. See [release automation](./release-automation.md). Only a real release — a merged `rc.N` bump publishing under `latest` — is a human act. +- **`dev`** — every routine push to `main` publishes `-dev.` here automatically (operator ruling 2026-08-13, superseding the earlier "no dev channel" ruling). The suffix derives from the workflow run number and is stamped ephemerally in CI, never committed, so release versions remain exactly what a commit says. The channel exists so a product's new version reaches a working CLI without a human: every push to `main` ships a dev build. A product release goes further and cuts a real release on its own; see [release automation](./release-automation.md). Only a release for a change in this repository alone is a human act. **Every** run of the publish workflow ships a dev build, including the one that cuts a release — the release publish is an additional half, not an alternative (operator ruling 2026-08-18). When they were alternatives, the release commit was the one merge to `main` that never reached the dev channel, so `dev` named an older version than the release until an unrelated commit landed. The dev build is published as its own version rather than by moving the `dev` tag, because OIDC trusted publishing authorises `npm publish` and nothing else. The dev channel covers `@prisma/cli` and `prisma` only. The engine has no dev builds; [Lockstep across the workspace](#lockstep-across-the-workspace) gives the reasons. @@ -70,7 +70,7 @@ This is by design. The alternatives cause silent problems: The publish workflow is **triggered by a change to the root `version`**: a push to `main` whose root `package.json` carries a different `version` than the previous tip is recognised as a release bump and ships that version under `latest` (the accompanying GitHub Release is marked pre-release on the RC line). This is what makes "merge the release PR" the publish trigger; there is no separate dispatch step. Every push publishes a `dev` build; one that changes the version publishes a release as well (operator ruling 2026-08-18). Within a publish, `@prisma/cli-engine` goes first, then `@prisma/cli` (which depends on it), then `prisma`. -**Nothing rewrites a `version` field outside a commit.** `set-version.ts` is run by `pnpm bump-version`, whose output a maintainer reviews and commits; the publish workflow never invokes it. That is what makes "the version is whatever `package.json` says" true rather than aspirational — CI has no way to ship a version no commit describes. It also keeps `pnpm-lock.yaml` honest: the lockfile records the `workspace:` specifiers that `set-version.ts` rewrites, so `bump-version` refreshes it in the same breath and the bump lands as one internally consistent commit. +**Nothing rewrites a `version` field outside a commit.** `set-version.ts` is run by `pnpm bump-version`, whose output is committed by a maintainer or by `update-product-versions.yml`; the publish workflow never invokes it. That is what makes "the version is whatever `package.json` says" true rather than aspirational — CI has no way to ship a version no commit describes. It also keeps `pnpm-lock.yaml` honest: the lockfile records the `workspace:` specifiers that `set-version.ts` rewrites, so `bump-version` refreshes it in the same breath and the bump lands as one internally consistent commit. Before anything reaches the registry, the workflow verifies the artifact it is about to ship: the grammar-completeness check (`pnpm check:grammar`) proves the assembled command tree lost nothing, and the conformance checks (`pnpm check:conformance`, [`packages/cli/scripts/conformance.ts`](../../packages/cli/scripts/conformance.ts)) prove the built output imports only declared dependencies, every mounted config-section validator survives hostile input, and the packed tarballs survive a registry consumer's install — a clean sandbox, npm with `--ignore-scripts`, unpublished workspace siblings resolved through computed `file:` overrides, every declared bin started on plain Node, the `@prisma/cli-engine` pins agreeing between the shell and every mounted family, and — on a release — no dependency being a dev build. The verified tarballs upload as workflow artifacts on every publishing run and attach to the GitHub Release on releases, so the artifact a maintainer can retrieve is the one the checks verified. The dry-run dispatch runs all of this without registry writes. @@ -78,7 +78,7 @@ Before anything reaches the registry, the workflow verifies the artifact it is a ## Procedure: cut the next release -The release cadence is one PR per release (on the RC line: one PR per `rc.N`). A maintainer: +The release cadence is one PR per release (on the RC line: one PR per `rc.N`). A product release cuts its own CLI release ([release-automation.md](release-automation.md)). For a change in this repository alone, a maintainer: 1. **Runs the [`publish-npm-version` skill](../../skills-contrib/publish-npm-version/SKILL.md)**, which drives `pnpm bump-version` in a fresh worktree off `origin/main` and opens the release PR under real maintainer credentials (so CI runs on it normally). The script reads the root version committed at HEAD, computes the next release version (`8.0.0-rc.N` → `8.0.0-rc.N+1`), and writes it to every lockstep `package.json`. `bump-version` refreshes `pnpm-lock.yaml` itself, because the lockfile records the `workspace:` pins it rewrites and a stale one fails every later frozen install. Commit both and open a `chore(release): 8.0.0-rc.N+1` PR. 2. **Reviews and merges the PR.** This is the point where a human verifies the release is intended — merging the bump PR is the deliberate act that publishes. The resulting push to `main` carries the bumped root `version`, the publish workflow detects the change, publishes under `latest`, and creates a matching GitHub Release (marked pre-release on the RC line). diff --git a/scripts/bump-version.ts b/scripts/bump-version.ts index 8db8ebf1..2cf51984 100644 --- a/scripts/bump-version.ts +++ b/scripts/bump-version.ts @@ -68,7 +68,7 @@ execFileSync("node", [setVersionScript, nextVersion], { console.log(""); console.log("Refreshing pnpm-lock.yaml to match the rewritten specifiers..."); -execFileSync("pnpm", ["install", "--lockfile-only"], { +execFileSync("pnpm", ["install", "--lockfile-only", "--no-frozen-lockfile"], { cwd: rootDir, stdio: "inherit", }); diff --git a/scripts/determine-version-utils.ts b/scripts/determine-version-utils.ts index cac4255c..e56e14dd 100644 --- a/scripts/determine-version-utils.ts +++ b/scripts/determine-version-utils.ts @@ -68,10 +68,8 @@ export interface VersionResult { /** * The version a routine main push publishes under the `dev` dist-tag - * (operator ruling 2026-08-13: a product's new version deploys as a dev - * version without a human; only a real release needs one). The run - * number makes - * the suffix monotonic per workflow run without reading the registry. + * (operator ruling 2026-08-13). The run number makes the suffix monotonic + * per workflow run without reading the registry. */ export function devVersion(base: string, runNumber: string): string { assertCanonicalBase(base); diff --git a/scripts/update-product-versions.mjs b/scripts/update-product-versions.mjs index e89b4adf..e80e1a4a 100644 --- a/scripts/update-product-versions.mjs +++ b/scripts/update-product-versions.mjs @@ -146,6 +146,60 @@ export function applyUpdates(manifests, updates) { } } +/** + * Each watched pin that differs between two sets of manifests, once per + * package. Pure; exported for tests. + * + * @param {ReadonlyArray<{ path: string; manifest: Record }>} before + * @param {ReadonlyArray<{ path: string; manifest: Record }>} after + * @returns {Array<{ name: string; from: string; to: string }>} + */ +export function pinChanges(before, after) { + const published = new Map(); + for (const { manifest } of after) { + for (const field of FIELDS) { + for (const { name } of WATCHED) { + const version = manifest[field]?.[name]; + if (version !== undefined) published.set(name, version); + } + } + } + const changes = new Map(); + for (const { name, from, to } of computeUpdates(before, published)) { + if (!changes.has(name)) changes.set(name, { name, from, to }); + } + return [...changes.values()]; +} + +/** + * The manifests as the last release shipped them, read from its tag, or + * undefined while that tag does not exist: publish.yml tags a release only + * after it publishes, so a run in between, or after a failed publish, has + * nothing to compare with. + */ +function releasedManifests() { + const { version } = JSON.parse( + readFileSync(join(rootDir, "package.json"), "utf-8"), + ); + try { + return MANIFEST_PATHS.map((path) => ({ + path, + manifest: JSON.parse( + execFileSync("git", ["show", `v${version}:${path}`], { + cwd: rootDir, + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }), + ), + })); + } catch { + console.warn( + `::warning::No tag v${version} yet (its publish is still running, or failed), so only the registry's versions are compared.`, + ); + return undefined; + } +} + const PACKAGE_ABSENT = /E404|404 Not Found/; /** @@ -244,16 +298,20 @@ function main() { const outputFile = process.env.GITHUB_OUTPUT; if (outputFile) { - // One line per package, not per manifest: both manifests carry the - // same pins, so naming each twice makes an unreadable PR title. - const summary = [ - ...new Set( - updates.map(({ name, from, to }) => `${name} ${from} -> ${to}`), - ), - ].join("; "); + // A release ships what main moved since the last release, which + // includes pins a follow pull request merged after a release that + // already took its version number. + const released = channel === "release" ? releasedManifests() : undefined; + const changes = + released === undefined + ? [...new Map(updates.map((update) => [update.name, update])).values()] + : pinChanges(released, manifests); + const summary = changes + .map(({ name, from, to }) => `${name} ${from} -> ${to}`) + .join("; "); appendFileSync( outputFile, - `changed< 0)}\nEOF\n`, + `changed< 0)}\nEOF\n`, ); appendFileSync(outputFile, `summary< { }); }); }); + +describe("pinChanges", () => { + const manifests = (cli, prisma) => [ + { path: CLI, manifest: { dependencies: cli } }, + { path: PRISMA, manifest: { dependencies: prisma } }, + ]; + + /** + * The case that needs it: a follow pull request merged after a release + * that already took its version number, so main holds pins no release + * shipped and the registry comparison finds nothing to move. + */ + it("lists each watched pin main moved since the last release, once", () => { + assert.deepEqual( + pinChanges( + manifests( + { "@prisma/orm-toolchain": "8.0.0-rc.21", execa: "^9.0.0" }, + { "@prisma/orm-toolchain": "8.0.0-rc.21" }, + ), + manifests( + { "@prisma/orm-toolchain": "8.0.0-rc.22", execa: "^9.6.1" }, + { "@prisma/orm-toolchain": "8.0.0-rc.22" }, + ), + ), + [ + { + name: "@prisma/orm-toolchain", + from: "8.0.0-rc.21", + to: "8.0.0-rc.22", + }, + ], + ); + }); + + it("finds nothing when main holds the pins the last release shipped", () => { + const pins = { "@prisma/composer-cli": "0.36.0" }; + assert.deepEqual( + pinChanges(manifests(pins, pins), manifests(pins, pins)), + [], + ); + }); +});