Skip to content

v1.8.0

v1.8.0 #13

Workflow file for this run

# Publish to npm when a GitHub release is promoted from prerelease to full release.
# Downloads the npm package tarball from the release (no rebuild).
#
# AUTH: npm OIDC trusted publishing (no long-lived NPM_TOKEN). One-time setup on
# npmjs.com — needed SEPARATELY for `@spacedevin/deck`, `@spacedevin/deck-synths` AND
# `@spacedevin/deck-player`, since trusted publishers are per package. Settings > Trusted Publisher > GitHub Actions:
# Organization or user: spacedevin
# Repository: deck
# Workflow filename: npm-release.yml
# Environment: (leave blank)
# Requires npm >= 11.5.1 (upgraded below) and the id-token: write permission.
#
# All three packages ship from one release at one version, published in dependency order —
# language, then synths, then player — because each one's dependency is pinned to that exact version.
name: NPM release
on:
release:
types: [published, edited]
# Re-dispatch to publish a release the event never reached — promoting a prerelease does not
# always fire `edited`, and without this the only recovery is editing the release again and hoping.
# Publishing is idempotent: a version already on npm is skipped rather than failing.
workflow_dispatch:
inputs:
tag:
description: "Release tag to publish, e.g. v1.2.6"
required: true
type: string
jobs:
publish:
name: Publish to npm
if: ${{ github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false }}
runs-on: ubuntu-latest
permissions:
contents: write # required to update release body via API
id-token: write # required for npm OIDC trusted publishing
env:
# No NODE_AUTH_TOKEN — auth is via OIDC.
# Provenance disabled: packages publish from pre-built tarballs (built in CI, not this run).
NPM_CONFIG_PROVENANCE: "false"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: refs/tags/${{ github.event.release.tag_name || inputs.tag }}
fetch-depth: 0
- name: Resolve the release
id: rel
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ github.event.release.tag_name || inputs.tag }}"
# `github.event.release.id` is empty on a workflow_dispatch, so look it up by tag — which
# also works for the release event and keeps one code path.
ID=$(curl -sL -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/${{ github.repository }}/releases/tags/$TAG" | jq -r '.id')
if [ -z "$ID" ] || [ "$ID" = "null" ]; then
echo "No release found for tag $TAG"
exit 1
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "id=$ID" >> "$GITHUB_OUTPUT"
- name: Download npm package tarball from release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Look the asset up through the API, NOT `github.event.release.assets`. That payload is a
# snapshot frozen when the release event fired, and ci.yml creates the release BEFORE it
# finishes uploading the tarball — so this races, and a re-run replays the SAME frozen
# payload and fails identically forever. (tishlang/tish#593 — it cost v3.2.1 its npm publish.)
for attempt in 1 2 3 4 5 6 7 8 9 10; do
DECK_URL=$(curl -sL -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.rel.outputs.id }}/assets?per_page=100" \
| jq -r '.[] | select(.name == "spacedevin-deck-npm-package.tgz" and .state == "uploaded") | .url')
[ -n "$DECK_URL" ] && [ "$DECK_URL" != "null" ] && break
echo "tarball not uploaded yet (attempt $attempt/10) — waiting 30s"
sleep 30
done
if [ -z "$DECK_URL" ] || [ "$DECK_URL" = "null" ]; then
echo "Release has no uploaded asset spacedevin-deck-npm-package.tgz after 5 minutes"
exit 1
fi
curl -sL -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/octet-stream" "$DECK_URL" -o spacedevin-deck-npm-package.tgz
# The synths catalog ships in lockstep from the same release. Same retry loop, same reason,
# and the same tolerance: a release predating the package has no such asset, so its absence
# is not fatal.
for attempt in 1 2 3 4 5; do
SYNTHS_URL=$(curl -sL -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.rel.outputs.id }}/assets?per_page=100" \
| jq -r '.[] | select(.name == "spacedevin-deck-synths-npm-package.tgz" and .state == "uploaded") | .url')
[ -n "$SYNTHS_URL" ] && [ "$SYNTHS_URL" != "null" ] && break
echo "synths tarball not uploaded yet (attempt $attempt/5) — waiting 30s"
sleep 30
done
if [ -n "$SYNTHS_URL" ] && [ "$SYNTHS_URL" != "null" ]; then
curl -sL -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/octet-stream" "$SYNTHS_URL" -o spacedevin-deck-synths-npm-package.tgz
else
echo "No spacedevin-deck-synths-npm-package.tgz on this release — skipping the synths publish."
fi
# The player ships in lockstep from the same release. Same retry loop, same reason. A
# release predating the player has no such asset, so its absence is not fatal — it just
# means there is nothing to publish.
for attempt in 1 2 3 4 5; do
PLAYER_URL=$(curl -sL -H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/${{ steps.rel.outputs.id }}/assets?per_page=100" \
| jq -r '.[] | select(.name == "spacedevin-deck-player-npm-package.tgz" and .state == "uploaded") | .url')
[ -n "$PLAYER_URL" ] && [ "$PLAYER_URL" != "null" ] && break
echo "player tarball not uploaded yet (attempt $attempt/5) — waiting 30s"
sleep 30
done
if [ -n "$PLAYER_URL" ] && [ "$PLAYER_URL" != "null" ]; then
curl -sL -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/octet-stream" "$PLAYER_URL" -o spacedevin-deck-player-npm-package.tgz
else
echo "No spacedevin-deck-player-npm-package.tgz on this release — skipping the player publish."
fi
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Upgrade npm for OIDC trusted publishing (needs >= 11.5.1)
run: |
# Pinned to npm 11.x (not 12.x) — same rationale as tishlang/lattish npm-release.yml.
npm install -g "npm@^11"
npm --version
- name: Publish @spacedevin/deck to npm
run: npm publish spacedevin-deck-npm-package.tgz --access public
# After the language, for the same reason: the synths package's dependency was pinned to this
# exact version at pack time, so it is uninstallable until the language is on the registry.
- name: Publish @spacedevin/deck-synths to npm
run: |
if [ -f spacedevin-deck-synths-npm-package.tgz ]; then
npm publish spacedevin-deck-synths-npm-package.tgz --access public
else
echo "No synths tarball — nothing to publish."
fi
# Second, and only after the language package is live: the player's dependency was pinned to
# this exact version at pack time, so publishing it first would put an uninstallable package on
# the registry for as long as the other step takes.
- name: Publish @spacedevin/deck-player to npm
run: |
if [ -f spacedevin-deck-player-npm-package.tgz ]; then
npm publish spacedevin-deck-player-npm-package.tgz --access public
else
echo "No player tarball — nothing to publish."
fi
- name: Update release description with npm URL
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ steps.rel.outputs.tag }}"
VERSION="${TAG#v}"
NPM_URL="https://www.npmjs.com/package/@spacedevin/deck/v/${VERSION}"
RELEASE_ID="${{ steps.rel.outputs.id }}"
REPO="${{ github.repository }}"
CURRENT_BODY=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/${REPO}/releases/${RELEASE_ID}" | jq -r '.body // ""')
NEW_BODY="${CURRENT_BODY}
---
Published to npm: ${NPM_URL}"
curl -s -X PATCH \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${REPO}/releases/${RELEASE_ID}" \
-d "$(printf '%s' "$NEW_BODY" | jq -Rs '{body: .}')"