Skip to content
Merged
43 changes: 36 additions & 7 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
name: Publish to npm

# Recovery/backfill path: publishes the npm packages from the binaries already
# attached to an existing GitHub release, without cutting a new release. The
# normal path is the npm job in release.yml; use this one when that job failed
# partway, or to publish npm packages for a release that predates them.
# The single npm publishing path: publishes the npm packages for an existing
# GitHub release from the binaries attached to it, without cutting a new
# release. Normally dispatched by release.yml after it publishes the GitHub
# release; run it manually against the same tag to retry a partial or failed
# publish — already-published packages are skipped, so a retry only fills in
# what is missing, and retrying a version older than the current npm latest
# will not steal the `latest` dist-tag. Keeping all publishing in one
# workflow file lets npm's trusted publisher config (one per package, matched
# by workflow filename) cover every publish.

on:
workflow_dispatch:
inputs:
tag:
description: "Existing GitHub release tag whose binaries to publish (e.g. v0.2.1)"
required: true
dispatch_id:
description: "Opaque id embedded in the run name so a dispatching workflow can find this exact run (leave empty for manual runs)"
required: false
default: ""

# The dispatch_id in the run name is what release.yml greps for to identify
# the run it dispatched — do not reword the "dispatch <id>" marker without
# updating the matching filter there.
run-name: "Publish to npm (${{ inputs.tag }}${{ inputs.dispatch_id != '' && format(', dispatch {0}', inputs.dispatch_id) || '' }})"

env:
TAG: ${{ github.event.inputs.tag }}
Expand All @@ -20,9 +34,12 @@ jobs:
name: Publish to npm
runs-on: ubuntu-latest
steps:
# Deliberately the default branch, not the tag: the npm packaging
# scripts must be present regardless of what the tag's tree contains.
# The tag's tree is what gets published: packaging scripts and package
# metadata come from the tag itself, so a republish is reproducible.
# Consequence: this only works for tags that contain scripts/build-npm.mjs.
- uses: actions/checkout@v4
with:
ref: ${{ env.TAG }}

- uses: actions/setup-node@v4
with:
Expand Down Expand Up @@ -57,10 +74,22 @@ jobs:
# so main's exact-pinned optionalDependencies always resolve on
# install.
publish() {
local name
local name latest
name="$(node -p "require('./$1/package.json').name")"
if npm view "${name}@${VERSION}" version >/dev/null 2>&1; then
echo "${name}@${VERSION} already published, skipping"
return
fi
# When retrying a version older than what is already on npm,
# publish under a temporary dist-tag so `latest` keeps pointing
# at the newer version, then drop the temporary tag. Compared
# with real semver (not sort -V), which ranks prereleases below
# their release: 0.3.0-rc.1 < 0.3.0.
latest="$(npm view "$name" dist-tags.latest 2>/dev/null || true)"
if [ -n "$latest" ] && [ "$(npx --yes semver "$latest" "$VERSION" | tail -n1)" != "$VERSION" ]; then
npm publish "$1" --access public --tag backfill
npm dist-tag rm "$name" backfill \
|| echo "::warning::failed to remove temporary dist-tag 'backfill' from ${name}"
else
npm publish "$1" --access public
fi
Expand Down
72 changes: 34 additions & 38 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,46 +173,42 @@ jobs:

npm:
name: Publish to npm
needs: [prepare, build]
needs: [prepare, release]
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.tag }}

- uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"

- name: Download build artifacts
uses: actions/download-artifact@v4
with:
pattern: release-*
path: artifacts

- name: Extract binaries by target
run: |
# artifacts/release-<target>/aleo-devnode-<tag>-<target>.zip
# -> bins/<target>/aleo-devnode[.exe]
for dir in artifacts/release-*; do
target="${dir#artifacts/release-}"
mkdir -p "bins/$target"
unzip -o "$dir"/*.zip -d "bins/$target"
done

- name: Build npm packages
run: node scripts/build-npm.mjs --version "${{ needs.prepare.outputs.version }}" --artifacts bins --out dist-npm

- name: Publish
# All npm publishing lives in npm-publish.yml so npm's trusted publisher
# config can point at a single workflow file. It publishes from the
# release assets, so it must run after the GitHub release exists.
# `gh workflow run` is fire-and-forget and does not report the run it
# created, so this run's id is embedded in the dispatched run's name
# ("dispatch <id>") and polled for, then the run is watched to
# completion — a failed npm publish must fail this job (and the
# release run) too.
- name: Run npm publish workflow
run: |
set -e
# Platform packages first, then the main launcher last, so main's
# exact-pinned optionalDependencies always resolve on install.
for pkg in dist-npm/*/; do
[ "$pkg" = "dist-npm/main/" ] && continue
npm publish "$pkg" --access public
# Backdated a minute so clock skew between this runner and GitHub's
# createdAt cannot hide the run from the --created filter below.
DISPATCHED_AT="$(date -u -d '1 minute ago' +%Y-%m-%dT%H:%M:%SZ)"
gh workflow run npm-publish.yml -f tag="$TAG" -f dispatch_id="$GITHUB_RUN_ID" -R "$REPO"
RUN_ID=""
for _ in $(seq 1 24); do
sleep 5
RUN_ID="$(gh run list -R "$REPO" --workflow=npm-publish.yml \
--event=workflow_dispatch --created ">=$DISPATCHED_AT" \
--json databaseId,displayTitle \
--jq "[.[] | select(.displayTitle | contains(\"dispatch $GITHUB_RUN_ID\"))][0].databaseId // empty")"
[ -n "$RUN_ID" ] && break
done
npm publish dist-npm/main/ --access public
if [ -z "$RUN_ID" ]; then
echo "::error::Dispatched npm-publish.yml but could not find the run it created"
exit 1
fi
gh run watch "$RUN_ID" -R "$REPO" --exit-status
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
GH_TOKEN: ${{ github.token }}
# Passed via env, not ${{ }} interpolation into the script: tag
# names may contain shell metacharacters.
TAG: ${{ needs.prepare.outputs.tag }}
REPO: ${{ github.repository }}
Loading