Skip to content

Publish NPM Packages #77

Publish NPM Packages

Publish NPM Packages #77

Workflow file for this run

name: Publish NPM Packages
on:
workflow_dispatch:
inputs:
package:
description: "Package to publish"
required: true
type: choice
options:
- all
- a2a
- types
- sdk-typescript
- cli
- mcp
- react
- openclaw
default: "all"
version:
description: "Version bump type"
required: true
type: choice
options:
- patch
- minor
- major
- prepatch
- preminor
- premajor
- prerelease
custom_version:
description: "Custom version (optional, overrides version type)"
required: false
type: string
dry_run:
description: "Dry run (do not actually publish)"
required: false
type: boolean
default: false
tag:
description: "NPM dist-tag"
required: false
type: choice
options:
- latest
- next
- beta
- alpha
default: "latest"
# Prevent concurrent publishes
concurrency:
group: publish-package
cancel-in-progress: false
permissions:
contents: write
id-token: write
env:
NPM_CONFIG_FUND: false
jobs:
# Build all packages, version them, run tests
build:
name: Build & Version
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.bump.outputs.new_version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22.22.3"
cache: "npm"
cache-dependency-path: package-lock.json
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: npm ci
- name: Version all packages
id: bump
env:
# Bind workflow inputs through env so free-form custom_version is not
# interpolated into the shell script body.
CUSTOM_VERSION: ${{ github.event.inputs.custom_version }}
VERSION_TYPE: ${{ github.event.inputs.version }}
run: |
# Read current version from types (canonical source)
CURRENT_VERSION=$(node -p "require('./packages/types/package.json').version")
echo "Current version: $CURRENT_VERSION"
if [ -n "$CUSTOM_VERSION" ]; then
NEW_VERSION="$CUSTOM_VERSION"
echo "Setting version to custom value: $NEW_VERSION"
else
# Use npm version on types to compute the new version
cd packages/types
npm version "$VERSION_TYPE" --no-git-tag-version --preid=beta
NEW_VERSION=$(node -p "require('./package.json').version")
cd ../..
echo "Bumped version: $VERSION_TYPE -> $NEW_VERSION"
fi
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "New version: $NEW_VERSION"
# Sync all package versions and internal dependencies
NEW_VERSION="$NEW_VERSION" node <<'NODE'
const fs = require('fs');
const path = require('path');
const version = process.env.NEW_VERSION;
if (!version) {
throw new Error('NEW_VERSION is required');
}
const packagesDir = 'packages';
for (const dir of fs.readdirSync(packagesDir)) {
const pkgPath = path.join(packagesDir, dir, 'package.json');
if (fs.existsSync(pkgPath)) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
pkg.version = version;
// Update @relaycast/* dependencies to exact version
for (const depType of ['dependencies', 'devDependencies', 'peerDependencies']) {
for (const dep of Object.keys(pkg[depType] || {})) {
if (dep.startsWith('@relaycast/')) {
pkg[depType][dep] = version;
}
}
}
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
console.log(pkg.name + '@' + version);
}
}
// Sync package version constants
const sdkVersionPath = path.join(packagesDir, 'sdk-typescript', 'src', 'version.ts');
fs.writeFileSync(sdkVersionPath, 'export const SDK_VERSION = ' + JSON.stringify(version) + ' as const;\n');
console.log('SDK_VERSION -> ' + version);
const cliVersionPath = path.join(packagesDir, 'cli', 'src', 'version.ts');
if (fs.existsSync(cliVersionPath)) {
fs.writeFileSync(cliVersionPath, 'export const CLI_VERSION = ' + JSON.stringify(version) + ' as const;\n');
console.log('CLI_VERSION -> ' + version);
}
NODE
- name: Clean reinstall after version bump
run: |
rm -rf node_modules packages/*/node_modules package-lock.json
npm install
- name: Build all packages
run: npx turbo build
- name: Run tests
run: npx turbo test
env:
DO_NOT_TRACK: "1"
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: |
package.json
package-lock.json
packages/*/package.json
packages/*/dist/
retention-days: 1
# Publish all packages in parallel
publish-packages:
name: Publish ${{ matrix.package }}
needs: build
runs-on: ubuntu-latest
if: github.event.inputs.package == 'all'
strategy:
fail-fast: false
max-parallel: 5
matrix:
package:
- a2a
- types
- engine
- sdk-typescript
- cli
- mcp
- react
- openclaw
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22.22.3"
registry-url: "https://registry.npmjs.org"
- name: Setup npm for trusted publishing
run: npm install -g npm@11
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
- name: Dry run check
if: github.event.inputs.dry_run == 'true'
working-directory: packages/${{ matrix.package }}
run: |
PACKAGE_NAME=$(node -p "require('./package.json').name")
echo "Dry run - would publish ${PACKAGE_NAME}@${{ needs.build.outputs.new_version }}"
npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Publish to NPM
if: github.event.inputs.dry_run != 'true'
working-directory: packages/${{ matrix.package }}
run: npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
# Publish a single package (when not "all")
publish-single:
name: Publish Single Package
needs: build
runs-on: ubuntu-latest
if: github.event.inputs.package != 'all'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22.22.3"
registry-url: "https://registry.npmjs.org"
- name: Setup npm for trusted publishing
run: npm install -g npm@11
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
- name: Dry run check
if: github.event.inputs.dry_run == 'true'
working-directory: packages/${{ github.event.inputs.package }}
run: |
PACKAGE_NAME=$(node -p "require('./package.json').name")
echo "Dry run - would publish ${PACKAGE_NAME}@${{ needs.build.outputs.new_version }}"
npm publish --dry-run --access public --tag ${{ github.event.inputs.tag }} --ignore-scripts
- name: Publish to NPM
if: github.event.inputs.dry_run != 'true'
working-directory: packages/${{ github.event.inputs.package }}
run: npm publish --access public --provenance --tag ${{ github.event.inputs.tag }} --ignore-scripts
# Create git tag and release
create-release:
name: Create Release
needs: [build, publish-packages, publish-single]
runs-on: ubuntu-latest
if: |
always() &&
github.event.inputs.dry_run != 'true' &&
needs.build.result == 'success' &&
(needs.publish-packages.result == 'success' || needs.publish-single.result == 'success')
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: .
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22.22.3"
# Move the curated [Unreleased] entries under the version being released
# and restore a bare [Unreleased], so a published version always has a
# heading. Prereleases are left pending.
- name: Cut changelogs
run: node scripts/cut-changelog.mjs --version "${{ needs.build.outputs.new_version }}"
- name: Commit and tag
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
NEW_VERSION="${{ needs.build.outputs.new_version }}"
git add package.json package-lock.json packages/*/package.json
git add CHANGELOG.md packages/*/CHANGELOG.md
if ! git diff --staged --quiet; then
git commit -m "chore(release): v${NEW_VERSION}"
# main can advance during the (multi-minute) build — e.g. a
# concurrent SDK publish — so rebase onto the latest main and retry
# to avoid non-fast-forward rejections. Version metadata rebases
# cleanly; a changelog can conflict when a PR curated [Unreleased]
# during the build, so take main's copy and re-cut it rather than
# failing a release whose packages are already published.
pushed=false
for attempt in 1 2 3 4 5; do
git fetch origin main
if ! git rebase origin/main; then
conflicted=$(git diff --name-only --diff-filter=U)
if [ -z "$conflicted" ] || echo "$conflicted" | grep -qvE '(^|/)CHANGELOG\.md$'; then
git rebase --abort
echo "::error::rebase onto origin/main failed (unexpected conflict)"
exit 1
fi
echo "changelog conflict on ${conflicted}; re-cutting against main"
# During a rebase --ours is the upstream side (origin/main).
echo "$conflicted" | xargs git checkout --ours --
node scripts/cut-changelog.mjs --version "$NEW_VERSION"
echo "$conflicted" | xargs git add --
if ! GIT_EDITOR=true git rebase --continue; then
git rebase --abort
echo "::error::failed to resolve changelog conflict during rebase"
exit 1
fi
fi
if git push origin HEAD:main; then
pushed=true
break
fi
echo "push rejected (main moved); retry ${attempt}"
sleep $(( attempt * 3 ))
done
if [ "$pushed" != "true" ]; then
echo "::error::failed to push release commit to main after retries"
exit 1
fi
fi
git tag -a "v${NEW_VERSION}" -m "Release v${NEW_VERSION}"
git push origin "v${NEW_VERSION}"
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ needs.build.outputs.new_version }}
name: v${{ needs.build.outputs.new_version }}
body: |
## Relaycast v${{ needs.build.outputs.new_version }}
### Install
```bash
npm install @relaycast/a2a@${{ needs.build.outputs.new_version }}
npm install @relaycast/sdk@${{ needs.build.outputs.new_version }}
npm install @relaycast/engine@${{ needs.build.outputs.new_version }}
npm install relaycast@${{ needs.build.outputs.new_version }}
npm install @relaycast/react@${{ needs.build.outputs.new_version }}
npm install @relaycast/mcp@${{ needs.build.outputs.new_version }}
npm install @relaycast/openclaw@${{ needs.build.outputs.new_version }}
```
### Packages
| Package | Version |
|---------|---------|
| @relaycast/a2a | ${{ needs.build.outputs.new_version }} |
| @relaycast/types | ${{ needs.build.outputs.new_version }} |
| @relaycast/engine | ${{ needs.build.outputs.new_version }} |
| @relaycast/sdk | ${{ needs.build.outputs.new_version }} |
| relaycast | ${{ needs.build.outputs.new_version }} |
| @relaycast/mcp | ${{ needs.build.outputs.new_version }} |
| @relaycast/react | ${{ needs.build.outputs.new_version }} |
| @relaycast/openclaw | ${{ needs.build.outputs.new_version }} |
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Summary
summary:
name: Summary
needs: [build, publish-packages, publish-single, create-release]
runs-on: ubuntu-latest
if: always()
steps:
- name: Summary
run: |
echo "## Publish Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version**: \`${{ needs.build.outputs.new_version }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Package**: \`${{ github.event.inputs.package }}\`" >> $GITHUB_STEP_SUMMARY
echo "**NPM Tag**: \`${{ github.event.inputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
echo "**Dry Run**: \`${{ github.event.inputs.dry_run }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Results" >> $GITHUB_STEP_SUMMARY
echo "| Stage | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Build & Version | ${{ needs.build.result == 'success' && '✅' || '❌' }} ${{ needs.build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Publish NPM Packages | ${{ needs.publish-packages.result == 'success' && '✅' || (needs.publish-packages.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-packages.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Publish Single | ${{ needs.publish-single.result == 'success' && '✅' || (needs.publish-single.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.publish-single.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Create Release | ${{ needs.create-release.result == 'success' && '✅' || (needs.create-release.result == 'skipped' && '⏭️' || '❌') }} ${{ needs.create-release.result }} |" >> $GITHUB_STEP_SUMMARY