Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
136 changes: 136 additions & 0 deletions scripts/publish.js
Original file line number Diff line number Diff line change
@@ -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<version>` 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;
}
114 changes: 0 additions & 114 deletions scripts/release.js

This file was deleted.

18 changes: 8 additions & 10 deletions scripts/set-workspace-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]} <new-version>`,
"",
"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"),
);
Expand Down Expand Up @@ -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}`);
}
Expand All @@ -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;
}

/**
Expand Down
Loading