Skip to content

Commit 3d5c6bf

Browse files
committed
Add GitHub publish workflow with trusted publishing
Signed-off-by: Ajeet D'Souza <98ajeet@gmail.com>
1 parent ef9eb4e commit 3d5c6bf

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

.github/workflows/publish.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Publish Package
2+
3+
# Requires npm trusted publishing to be configured for each package.
4+
# Minimum versions: npm >= 11.5.1, Node.js >= 22.14.0.
5+
# See: https://docs.npmjs.com/trusted-publishers
6+
7+
on:
8+
push:
9+
tags:
10+
- "v*"
11+
12+
permissions:
13+
id-token: write # Required for OIDC, see https://docs.npmjs.com/trusted-publishers
14+
contents: read
15+
16+
jobs:
17+
publish:
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v6
21+
- uses: actions/setup-node@v6
22+
with:
23+
node-version-file: .nvmrc
24+
registry-url: "https://registry.npmjs.org"
25+
- run: npm ci
26+
- run: npm run all
27+
- run: node scripts/gh-diffcheck.js
28+
- run: node scripts/publish.js

scripts/publish.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright 2024-2026 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { execSync } from "node:child_process";
16+
17+
/*
18+
* Publish workspace packages to npm
19+
*
20+
* Recommended procedure:
21+
* 1. Set a new version with `npm run setversion 1.2.3`
22+
* 2. Commit and push all changes to a PR, wait for approval.
23+
* 3. Merge the PR.
24+
* 4. Create a release on GitHub with tag `v1.2.3`, which triggers the
25+
* publish workflow that runs this script.
26+
*/
27+
28+
const packages = discoverPackages();
29+
validatePackages(packages);
30+
31+
const version = packages[0].version;
32+
gitCheckReleaseTag(version);
33+
npmPublish(version);
34+
35+
/**
36+
* @param {string} version
37+
*/
38+
function npmPublish(version) {
39+
const tag = determinePublishTag(version);
40+
execSync(`npm publish --tag ${tag} --workspaces`, {
41+
stdio: "inherit",
42+
});
43+
}
44+
45+
/**
46+
* Validate the discovered workspace packages: at least one must exist, and
47+
* all must share the same version.
48+
*
49+
* @param {DiscoveredPackage[]} packages
50+
*/
51+
function validatePackages(packages) {
52+
if (packages.length === 0) {
53+
throw new Error("No publishable packages found");
54+
}
55+
const version = packages[0].version;
56+
for (const pkg of packages) {
57+
if (pkg.version !== version) {
58+
throw new Error(
59+
`Inconsistent workspace versions: ${packages[0].name}@${version} vs ${pkg.name}@${pkg.version}`,
60+
);
61+
}
62+
}
63+
}
64+
65+
/**
66+
* Throws if the tag `v<version>` is not among the tags pointing at HEAD.
67+
*
68+
* @param {string} version
69+
*/
70+
function gitCheckReleaseTag(version) {
71+
const expected = `v${version}`;
72+
const out = execSync("git tag --points-at HEAD", {
73+
encoding: "utf-8",
74+
});
75+
const tags = out
76+
.split("\n")
77+
.map((line) => line.trim())
78+
.filter((line) => line.length > 0);
79+
if (!tags.includes(expected)) {
80+
throw new Error(
81+
`Expected git tag ${expected} on HEAD, found: ${tags.join(", ") || "(none)"}`,
82+
);
83+
}
84+
}
85+
86+
/**
87+
* @param {string} version
88+
* @returns {string}
89+
*/
90+
function determinePublishTag(version) {
91+
if (/^\d+\.\d+\.\d+$/.test(version)) {
92+
return "latest";
93+
}
94+
if (/^\d+\.\d+\.\d+-alpha.*$/.test(version)) {
95+
return "alpha";
96+
}
97+
if (/^\d+\.\d+\.\d+-beta.*$/.test(version)) {
98+
return "beta";
99+
}
100+
if (/^\d+\.\d+\.\d+-rc.*$/.test(version)) {
101+
return "rc";
102+
}
103+
throw new Error(`Unable to determine publish tag from version ${version}`);
104+
}
105+
106+
/**
107+
* @typedef {{name: string; version: string}} DiscoveredPackage
108+
*/
109+
110+
/**
111+
* Discover all non-private workspace packages by reading their name and
112+
* version from the npm CLI.
113+
*
114+
* @returns {DiscoveredPackage[]}
115+
*/
116+
function discoverPackages() {
117+
const out = execSync("npm pkg get name version private --workspaces --json", {
118+
encoding: "utf-8",
119+
});
120+
const workspaces = JSON.parse(out);
121+
/** @type {DiscoveredPackage[]} */
122+
const packages = [];
123+
for (const [key, value] of Object.entries(workspaces)) {
124+
if (value.private === true) {
125+
continue;
126+
}
127+
if (typeof value.name !== "string" || value.name.length === 0) {
128+
throw new Error(`workspace ${key} is missing "name"`);
129+
}
130+
if (typeof value.version !== "string" || value.version.length === 0) {
131+
throw new Error(`workspace ${key} is missing "version"`);
132+
}
133+
packages.push({ name: value.name, version: value.version });
134+
}
135+
return packages;
136+
}

0 commit comments

Comments
 (0)