diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..c4130e7 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,28 @@ +name: Publish Package + +# Requires npm trusted publishing to be configured for each package. +# Minimum versions: npm >= 11.5.1, Node.js >= 22.14.0. +# See: https://docs.npmjs.com/trusted-publishers + +on: + push: + tags: + - "v*" + +permissions: + id-token: write # Required for OIDC, see https://docs.npmjs.com/trusted-publishers + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + registry-url: "https://registry.npmjs.org" + - run: npm ci + - run: npm run all + - run: node scripts/gh-diffcheck.js + - run: node scripts/publish.js diff --git a/package.json b/package.json index 1ee9136..df749eb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "all": "turbo run --ui tui build format lint test attw license-header update-readme", "setversion": "node scripts/set-workspace-version.js", "postsetversion": "npm run all", - "release": "npm run all && node scripts/release.js", "format": "biome format --write", "license-header": "license-header --ignore 'packages/**'", "lint": "biome lint --error-on-warnings" diff --git a/scripts/publish.js b/scripts/publish.js new file mode 100644 index 0000000..23c47d8 --- /dev/null +++ b/scripts/publish.js @@ -0,0 +1,136 @@ +// Copyright 2024-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { execSync } from "node:child_process"; + +/* + * Publish workspace packages to npm + * + * Recommended procedure: + * 1. Set a new version with `npm run setversion 1.2.3` + * 2. Commit and push all changes to a PR, wait for approval. + * 3. Merge the PR. + * 4. Create a release on GitHub with tag `v1.2.3`, which triggers the + * publish workflow that runs this script. + */ + +const packages = discoverPackages(); +validatePackages(packages); + +const version = packages[0].version; +gitCheckReleaseTag(version); +npmPublish(version); + +/** + * @param {string} version + */ +function npmPublish(version) { + const tag = determinePublishTag(version); + execSync(`npm publish --tag ${tag} --workspaces`, { + stdio: "inherit", + }); +} + +/** + * Validate the discovered workspace packages: at least one must exist, and + * all must share the same version. + * + * @param {DiscoveredPackage[]} packages + */ +function validatePackages(packages) { + if (packages.length === 0) { + throw new Error("No publishable packages found"); + } + const version = packages[0].version; + for (const pkg of packages) { + if (pkg.version !== version) { + throw new Error( + `Inconsistent workspace versions: ${packages[0].name}@${version} vs ${pkg.name}@${pkg.version}`, + ); + } + } +} + +/** + * Throws if the tag `v` is not among the tags pointing at HEAD. + * + * @param {string} version + */ +function gitCheckReleaseTag(version) { + const expected = `v${version}`; + const out = execSync("git tag --points-at HEAD", { + encoding: "utf-8", + }); + const tags = out + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0); + if (!tags.includes(expected)) { + throw new Error( + `Expected git tag ${expected} on HEAD, found: ${tags.join(", ") || "(none)"}`, + ); + } +} + +/** + * @param {string} version + * @returns {string} + */ +function determinePublishTag(version) { + if (/^\d+\.\d+\.\d+$/.test(version)) { + return "latest"; + } + if (/^\d+\.\d+\.\d+-alpha.*$/.test(version)) { + return "alpha"; + } + if (/^\d+\.\d+\.\d+-beta.*$/.test(version)) { + return "beta"; + } + if (/^\d+\.\d+\.\d+-rc.*$/.test(version)) { + return "rc"; + } + throw new Error(`Unable to determine publish tag from version ${version}`); +} + +/** + * @typedef {{name: string; version: string}} DiscoveredPackage + */ + +/** + * Discover all non-private workspace packages by reading their name and + * version from the npm CLI. + * + * @returns {DiscoveredPackage[]} + */ +function discoverPackages() { + const out = execSync("npm pkg get name version private --workspaces --json", { + encoding: "utf-8", + }); + const workspaces = JSON.parse(out); + /** @type {DiscoveredPackage[]} */ + const packages = []; + for (const [key, value] of Object.entries(workspaces)) { + if (value.private === true) { + continue; + } + if (typeof value.name !== "string" || value.name.length === 0) { + throw new Error(`workspace ${key} is missing "name"`); + } + if (typeof value.version !== "string" || value.version.length === 0) { + throw new Error(`workspace ${key} is missing "version"`); + } + packages.push({ name: value.name, version: value.version }); + } + return packages; +} diff --git a/scripts/release.js b/scripts/release.js deleted file mode 100644 index fde1258..0000000 --- a/scripts/release.js +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2024-2026 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { readdirSync, readFileSync } from "node:fs"; -import { join } from "node:path"; -import { existsSync } from "node:fs"; -import { execSync } from "node:child_process"; - -/* - * Publish cel-es - * - * Recommended procedure: - * 1. Set a new version with `npm run setversion 1.2.3` - * 2. Commit and push all changes to a PR, wait for approval. - * 3. Login with `npm login` - * 4. Publish to npmjs.com with `npm run release` - * 5. Merge PR and create a release on GitHub - */ - -const tag = determinePublishTag(findWorkspaceVersion("packages")); -const uncommitted = gitUncommitted(); -if (uncommitted.length > 0) { - throw new Error("Uncommitted changes found: \n" + uncommitted); -} -npmPublish(); - -/** - * - */ -function npmPublish() { - const command = - `npm publish --tag ${tag}` + - " --workspace packages/cel" + - " --workspace packages/cel-spec"; - execSync(command, { - stdio: "inherit", - }); -} - -/** - * @returns {string} - */ -function gitUncommitted() { - const out = execSync("git status --short", { - encoding: "utf-8", - }); - if (out.trim().length === 0) { - return ""; - } - return out; -} - -/** - * @param {string} version - * @returns {string} - */ -function determinePublishTag(version) { - if (/^\d+\.\d+\.\d+$/.test(version)) { - return "latest"; - } - if (/^\d+\.\d+\.\d+-alpha.*$/.test(version)) { - return "alpha"; - } - if (/^\d+\.\d+\.\d+-beta.*$/.test(version)) { - return "beta"; - } - if (/^\d+\.\d+\.\d+-rc.*$/.test(version)) { - return "rc"; - } - throw new Error(`Unable to determine publish tag from version ${version}`); -} - -/** - * @param {string} packagesDir - * @returns {string} - */ -function findWorkspaceVersion(packagesDir) { - let version = undefined; - for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { - if (!entry.isDirectory()) { - continue; - } - const path = join(packagesDir, entry.name, "package.json"); - if (existsSync(path)) { - const pkg = JSON.parse(readFileSync(path, "utf-8")); - if (pkg.private === true) { - continue; - } - if (!pkg.version) { - throw new Error(`${path} is missing "version"`); - } - if (version === undefined) { - version = pkg.version; - } else if (version !== pkg.version) { - throw new Error(`${path} has unexpected version ${pkg.version}`); - } - } - } - if (version === undefined) { - throw new Error("unable to find workspace version"); - } - return version; -} diff --git a/scripts/set-workspace-version.js b/scripts/set-workspace-version.js index 0526a51..1e6cc87 100755 --- a/scripts/set-workspace-version.js +++ b/scripts/set-workspace-version.js @@ -17,15 +17,17 @@ import { readFileSync, writeFileSync, existsSync, globSync } from "node:fs"; import { dirname, join } from "node:path"; -if (process.argv.length !== 3 || !/^\d+\.\d+\.\d+$/.test(process.argv[2])) { +if ( + process.argv.length !== 3 || + !/^\d+\.\d+\.\d+(-(?:alpha|beta|rc).*)?$/.test(process.argv[2]) +) { process.stderr.write( [ `USAGE: ${process.argv[1]} `, "", - "Walks through all workspace packages and sets the version of each ", - "package to the given version.", - "If a package depends on another package from the workspace, the", - "dependency version is updated as well.", + "Sets the version across all workspace packages. For example 1.2.3, or 2.0.0-alpha.0.", + "", + "This script exists because `npm version` does not update cross-workspace dependency entries.", "", ].join("\n"), ); @@ -187,10 +189,6 @@ function readPackage(path) { if (typeof json !== "object" || json === null) { throw new Error(`Failed to parse ${path}`); } - const lock = JSON.parse(readFileSync(path, "utf-8")); - if (typeof lock !== "object" || lock === null) { - throw new Error(`Failed to parse ${path}`); - } if (!("name" in json) || typeof json.name != "string") { throw new Error(`Missing "name" in ${path}`); } @@ -201,7 +199,7 @@ function readPackage(path) { } else if (!("private" in json) || json.private !== true) { throw new Error(`Need either "version" or "private":true in ${path}`); } - return lock; + return json; } /**