-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add publishing npm package flow #139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
68a95ab
55f0562
7e63017
b6f6b97
adf17c5
aab1aba
f398816
d976a07
44e827e
6c63eae
eb45059
6c02f9b
6c7149a
541bcee
4d04660
bae4326
4b8e58a
b5b0090
3190d0e
7a78d05
7005093
ba596f8
f4cf043
1f13365
af8cfd3
f733e64
0ae07e2
b924e85
0e15786
dfaba66
fa8c4d2
f746b8f
f4f065e
7012d1f
4ba1d33
c1e993b
3cb4b50
efe5ef1
ef978a8
3acbf98
49a649e
9736f86
f9f996b
54699c3
0d79022
1208a2a
85b4c2f
be88336
fedde4e
5434c20
5855be9
5ca9842
c9d9cb6
1d5e00f
e4af21b
d0bbf8c
a010d89
f797e26
3e7b018
a1438ee
63743d5
b51ce52
588fddb
3f08089
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| name: Auto-create release on version bump | ||
|
|
||
| # Watches pushes to main for a change to the workspace version in Cargo.toml | ||
| # (the dedicated "bump version" PR from README.md's release flow). When it | ||
| # changes, this automatically cuts the GitHub Release that publish-npm.yml | ||
| # listens for, so merging the bump PR is the only manual step left before a | ||
| # release goes out (npm publish itself still needs manual approval, see | ||
| # publish-npm.yml's npm-publish environment). | ||
| on: | ||
| push: | ||
| branches: [main] | ||
|
|
||
| permissions: | ||
| contents: write | ||
| actions: write # to dispatch publish-npm.yml, see the note on the "Create release" step | ||
|
|
||
| jobs: | ||
| auto-release: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Detect version bump | ||
| id: detect | ||
| run: | | ||
| # All-zeros `before` shows up on branch creation/force-push; there's | ||
| # no meaningful "previous version" to diff against, so skip. | ||
| if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then | ||
| echo "changed=false" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| old_version=$(git show "${{ github.event.before }}:Cargo.toml" 2>/dev/null | grep -m1 '^version = ' | sed -E 's/version = "(.*)"/\1/') || true | ||
| new_version=$(grep -m1 '^version = ' Cargo.toml | sed -E 's/version = "(.*)"/\1/') | ||
|
|
||
| if [ -z "$new_version" ] || [ "$old_version" = "$new_version" ]; then | ||
| echo "changed=false" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Fail before creating anything if the bump PR forgot the npm package. | ||
| # Catching this here means no release/tag ever gets created against | ||
| # this commit — checking only after publish-npm.yml runs would leave | ||
| # a broken, immutable tag pinned to a commit that can never actually | ||
| # publish (a follow-up commit fixing just package.json wouldn't | ||
| # re-trigger this workflow, since it only watches Cargo.toml). | ||
| npm_version=$(node -p 'require("./programs/settlement/idl/client/js/package.json").version') | ||
| if [ "$new_version" != "$npm_version" ]; then | ||
| echo "::error::Cargo version $new_version does not match npm version $npm_version in programs/settlement/idl/client/js/package.json. Bump both together before merging." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "changed=true" >> "$GITHUB_OUTPUT" | ||
| echo "version=$new_version" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Check for existing release | ||
| id: check | ||
| if: steps.detect.outputs.changed == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| VERSION: ${{ steps.detect.outputs.version }} | ||
| run: | | ||
| tag="v$VERSION" | ||
| if gh release view "$tag" >/dev/null 2>&1; then | ||
| echo "exists=true" >> "$GITHUB_OUTPUT" | ||
| else | ||
| echo "exists=false" >> "$GITHUB_OUTPUT" | ||
| fi | ||
|
|
||
| - name: Create release | ||
| if: steps.detect.outputs.changed == 'true' && steps.check.outputs.exists == 'false' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| VERSION: ${{ steps.detect.outputs.version }} | ||
| run: | | ||
| tag="v$VERSION" | ||
| # --target pins the release to the exact commit this run is for, not | ||
| # whatever main's tip happens to be when this step runs — otherwise a | ||
| # commit landing on main mid-run could get tagged/published instead. | ||
| gh release create "$tag" \ | ||
| --target "$GITHUB_SHA" \ | ||
| --title "Alpha release, $tag" \ | ||
| --generate-notes | ||
|
kaze-cow marked this conversation as resolved.
|
||
|
|
||
| # A separate, unconditional-on-"just created" step: if a previous run | ||
| # created the release but failed before dispatching (e.g. a transient | ||
| # API error), re-running this job must still retry the dispatch instead | ||
| # of short-circuiting on "release already exists". | ||
| - name: Dispatch publish workflow | ||
| if: steps.detect.outputs.changed == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| VERSION: ${{ steps.detect.outputs.version }} | ||
| run: | | ||
| tag="v$VERSION" | ||
| # GitHub suppresses the `release` event when the resource that triggers | ||
| # it (this release) was itself created using GITHUB_TOKEN — otherwise | ||
| # this would recurse. That means publish-npm.yml's `release: published` | ||
| # trigger will NOT fire for a release created here. | ||
| # https://docs.github.com/en/actions/concepts/security/github_token | ||
| # So dispatch it explicitly instead of relying on that event. | ||
| # | ||
| # Dispatch against the tag itself, not `main`: the tag is immutable and | ||
| # points at $GITHUB_SHA above, whereas `main` can move between this line | ||
| # running and the dispatched run's checkout, which would silently build | ||
| # and (pending approval) publish a different, unreviewed commit. | ||
| gh workflow run publish-npm.yml --ref "$tag" -f tag="$tag" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| name: Publish npm package | ||
|
|
||
| on: | ||
| release: | ||
| types: [published] | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag: | ||
| description: >- | ||
| Release tag to publish (e.g. v0.4.0). Set automatically when | ||
| auto-release.yml dispatches this workflow (GITHUB_TOKEN-created | ||
| releases don't fire the `release` event, so it can't rely on that | ||
| trigger — see https://docs.github.com/en/actions/concepts/security/github_token). | ||
| Leave empty for a manual build-only dry run (skips the tag/version check). | ||
| required: false | ||
|
|
||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read # to look up releases in the "Cargo and npm versions must match" step | ||
| # Corepack otherwise asks for confirmation before fetching pnpm, which | ||
| # would hang the job. | ||
| env: | ||
| COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" | ||
| outputs: | ||
| package-name: ${{ steps.pkg.outputs.name }} | ||
| package-version: ${{ steps.pkg.outputs.version }} | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | ||
| with: | ||
| # A manual dispatch could pass --ref main while also supplying an | ||
| # unrelated existing release's tag as input; without this, the tag | ||
| # would pass the version check below while the build actually ran | ||
| # against whatever `main` happens to be, not the released commit. | ||
| # Pinning to the tag input (when present) makes that impossible. | ||
| ref: ${{ github.event.inputs.tag || github.ref }} | ||
| persist-credentials: false | ||
|
|
||
| - uses: ./.github/actions/setup-solana | ||
| - uses: ./.github/actions/setup-just | ||
| - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 | ||
|
|
||
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version: "24" # matches ci.yml's test-js-client job | ||
|
|
||
| - name: Build JS client | ||
| run: just build-js-client | ||
| - name: Test JS client | ||
| run: just test-js-client | ||
|
kaze-cow marked this conversation as resolved.
|
||
|
|
||
| - name: Read package metadata | ||
| id: pkg | ||
| working-directory: programs/settlement/idl/client/js | ||
| run: | | ||
| echo "name=$(node -p 'require("./package.json").name')" >> "$GITHUB_OUTPUT" | ||
| echo "version=$(node -p 'require("./package.json").version')" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Release tag, Cargo.toml, and package.json versions must all match | ||
| if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.tag != '') | ||
| working-directory: programs/settlement/idl/client/js | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| # Real `release` events carry the tag as the ref; a dispatch from | ||
| # auto-release.yml passes it as an explicit input instead. That | ||
| # input is free text from whoever ran the dispatch, so unlike the | ||
| # `release` case it isn't proof a real release exists — confirm one | ||
| # does, rather than only checking it's self-consistent otherwise. | ||
| if [ "${{ github.event_name }}" = "release" ]; then | ||
| raw_tag="$GITHUB_REF_NAME" | ||
| else | ||
| raw_tag="${{ github.event.inputs.tag }}" | ||
| if ! gh release view "$raw_tag" >/dev/null 2>&1; then | ||
| echo "::error::No release named $raw_tag exists." >&2 | ||
| exit 1 | ||
| fi | ||
| fi | ||
|
|
||
| # Existing tags (v0.2, v0.3) are major.minor only, while npm/Cargo | ||
| # need full semver (0.3.0), so pad any missing patch component. | ||
| tag="${raw_tag#v}" | ||
| IFS='.' read -r major minor patch <<< "$tag" | ||
| normalized_tag="${major}.${minor:-0}.${patch:-0}" | ||
|
|
||
| cargo_version=$(grep -m1 '^version = ' ../../../../../Cargo.toml | sed -E 's/version = "(.*)"/\1/') | ||
| pkg_version="${{ steps.pkg.outputs.version }}" | ||
|
|
||
| if [ "$normalized_tag" != "$cargo_version" ] || [ "$cargo_version" != "$pkg_version" ]; then | ||
| echo "::error::Version mismatch — release tag $raw_tag (normalized $normalized_tag), Cargo.toml $cargo_version, package.json $pkg_version must all match." >&2 | ||
| exit 1 | ||
| fi | ||
|
Comment on lines
+66
to
+93
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we simplify this? like maybe read the version from Cargo.toml instead to see that it matches.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done, now reads the version from Cargo.toml directly instead of parsing the git tag string. one thing I had to add back in: the check still also compares against the release tag itself, not just Cargo.toml vs package.json. First pass dropped that and I found it, since a release tagged v0.4.0 could otherwise pass with cargo.toml and package.json both still at 0.3.0, publishing the wrong version under the wrong tag. So now all three (tag, cargo.toml, package.json) have to agree. |
||
|
|
||
| - name: Write publish summary | ||
| working-directory: programs/settlement/idl/client/js | ||
| run: ./scripts/publish-summary.sh "${{ github.event.release.tag_name || github.event.inputs.tag || github.ref_name }}" >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| with: | ||
| name: npm-package | ||
| path: | | ||
| programs/settlement/idl/client/js/dist | ||
| programs/settlement/idl/client/js/package.json | ||
| programs/settlement/idl/client/js/README.md | ||
|
|
||
| publish: | ||
| needs: build | ||
| # A workflow_dispatch with an empty `tag` is documented as a build-only dry | ||
| # run (skips the version check above) — it must not be able to reach an | ||
| # actual `npm publish` just because someone approves the environment gate. | ||
| if: github.event_name == 'release' || github.event.inputs.tag != '' | ||
| runs-on: ubuntu-latest | ||
| # See README.md's "Publishing the Node.js client" section for what an | ||
| # approver here should check. | ||
| environment: npm-publish | ||
| permissions: | ||
| id-token: write # for npm provenance | ||
| steps: | ||
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | ||
| with: | ||
| name: npm-package | ||
| path: package | ||
|
|
||
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version: "24" | ||
| registry-url: https://registry.npmjs.org | ||
|
|
||
| # Trusted Publishing (OIDC) needs npm >= 11.5.1; pinned since the bundled | ||
| # version can be older even on Node 24. | ||
| - name: Upgrade npm for Trusted Publishing | ||
| run: npm install -g npm@~11.10.0 # pinned like cow-sdk's release workflow, see https://github.com/npm/cli/issues/9151 | ||
|
|
||
| - name: Publish ${{ needs.build.outputs.package-name }}@${{ needs.build.outputs.package-version }} | ||
| working-directory: package | ||
| # NODE_AUTH_TOKEN is only a bootstrap fallback for this package's very | ||
| # first publish, before a Trusted Publisher can be configured for it | ||
| # (see README.md's "Publishing the Node.js client"). `npm publish` | ||
| # always tries OIDC first regardless, so this is safe to leave set | ||
| # even after NPM_TOKEN is deleted (it just resolves to an empty string). | ||
| # --ignore-scripts: publish exactly the artifact reviewed in `build`, | ||
| # with no lifecycle script able to run and alter it at this point. | ||
| run: npm publish --provenance --access public --ignore-scripts | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| src/generated/ | ||
| pnpm-lock.yaml | ||
| dist/ |
Uh oh!
There was an error while loading. Please reload this page.