Skip to content

Commit 110e252

Browse files
authored
chore: Publish JS package via gha trusted publishing (#180)
<img width="805" height="377" alt="image" src="https://github.com/user-attachments/assets/4bd6e244-5a03-48ac-aebf-78558b53428d" />
1 parent 5b4b90c commit 110e252

4 files changed

Lines changed: 303 additions & 4 deletions

File tree

.github/workflows/lint.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@ jobs:
1212
steps:
1313
- uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
1414
- uses: actions/setup-python@3542bca2639a428e1796aaa6a2ffef0c0f575566 # v3.1.4
15-
- uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 # v3.0.0
15+
- name: Install pre-commit
16+
run: python -m pip install pre-commit
17+
- name: Run pre-commit
18+
run: pre-commit run --all-files

.github/workflows/publish-js.yaml

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
name: publish-js
2+
3+
concurrency:
4+
group: publish-js-${{ inputs.release_type }}-${{ inputs.branch }}
5+
cancel-in-progress: false
6+
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
release_type:
11+
description: Release type
12+
required: true
13+
default: stable
14+
type: choice
15+
options:
16+
- stable
17+
- prerelease
18+
branch:
19+
description: Branch to release from
20+
required: true
21+
default: main
22+
type: string
23+
24+
jobs:
25+
prepare-release:
26+
runs-on: ubuntu-latest
27+
timeout-minutes: 10
28+
outputs:
29+
version: ${{ steps.release_metadata.outputs.version }}
30+
release_tag: ${{ steps.release_metadata.outputs.release_tag }}
31+
branch: ${{ steps.release_metadata.outputs.branch }}
32+
commit: ${{ steps.release_metadata.outputs.commit }}
33+
release_type: ${{ steps.release_metadata.outputs.release_type }}
34+
steps:
35+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
36+
with:
37+
fetch-depth: 1
38+
ref: ${{ inputs.branch }}
39+
- name: Set up Node.js
40+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
41+
with:
42+
node-version: 22
43+
- name: Determine release metadata
44+
id: release_metadata
45+
env:
46+
RELEASE_TYPE: ${{ inputs.release_type }}
47+
TARGET_BRANCH: ${{ inputs.branch }}
48+
run: |
49+
set -euo pipefail
50+
51+
CURRENT_VERSION=$(node -p "require('./package.json').version")
52+
RELEASE_COMMIT=$(git rev-parse HEAD)
53+
54+
echo "release_type=${RELEASE_TYPE}" >> "$GITHUB_OUTPUT"
55+
echo "branch=${TARGET_BRANCH}" >> "$GITHUB_OUTPUT"
56+
echo "commit=${RELEASE_COMMIT}" >> "$GITHUB_OUTPUT"
57+
58+
if [[ "$RELEASE_TYPE" == "stable" ]]; then
59+
RELEASE_TAG="js-${CURRENT_VERSION}"
60+
61+
if git ls-remote --exit-code --tags origin "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then
62+
echo "Tag ${RELEASE_TAG} already exists on origin" >&2
63+
exit 1
64+
fi
65+
66+
echo "version=${CURRENT_VERSION}" >> "$GITHUB_OUTPUT"
67+
echo "release_tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
68+
else
69+
VERSION="${CURRENT_VERSION}-rc.${GITHUB_RUN_NUMBER}"
70+
71+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
72+
echo "release_tag=" >> "$GITHUB_OUTPUT"
73+
fi
74+
75+
publish:
76+
needs: prepare-release
77+
runs-on: ubuntu-latest
78+
timeout-minutes: 20
79+
permissions:
80+
contents: write
81+
id-token: write
82+
environment: npm-publish
83+
env:
84+
PACKAGE_NAME: autoevals
85+
VERSION: ${{ needs.prepare-release.outputs.version }}
86+
RELEASE_TAG: ${{ needs.prepare-release.outputs.release_tag }}
87+
RELEASE_TYPE: ${{ needs.prepare-release.outputs.release_type }}
88+
TARGET_BRANCH: ${{ needs.prepare-release.outputs.branch }}
89+
RELEASE_COMMIT: ${{ needs.prepare-release.outputs.commit }}
90+
steps:
91+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
92+
with:
93+
fetch-depth: 0
94+
ref: ${{ needs.prepare-release.outputs.branch }}
95+
96+
- name: Set up Node.js
97+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
98+
with:
99+
node-version: 22
100+
registry-url: https://registry.npmjs.org
101+
102+
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
103+
with:
104+
version: 10.26.2
105+
106+
- name: Check npm version availability
107+
run: |
108+
set -euo pipefail
109+
110+
if npm view "${PACKAGE_NAME}@${VERSION}" version --registry=https://registry.npmjs.org >/dev/null 2>&1; then
111+
echo "${PACKAGE_NAME}@${VERSION} already exists on npm" >&2
112+
exit 1
113+
fi
114+
115+
- name: Install dependencies
116+
run: pnpm install --frozen-lockfile
117+
118+
- name: Prepare prerelease package metadata
119+
if: ${{ env.RELEASE_TYPE == 'prerelease' }}
120+
run: |
121+
set -euo pipefail
122+
123+
node -e '
124+
const fs = require("fs");
125+
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
126+
pkg.version = process.env.VERSION;
127+
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n");
128+
'
129+
130+
- name: Build package
131+
run: pnpm run build
132+
133+
- name: Publish stable release to npm
134+
if: ${{ env.RELEASE_TYPE == 'stable' }}
135+
run: npm publish --provenance --access public
136+
137+
- name: Publish prerelease to npm
138+
if: ${{ env.RELEASE_TYPE == 'prerelease' }}
139+
run: npm publish --tag rc --provenance --access public
140+
141+
- name: Create and push stable release tag
142+
if: ${{ env.RELEASE_TYPE == 'stable' }}
143+
run: |
144+
set -euo pipefail
145+
146+
git config user.name "github-actions[bot]"
147+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
148+
git tag "${RELEASE_TAG}" "${RELEASE_COMMIT}"
149+
git push origin "${RELEASE_TAG}"
150+
151+
- name: Create GitHub release
152+
if: ${{ env.RELEASE_TYPE == 'stable' }}
153+
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
154+
env:
155+
RELEASE_TAG: ${{ env.RELEASE_TAG }}
156+
VERSION: ${{ env.VERSION }}
157+
with:
158+
script: |
159+
await github.rest.repos.createRelease({
160+
owner: context.repo.owner,
161+
repo: context.repo.repo,
162+
tag_name: process.env.RELEASE_TAG,
163+
name: `autoevals v${process.env.VERSION}`,
164+
draft: false,
165+
prerelease: false,
166+
generate_release_notes: true,
167+
});
168+
169+
- name: Summarize release
170+
run: |
171+
set -euo pipefail
172+
173+
{
174+
echo "## npm publish complete"
175+
echo
176+
echo "- Package: \`${PACKAGE_NAME}\`"
177+
echo "- Version: \`${VERSION}\`"
178+
echo "- Release type: \`${RELEASE_TYPE}\`"
179+
if [ "${RELEASE_TYPE}" = "prerelease" ]; then
180+
echo "- npm tag: \`rc\`"
181+
echo "- Install: \`npm install ${PACKAGE_NAME}@rc\`"
182+
else
183+
echo "- Git tag: \`${RELEASE_TAG}\`"
184+
echo "- Install: \`npm install ${PACKAGE_NAME}\`"
185+
fi
186+
} >> "$GITHUB_STEP_SUMMARY"

docs/PUBLISHING.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Publishing
2+
3+
This repository contains both JavaScript and Python packages. The JavaScript package (`autoevals`) is published to npm via GitHub Actions trusted publishing with provenance attestations.
4+
5+
## JavaScript npm publishing
6+
7+
The JavaScript publish workflow lives at:
8+
9+
- `.github/workflows/publish-js.yaml`
10+
11+
It supports two release types:
12+
13+
- `stable`: publishes the exact version in `package.json`
14+
- `prerelease`: publishes `<package.json version>-rc.<github run number>` with the `rc` dist-tag
15+
16+
For stable releases, the workflow also:
17+
18+
- creates and pushes a git tag named `js-<version>`
19+
- creates a GitHub Release named `autoevals v<version>`
20+
21+
## npm trusted publishing setup
22+
23+
Configure trusted publishing for the `autoevals` package in npm with these values:
24+
25+
- Package: `autoevals`
26+
- Provider: `GitHub Actions`
27+
- Repository owner: `braintrustdata`
28+
- Repository name: `autoevals`
29+
- Workflow file: `.github/workflows/publish-js.yaml`
30+
- Environment: `npm-publish`
31+
32+
Notes:
33+
34+
- The workflow uses GitHub OIDC, so no `NPM_TOKEN` is required.
35+
- The workflow publishes with provenance enabled via `npm publish --provenance`.
36+
37+
## GitHub environment setup
38+
39+
Create a GitHub Actions environment named:
40+
41+
- `npm-publish`
42+
43+
Recommended configuration:
44+
45+
- restrict deployments to `main`
46+
- add required reviewers if you want manual approval before publish
47+
48+
The workflow already references this environment:
49+
50+
```yaml
51+
environment: npm-publish
52+
```
53+
54+
## How to publish a stable release
55+
56+
1. Bump the JavaScript package version in `package.json`.
57+
2. Merge the change to `main`.
58+
3. In GitHub Actions, run the `publish-js` workflow.
59+
4. Choose:
60+
- `release_type=stable`
61+
- `branch=main`
62+
63+
Expected outcome:
64+
65+
- npm package `autoevals@<version>` is published
66+
- git tag `js-<version>` is created and pushed
67+
- GitHub Release `autoevals v<version>` is created
68+
69+
## How to publish a prerelease
70+
71+
1. Make sure `package.json` contains the base version you want to prerelease from.
72+
2. In GitHub Actions, run the `publish-js` workflow.
73+
3. Choose:
74+
- `release_type=prerelease`
75+
- `branch=main`
76+
77+
Expected outcome:
78+
79+
- npm package `autoevals@<version>-rc.<run_number>` is published
80+
- npm dist-tag `rc` is updated
81+
- no git tag is created
82+
- no GitHub Release is created
83+
84+
## Safeguards in the workflow
85+
86+
The workflow will fail early if:
87+
88+
- the stable tag `js-<version>` already exists on `origin`
89+
- the npm version being published already exists
90+
91+
## Local validation
92+
93+
Useful commands before triggering a release:
94+
95+
```bash
96+
pnpm install --frozen-lockfile
97+
pnpm run build
98+
npm publish --dry-run --access public
99+
```
100+
101+
## Historical releases and source mapping
102+
103+
Older npm releases may not be traceable back to an exact git commit from npm alone because they were published before trusted publishing and provenance attestations were enabled. In particular:
104+
105+
- npm metadata for older releases may not include `gitHead`
106+
- those releases do not have OIDC/provenance attestations tying the package to a workflow run and commit
107+
108+
For those historical versions, the best commit mapping may need to be inferred from repository history, publish timestamps, and version bumps. New releases published through `.github/workflows/publish-js.yaml` are expected to be easier to trace because they use trusted publishing with provenance.
109+
110+
## Future publishing work
111+
112+
Python publishing is not yet covered by this document. When a Python release workflow is added, keep Python tags and release process separate from the JavaScript `js-<version>` tag namespace.

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@
2626
"build": "tsup",
2727
"watch": "tsup --watch",
2828
"docs": "npx typedoc --options typedoc.json js/index.ts",
29-
"test": "vitest",
30-
"prepublishOnly": "../scripts/node_prepublish_autoevals.py",
31-
"postpublish": "../scripts/node_postpublish_autoevals.py"
29+
"test": "vitest"
3230
},
3331
"author": "",
3432
"license": "MIT",

0 commit comments

Comments
 (0)