diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b06460c8..1dfa0548 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,6 +1,6 @@ name: Publish to npm -# Every run publishes a dev build. A run also publishes a release when +# Every normal run publishes a dev build. A run also publishes a release when # the push changed the root `version`, or a dispatch asked for one. The # two are not alternatives: when they were, the release commit never # reached the dev channel and `dev` sat on an older version than the @@ -25,18 +25,123 @@ on: required: false default: true type: boolean + engine-only: + description: "Test and publish only the engine, before its Composer and ORM consumers can update." + required: false + default: false + type: boolean concurrency: group: npm-publish cancel-in-progress: false jobs: + publish-engine: + name: Publish engine only + if: ${{ github.event_name == 'workflow_dispatch' && inputs.engine-only }} + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + pull-requests: read + id-token: write + + steps: + - name: Verify the release source + id: source + env: + GH_TOKEN: ${{ github.token }} + DIST_TAG: ${{ inputs.dist-tag }} + run: | + if [ -n "$DIST_TAG" ] && [ "$DIST_TAG" != latest ]; then + echo "Engine releases use the latest dist-tag." + exit 1 + fi + if [ "$GITHUB_REF" != refs/heads/main ]; then + pr=$(gh pr list --repo "$GITHUB_REPOSITORY" --state open --head "$GITHUB_REF_NAME" \ + --json number,headRefOid,baseRefName,isCrossRepository,reviewDecision,reviews \ + | jq -er --arg sha "$GITHUB_SHA" '.[] | select(.headRefOid == $sha and .baseRefName == "main" and .isCrossRepository == false and .reviewDecision == "APPROVED" and any(.reviews[]; .state == "APPROVED" and .commit.oid == $sha)) | .number') + echo "pr=$pr" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Set up pnpm + uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version-file: .node-version + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Verify the engine version + run: node scripts/check-engine-version.mjs origin/main + + - name: Test the engine + run: pnpm --filter @prisma/cli-engine test + + - name: Verify the packed engine + id: engine + run: | + version=$(node -p "require('./packages/cli-engine/package.json').version") + pnpm --filter @prisma/cli-engine pack --pack-destination "$RUNNER_TEMP/engine" + tarball="$RUNNER_TEMP/engine/prisma-cli-engine-$version.tgz" + sandbox=$(mktemp -d) + cd "$sandbox" + npm init -y + npm install "$tarball" --ignore-scripts --no-audit --no-fund + node --input-type=module -e 'await import("@prisma/cli-engine"); await import("@prisma/cli-engine/protocol"); await import("@prisma/cli-engine/testing");' + npm publish "$tarball" --dry-run --ignore-scripts --access public --tag latest + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tarball=$tarball" >> "$GITHUB_OUTPUT" + + - name: Upload the verified engine + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: engine-tarball + path: ${{ steps.engine.outputs.tarball }} + if-no-files-found: error + + - name: Verify the source has not changed + if: ${{ !inputs.dry-run }} + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ steps.source.outputs.pr }} + run: | + if [ -n "$PR_NUMBER" ]; then + gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \ + --json state,headRefOid,baseRefName,isCrossRepository,reviewDecision,reviews \ + | jq -e --arg sha "$GITHUB_SHA" '.state == "OPEN" and .headRefOid == $sha and .baseRefName == "main" and .isCrossRepository == false and .reviewDecision == "APPROVED" and any(.reviews[]; .state == "APPROVED" and .commit.oid == $sha)' + else + test "$(gh api "repos/$GITHUB_REPOSITORY/branches/main" --jq .commit.sha)" = "$GITHUB_SHA" + fi + + - name: Publish the verified engine + if: ${{ !inputs.dry-run }} + env: + TARBALL: ${{ steps.engine.outputs.tarball }} + NPM_CONFIG_PROVENANCE: "true" + run: bash scripts/publish-packages.sh latest "$TARBALL" + + - name: Verify the published engine resolves + if: ${{ !inputs.dry-run }} + env: + ENGINE_VERSION: ${{ steps.engine.outputs.version }} + run: node scripts/verify-published.mjs "@prisma/cli-engine@$ENGINE_VERSION" + publish: name: Publish packages to npm runs-on: ubuntu-latest # Only `main` publishes; a dry-run dispatch may validate the pipeline # from any branch. - if: ${{ github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.dry-run == 'true') }} + if: ${{ !inputs.engine-only && (github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.dry-run == 'true')) }} permissions: contents: write # Required to create the GitHub Release + tag for latest publishes id-token: write # Required for npm OIDC Trusted Publishing diff --git a/docs/oss/versioning.md b/docs/oss/versioning.md index d63bfc75..8702e7b0 100644 --- a/docs/oss/versioning.md +++ b/docs/oss/versioning.md @@ -55,7 +55,7 @@ Publishing requires: - **Push access to `main`** — pushing to `main` or merging a release PR is restricted to maintainers. - **A green run of the [`Publish to npm`](../../.github/workflows/publish.yml) workflow.** The workflow uses npm OIDC trusted publishing — no long-lived `NPM_TOKEN` exists in repository secrets, so a leaked secret cannot be used to publish out-of-band. Each published tarball carries an [npm provenance attestation](https://docs.npmjs.com/generating-provenance-statements) tying it to this repository and the workflow run that produced it. -- The workflow only publishes from `main`. Dry-runs are permitted from any branch (see "validate publish changes" below); every step that would mutate external state is independently guarded. +- CLI packages publish only from `main`. An engine-only dispatch can also publish from the current head of an approved, same-repository pull request targeting `main`. This lets the engine publish before Composer and ORM can update their exact peers. Every publishing step is guarded; dry-runs never publish. ## Mechanism: how we deliver the contract @@ -85,6 +85,16 @@ The release cadence is one PR per release (on the RC line: one PR per `rc.N`). A If the publish needs to be re-run (transient registry failure, etc.), a maintainer can dispatch the [`Publish to npm`](../../.github/workflows/publish.yml) workflow from `main` with `dist-tag: latest` and `dry-run=false`; the workflow re-publishes the version currently committed at HEAD, and because the chosen tag matches the canonical one it also re-creates the GitHub Release if it is missing. This is the same path used to cut a hand-rolled `beta` (`dist-tag=beta`, no Release). +## Procedure: release the engine before its consumers + +Dispatch `publish.yml` from the approved engine PR's branch with `engine-only=true` and `dry-run=true`. The workflow runs the engine's build, typecheck and tests, packs it with pnpm, installs the tarball into a clean npm project, and imports each public entrypoint. It does not stamp versions or publish a CLI. + +Once that passes, dispatch the same branch with `engine-only=true` and `dry-run=false`. Both source checks require an approval for the exact dispatched commit, not an approval carried over from an older PR head. The workflow publishes the verified engine tarball through trusted publishing. It keeps the existing `publish.yml` identity and records the dispatched commit in npm provenance. An already-published version is a no-op, so a rerun can still reach registry verification. + +When the PR is checked again, the engine version guard compares its source with the commit recorded in that artifact's npm provenance. The published version is accepted only while the engine remains unchanged; further changes still require a new version. + +Release Composer and ORM against that engine version, update both CLI consumers, and run full CLI conformance before merging the engine PR or cutting the CLI release. No conformance exceptions are needed. + ## Procedure: validate publish changes The publish workflow's `dry-run` mode (the input default) can be invoked from any branch to validate that the publish pipeline still works after touching `publish.yml`, `set-version.ts`, `determine-version.ts`, or the build scripts. A dry-run exercises `pnpm publish --dry-run` against both CLI packages and skips the registry publish + GitHub Release. diff --git a/package.json b/package.json index 4d0ee612..897f9db3 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "lint:fix": "biome check . --write", "bump-version": "node scripts/bump-version.ts", "test": "turbo run test", - "test:scripts": "node --test scripts/determine-version-utils.test.ts scripts/set-version-utils.test.ts scripts/bump-cli-engine-version-utils.test.ts scripts/resolve-package-version.test.mjs scripts/update-product-versions.test.mjs scripts/verify-published.test.mjs", + "test:scripts": "node --test scripts/determine-version-utils.test.ts scripts/set-version-utils.test.ts scripts/bump-cli-engine-version-utils.test.ts scripts/resolve-package-version.test.mjs scripts/update-product-versions.test.mjs scripts/verify-published.test.mjs scripts/check-engine-version.test.mjs", "typecheck": "turbo run typecheck", "prisma-cli": "tsx packages/cli/src/bin.ts", "prisma": "tsx packages/cli/src/bin.ts", diff --git a/packages/cli-engine/package.json b/packages/cli-engine/package.json index 2d15f45c..c49f9734 100644 --- a/packages/cli-engine/package.json +++ b/packages/cli-engine/package.json @@ -1,6 +1,6 @@ { "name": "@prisma/cli-engine", - "version": "0.6.2", + "version": "0.6.3", "description": "The execution engine of the unified Prisma CLI.", "type": "module", "exports": { @@ -48,7 +48,7 @@ "dependencies": { "@clack/prompts": "1.5.0", "@stricli/core": "1.3.0", - "arktype": "2.2.3", + "arktype": "^2.2.7", "c12": "3.3.4", "colorette": "^2.0.20", "package-manager-detector": "1.8.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index 5f6da732..25061a9b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,7 +49,7 @@ }, "dependencies": { "@manypkg/tools": "^2.1.2", - "@prisma/cli-engine": "workspace:0.6.2", + "@prisma/cli-engine": "workspace:0.6.3", "@prisma/composer-cli": "0.26.0", "@prisma/compute-sdk": "0.43.0", "@prisma/management-api-sdk": "1.80.0", diff --git a/packages/prisma/package.json b/packages/prisma/package.json index 3361fe89..0f5b39cd 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -50,7 +50,7 @@ }, "dependencies": { "@manypkg/tools": "^2.1.2", - "@prisma/cli-engine": "workspace:0.6.2", + "@prisma/cli-engine": "workspace:0.6.3", "@prisma/composer-cli": "0.26.0", "@prisma/compute-sdk": "0.43.0", "@prisma/management-api-sdk": "1.80.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fc6801d..b5c307fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,7 +27,7 @@ importers: specifier: ^2.1.2 version: 2.1.2 '@prisma/cli-engine': - specifier: workspace:0.6.2 + specifier: workspace:0.6.3 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.26.0 @@ -124,8 +124,8 @@ importers: specifier: 1.3.0 version: 1.3.0 arktype: - specifier: 2.2.3 - version: 2.2.3 + specifier: ^2.2.7 + version: 2.2.7 c12: specifier: 3.3.4 version: 3.3.4(magicast@0.5.3) @@ -210,7 +210,7 @@ importers: specifier: ^2.1.2 version: 2.1.2 '@prisma/cli-engine': - specifier: workspace:0.6.2 + specifier: workspace:0.6.3 version: link:../cli-engine '@prisma/composer-cli': specifier: 0.26.0 @@ -314,9 +314,15 @@ packages: '@ark/schema@0.56.2': resolution: {integrity: sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg==} + '@ark/schema@0.56.6': + resolution: {integrity: sha512-OmLv80BCF2ib+zjecrXDosip/CSnZSYBGjblhqWYZWFOzdLQka6QymHToE9u7Z/HZW2KA8Hwc8MCNXoI3uGSIA==} + '@ark/util@0.56.2': resolution: {integrity: sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ==} + '@ark/util@0.56.6': + resolution: {integrity: sha512-mHqq0f0HyDd9YadpFl25w0QECAP7d8HGIUpEC85duD+L+xr4i/CwfOid/Z7zJFFF/47oLt6+pZB9RbqWpBYTYg==} + '@aws-sdk/core@3.977.6': resolution: {integrity: sha512-QiaJV4/zDrB4ZY2mfeSXSzSTc36W16sZXcGz+SPFk0CJ26gziO0cS+4LjJUMAbdeeBOvS0k0Aq1cZpfGdUXxSw==} engines: {node: '>=20.0.0'} @@ -1943,12 +1949,18 @@ packages: anynum@1.0.1: resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + arkregex@0.0.12: + resolution: {integrity: sha512-sOxL0eX50pnRYO4OyajKZChGuuj/iHwC5tysiAaGOPBu+zJwJQckWn7/DRH2++ol5uaKpyRn7N0hKT7nDqGmgg==} + arkregex@0.0.8: resolution: {integrity: sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ==} arktype@2.2.3: resolution: {integrity: sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg==} + arktype@2.2.7: + resolution: {integrity: sha512-AbgM5SQvwCTURSR+ENfPtOG7cL5VZ0hqOVtoWyXKw2SviFddZhCBEUAjjVJYKCNFlPPR1VX5LGUKxsSqqc4Kdw==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -3157,8 +3169,14 @@ snapshots: dependencies: '@ark/util': 0.56.2 + '@ark/schema@0.56.6': + dependencies: + '@ark/util': 0.56.6 + '@ark/util@0.56.2': {} + '@ark/util@0.56.6': {} + '@aws-sdk/core@3.977.6': dependencies: '@aws-sdk/types': 3.974.6 @@ -4611,6 +4629,10 @@ snapshots: anynum@1.0.1: {} + arkregex@0.0.12: + dependencies: + '@ark/util': 0.56.6 + arkregex@0.0.8: dependencies: '@ark/util': 0.56.2 @@ -4621,6 +4643,12 @@ snapshots: '@ark/util': 0.56.2 arkregex: 0.0.8 + arktype@2.2.7: + dependencies: + '@ark/schema': 0.56.6 + '@ark/util': 0.56.6 + arkregex: 0.0.12 + assertion-error@2.0.1: {} ast-kit@3.0.0-beta.1: diff --git a/scripts/check-engine-version.mjs b/scripts/check-engine-version.mjs index da95f246..efc09c27 100644 --- a/scripts/check-engine-version.mjs +++ b/scripts/check-engine-version.mjs @@ -6,6 +6,8 @@ // that the published `@prisma/cli-engine@0.1.1` does not contain, and // `npx prisma@next` crashed on import. The registry is immutable, so a // changed engine must claim a new version (`pnpm bump-cli-engine-version`). +// An engine-first release can already be published: its npm provenance +// must identify the same engine source before that version may be reused. // // Usage: node scripts/check-engine-version.mjs @@ -30,6 +32,38 @@ function isNotFoundError(error) { const rootDir = dirname(dirname(fileURLToPath(import.meta.url))); const ENGINE_MANIFEST = "packages/cli-engine/package.json"; +const COMMIT_SHA = /^[a-f0-9]{40}$/; + +/** The registry's provenance must name this engine artifact and publisher. */ +export function publishedEngineCommit(attestations, version, integrity) { + if (!integrity?.startsWith("sha512-")) return undefined; + const digest = Buffer.from(integrity.slice(7), "base64").toString("hex"); + for (const attestation of attestations.attestations ?? []) { + if (attestation.predicateType !== "https://slsa.dev/provenance/v1") + continue; + const payload = attestation.bundle?.dsseEnvelope?.payload; + if (typeof payload !== "string") continue; + const statement = JSON.parse(Buffer.from(payload, "base64").toString()); + const build = statement.predicate?.buildDefinition; + const workflow = build?.externalParameters?.workflow; + if ( + workflow?.repository !== "https://github.com/prisma/prisma-cli" || + workflow.path !== ".github/workflows/publish.yml" || + !statement.subject?.some( + (subject) => + subject.name === `pkg:npm/%40prisma/cli-engine@${version}` && + subject.digest?.sha512 === digest, + ) + ) + continue; + const source = build.resolvedDependencies?.find((dependency) => + dependency.uri?.startsWith("git+https://github.com/prisma/prisma-cli@"), + ); + if (COMMIT_SHA.test(source?.digest?.gitCommit ?? "")) + return source.digest.gitCommit; + } + return undefined; +} /** * Whether a manifest change alters what npm publishes. devDependencies @@ -47,18 +81,20 @@ export function manifestChangeShips(base, head) { } /** - * @param {{ changedFiles: readonly string[], engineVersion: string, versionOnRegistry: boolean }} input + * @param {{ changedFiles: readonly string[], engineVersion: string, versionOnRegistry: boolean, publishedSourceMatches?: boolean }} input * @returns {string | null} the failure message, or null when the change is fine */ export function engineBumpVerdict({ changedFiles, engineVersion, versionOnRegistry, + publishedSourceMatches = false, }) { const engineChanged = changedFiles.some((file) => file.startsWith("packages/cli-engine/"), ); - if (!engineChanged || !versionOnRegistry) return null; + if (!engineChanged || !versionOnRegistry || publishedSourceMatches) + return null; return ( `packages/cli-engine changed, but its version (${engineVersion}) is already on the registry, ` + "and published versions are immutable. Run `pnpm bump-cli-engine-version ` " + @@ -66,21 +102,10 @@ export function engineBumpVerdict({ ); } -async function main() { - const baseSha = process.argv[2]; - if (!baseSha) { - console.error("Usage: node scripts/check-engine-version.mjs "); - process.exit(1); - } - - const { stdout: mergeBase } = await execFileAsync( - "git", - ["merge-base", baseSha, "HEAD"], - { cwd: rootDir }, - ); +async function changedFilesSince(baseSha) { const { stdout: diff } = await execFileAsync( "git", - ["diff", "--name-only", mergeBase.trim(), "HEAD"], + ["diff", "--name-only", baseSha, "HEAD"], { cwd: rootDir }, ); let changedFiles = diff.split("\n").filter(Boolean); @@ -88,7 +113,7 @@ async function main() { if (changedFiles.includes(ENGINE_MANIFEST)) { const { stdout: baseManifest } = await execFileAsync( "git", - ["show", `${mergeBase.trim()}:${ENGINE_MANIFEST}`], + ["show", `${baseSha}:${ENGINE_MANIFEST}`], { cwd: rootDir }, ); const headManifest = readFileSync(join(rootDir, ENGINE_MANIFEST), "utf-8"); @@ -98,6 +123,22 @@ async function main() { changedFiles = changedFiles.filter((file) => file !== ENGINE_MANIFEST); } } + return changedFiles; +} + +async function main() { + const baseSha = process.argv[2]; + if (!baseSha) { + console.error("Usage: node scripts/check-engine-version.mjs "); + process.exit(1); + } + + const { stdout: mergeBase } = await execFileAsync( + "git", + ["merge-base", baseSha, "HEAD"], + { cwd: rootDir }, + ); + const changedFiles = await changedFilesSince(mergeBase.trim()); const manifest = JSON.parse( readFileSync(join(rootDir, "packages/cli-engine/package.json"), "utf-8"), @@ -105,22 +146,50 @@ async function main() { const engineVersion = manifest.version; let versionOnRegistry = true; + let publishedManifest; try { - await execFileAsync("npm", [ + const { stdout } = await execFileAsync("npm", [ "view", `@prisma/cli-engine@${engineVersion}`, - "version", + "--json", "--prefer-online", ]); + publishedManifest = JSON.parse(stdout); } catch (error) { if (!isNotFoundError(error)) throw error; versionOnRegistry = false; } + let publishedSourceMatches = false; + if ( + versionOnRegistry && + changedFiles.some((file) => file.startsWith("packages/cli-engine/")) && + publishedManifest?.dist?.attestations?.provenance + ) { + const response = await fetch( + `https://registry.npmjs.org/-/npm/v1/attestations/@prisma%2fcli-engine@${encodeURIComponent(engineVersion)}`, + { signal: AbortSignal.timeout(15_000) }, + ); + if (!response.ok) + throw new Error(`Engine provenance lookup failed (${response.status})`); + const publishedCommit = publishedEngineCommit( + await response.json(), + engineVersion, + publishedManifest.dist.integrity, + ); + if (publishedCommit !== undefined) { + const sincePublication = await changedFilesSince(publishedCommit); + publishedSourceMatches = !sincePublication.some((file) => + file.startsWith("packages/cli-engine/"), + ); + } + } + const verdict = engineBumpVerdict({ changedFiles, engineVersion, versionOnRegistry, + publishedSourceMatches, }); if (verdict !== null) { console.error(`::error::${verdict}`); diff --git a/scripts/check-engine-version.test.mjs b/scripts/check-engine-version.test.mjs index 1237366b..c43a2bac 100644 --- a/scripts/check-engine-version.test.mjs +++ b/scripts/check-engine-version.test.mjs @@ -3,6 +3,7 @@ import { describe, it } from "node:test"; import { engineBumpVerdict, manifestChangeShips, + publishedEngineCommit, } from "./check-engine-version.mjs"; const NAMES_THE_STALE_VERSION = /0\.1\.1/; @@ -40,6 +41,18 @@ describe("engineBumpVerdict", () => { assert.match(verdict, NAMES_THE_BUMP_COMMAND); }); + it("passes when the engine matches its already-published source", () => { + assert.equal( + engineBumpVerdict({ + changedFiles: ["packages/cli-engine/package.json"], + engineVersion: "0.6.3", + versionOnRegistry: true, + publishedSourceMatches: true, + }), + null, + ); + }); + it("does not mistake other packages' paths for the engine", () => { const verdict = engineBumpVerdict({ changedFiles: ["packages/cli-engine-docs/readme.md"], @@ -50,6 +63,89 @@ describe("engineBumpVerdict", () => { }); }); +describe("publishedEngineCommit", () => { + const commit = "b".repeat(40); + const digest = "a".repeat(128); + const integrity = `sha512-${Buffer.from(digest, "hex").toString("base64")}`; + const statement = { + subject: [ + { + name: "pkg:npm/%40prisma/cli-engine@0.6.3", + digest: { sha512: digest }, + }, + ], + predicate: { + buildDefinition: { + externalParameters: { + workflow: { + repository: "https://github.com/prisma/prisma-cli", + path: ".github/workflows/publish.yml", + }, + }, + resolvedDependencies: [ + { + uri: "git+https://github.com/prisma/prisma-cli@refs/heads/engine-fix", + digest: { gitCommit: commit }, + }, + ], + }, + }, + }; + function attestations(value) { + return { + attestations: [ + { + predicateType: "https://slsa.dev/provenance/v1", + bundle: { + dsseEnvelope: { + payload: Buffer.from(JSON.stringify(value)).toString("base64"), + }, + }, + }, + ], + }; + } + + it("reads the source of the published engine artifact", () => { + assert.equal( + publishedEngineCommit(attestations(statement), "0.6.3", integrity), + commit, + ); + }); + + it("rejects another version or artifact digest", () => { + assert.equal( + publishedEngineCommit(attestations(statement), "0.6.2", integrity), + undefined, + ); + assert.equal( + publishedEngineCommit( + attestations(statement), + "0.6.3", + `sha512-${Buffer.alloc(64).toString("base64")}`, + ), + undefined, + ); + }); + + it("rejects an engine built by a different repository", () => { + const other = structuredClone(statement); + other.predicate.buildDefinition.externalParameters.workflow.repository = + "https://github.com/other/repo"; + assert.equal( + publishedEngineCommit(attestations(other), "0.6.3", integrity), + undefined, + ); + }); + + it("does not trust a missing provenance statement", () => { + assert.equal( + publishedEngineCommit({ attestations: [] }, "0.6.3", integrity), + undefined, + ); + }); +}); + describe("manifestChangeShips", () => { const base = { name: "@prisma/cli-engine", diff --git a/scripts/publish-packages.sh b/scripts/publish-packages.sh index 7c050933..eafcd8a1 100755 --- a/scripts/publish-packages.sh +++ b/scripts/publish-packages.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash -# Usage: publish-packages.sh ... +# Usage: publish-packages.sh ... # -# `pnpm publish` for each package, treating an already-published version +# `pnpm publish` for packages, or `npm publish` for already-packed tarballs. +# Treat an already-published version # as done: a re-run of a partially failed workflow run must reach the # later steps. Every other failure fails the run. @@ -12,7 +13,12 @@ tag="$1" shift for pkg in "$@"; do - if out=$(pnpm --filter "$pkg" publish --tag "$tag" --access public --no-git-checks 2>&1); then + if [[ "$pkg" == *.tgz ]]; then + publish_command=(npm publish "$pkg" --tag "$tag" --access public --ignore-scripts) + else + publish_command=(pnpm --filter "$pkg" publish --tag "$tag" --access public --no-git-checks) + fi + if out=$("${publish_command[@]}" 2>&1); then printf '%s\n' "$out" else printf '%s\n' "$out"