Skip to content

Merge pull request #6201 from elizaOS/feat/unified-api-elizaos #347

Merge pull request #6201 from elizaOS/feat/unified-api-elizaos

Merge pull request #6201 from elizaOS/feat/unified-api-elizaos #347

Workflow file for this run

name: NPM Release
# Unified workflow for all NPM releases:
# - Alpha: On merge to develop branch
# - Beta: On merge to main branch
# - Production: On GitHub release creation
#
# Version Management:
# - Uses lerna version and publish commands with consistent patterns
# - Commits version changes to git FIRST, then publishes to NPM
# - Prevents infinite loops with [skip ci] in commit messages
# - Proper error handling without masking critical failures
on:
push:
branches:
- develop # Triggers alpha releases
- main # Triggers beta releases
paths-ignore:
- '**/*.md'
- 'docs/**'
- '.github/**/*.md'
- 'LICENSE'
- '.gitignore'
- '.dockerignore'
- '**/*.example'
- '.vscode/**'
- '.devcontainer/**'
release:
types: [created] # Triggers production releases
workflow_dispatch:
inputs:
release_type:
description: 'Manual release type (only for prerelease testing)'
required: true
type: choice
options:
- alpha
- beta
# Note: 'latest' removed - production releases MUST be done via GitHub releases
jobs:
release:
runs-on: ubuntu-latest
# Skip if commit message contains [skip ci]
if: ${{ !contains(github.event.head_commit.message || '', '[skip ci]') }}
permissions:
contents: write
packages: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# If triggered by a release, we're in detached HEAD state
# Create a temporary branch that matches Lerna's allowBranch pattern
if [[ "${{ github.event_name }}" == "release" ]]; then
echo "Creating temporary release branch from tag..."
git checkout -b release/production-${{ github.sha }}
fi
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '23.3.0'
registry-url: 'https://registry.npmjs.org'
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: '1.2.21'
- name: Install dependencies
run: bun install
# Determine release type and version
- name: Determine release type
id: release_type
run: |
if [[ "${{ github.event_name }}" == "release" ]]; then
echo "type=latest" >> $GITHUB_OUTPUT
echo "dist_tag=latest" >> $GITHUB_OUTPUT
# Extract version from tag (remove 'v' prefix if present)
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "is_release_event=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "type=${{ github.event.inputs.release_type }}" >> $GITHUB_OUTPUT
echo "dist_tag=${{ github.event.inputs.release_type }}" >> $GITHUB_OUTPUT
echo "is_release_event=false" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then
echo "type=alpha" >> $GITHUB_OUTPUT
echo "dist_tag=alpha" >> $GITHUB_OUTPUT
echo "is_release_event=false" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "type=beta" >> $GITHUB_OUTPUT
echo "dist_tag=beta" >> $GITHUB_OUTPUT
echo "is_release_event=false" >> $GITHUB_OUTPUT
fi
# Version Management
- name: Version packages
id: version
run: |
RELEASE_TYPE="${{ steps.release_type.outputs.type }}"
CURRENT_VERSION=$(node -p "require('./lerna.json').version")
echo "Current version: ${CURRENT_VERSION}"
# Helper functions for version manipulation
get_base_version() {
echo "$1" | sed 's/-.*$//'
}
get_prerelease_type() {
if [[ "$1" =~ -([a-z]+)\. ]]; then
echo "${BASH_REMATCH[1]}"
else
echo ""
fi
}
bump_version() {
local version="$1"
local type="$2" # major, minor, patch
IFS='.' read -r major minor patch <<< "$version"
patch=${patch%%-*} # Remove any prerelease suffix
case "$type" in
major)
echo "$((major + 1)).0.0"
;;
minor)
echo "${major}.$((minor + 1)).0"
;;
patch)
echo "${major}.${minor}.$((patch + 1))"
;;
*)
echo "$version"
;;
esac
}
# Determine version strategy based on release type and current version
if [[ "${{ github.event_name }}" == "release" ]]; then
# Production release from GitHub release tag
VERSION="${{ steps.release_type.outputs.version }}"
echo "📦 Production release: Setting exact version to ${VERSION}"
bunx lerna version ${VERSION} \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push \
--allow-branch release/*
elif [[ "${RELEASE_TYPE}" == "alpha" ]]; then
echo "🚀 Alpha release workflow..."
PRERELEASE_TYPE=$(get_prerelease_type "$CURRENT_VERSION")
BASE_VERSION=$(get_base_version "$CURRENT_VERSION")
if [[ "$PRERELEASE_TYPE" == "alpha" ]]; then
# Already on alpha, just increment the alpha counter
echo "Incrementing alpha version from ${CURRENT_VERSION}..."
bunx lerna version prerelease \
--preid alpha \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push
else
# Not on alpha (could be stable, beta, etc)
# Check if there are existing alpha tags for this base version
echo "Checking for existing alpha tags for base version ${BASE_VERSION}..."
# Fetch all tags to ensure we have the complete history
git fetch --tags
# Find the highest existing alpha version for this base
EXISTING_ALPHAS=$(git tag -l "v${BASE_VERSION}-alpha.*" 2>/dev/null | sort -V | tail -n 1)
if [[ -n "$EXISTING_ALPHAS" ]]; then
# Found existing alpha tags, continue from there
LAST_ALPHA_VERSION=${EXISTING_ALPHAS#v}
echo "Found existing alpha version: ${LAST_ALPHA_VERSION}"
# Set to the last version, then increment
bunx lerna version ${LAST_ALPHA_VERSION} \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push
# Now increment to get the next alpha
echo "Incrementing from ${LAST_ALPHA_VERSION}..."
bunx lerna version prerelease \
--preid alpha \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push
else
# No existing alpha tags, safe to start at .0
echo "No existing alpha tags found, starting at ${BASE_VERSION}-alpha.0"
bunx lerna version "${BASE_VERSION}-alpha.0" \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push
fi
fi
elif [[ "${RELEASE_TYPE}" == "beta" ]]; then
echo "🔵 Beta release workflow..."
PRERELEASE_TYPE=$(get_prerelease_type "$CURRENT_VERSION")
BASE_VERSION=$(get_base_version "$CURRENT_VERSION")
if [[ "$PRERELEASE_TYPE" == "beta" ]]; then
# Already on beta, just increment the beta counter
echo "Incrementing beta version from ${CURRENT_VERSION}..."
bunx lerna version prerelease \
--preid beta \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push
else
# Not on beta (could be stable, alpha, etc)
# Check if there are existing beta tags for this base version
echo "Checking for existing beta tags for base version ${BASE_VERSION}..."
# Fetch all tags to ensure we have the complete history
git fetch --tags
# Find the highest existing beta version for this base
EXISTING_BETAS=$(git tag -l "v${BASE_VERSION}-beta.*" 2>/dev/null | sort -V | tail -n 1)
if [[ -n "$EXISTING_BETAS" ]]; then
# Found existing beta tags, continue from there
LAST_BETA_VERSION=${EXISTING_BETAS#v}
echo "Found existing beta version: ${LAST_BETA_VERSION}"
# Set to the last version, then increment
bunx lerna version ${LAST_BETA_VERSION} \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push
# Now increment to get the next beta
echo "Incrementing from ${LAST_BETA_VERSION}..."
bunx lerna version prerelease \
--preid beta \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push
else
# No existing beta tags, safe to start at .0
echo "No existing beta tags found, starting at ${BASE_VERSION}-beta.0"
bunx lerna version "${BASE_VERSION}-beta.0" \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push
fi
fi
elif [[ "${RELEASE_TYPE}" == "latest" ]]; then
# Manual workflow dispatch for 'latest' should NOT be used for version bumps!
# Version bumps should ONLY come from GitHub releases with tags
echo "❌ ERROR: Manual 'latest' releases are not allowed!"
echo "Production version changes must be done through GitHub releases."
echo "Please create a GitHub release with the desired version tag instead."
exit 1
fi
# Get the new version
VERSION=$(node -p "require('./lerna.json').version")
echo "version=${VERSION}" >> $GITHUB_OUTPUT
# Update lockfile after version changes
- name: Update lockfile
run: |
bun install --no-frozen-lockfile
# Commit and push version changes BEFORE building and publishing
# This ensures git is the source of truth
- name: Commit version changes
id: commit
run: |
VERSION="${{ steps.version.outputs.version }}"
RELEASE_TYPE="${{ steps.release_type.outputs.type }}"
# Stage all changes
git add -A
# Check if there are changes to commit
if git diff --staged --quiet; then
echo "No changes to commit - this might indicate a problem"
echo "has_changes=false" >> $GITHUB_OUTPUT
exit 0
fi
# Commit with [skip ci] to prevent infinite loop
git commit -m "chore: release v${VERSION} (${RELEASE_TYPE}) [skip ci]"
echo "has_changes=true" >> $GITHUB_OUTPUT
echo "commit_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
# Create and push git tag (only if not from a GitHub release)
- name: Create git tag
if: steps.release_type.outputs.is_release_event != 'true' && steps.commit.outputs.has_changes == 'true'
id: tag
run: |
VERSION="${{ steps.version.outputs.version }}"
TAG_NAME="v${VERSION}"
# Check if tag already exists
if git rev-parse "${TAG_NAME}" >/dev/null 2>&1; then
echo "❌ Error: Tag ${TAG_NAME} already exists"
echo "This indicates a version conflict that needs manual resolution"
exit 1
fi
# Create the tag
git tag "${TAG_NAME}"
echo "tag_created=true" >> $GITHUB_OUTPUT
echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT
# Push changes to git (fails the workflow if it can't push)
- name: Push to git
if: steps.commit.outputs.has_changes == 'true'
run: |
# Determine target branch for push
if [[ "${{ github.event_name }}" == "release" ]]; then
# For GitHub releases, push to main branch
TARGET_BRANCH="main"
echo "Pushing changes to main branch..."
# Push the changes to main branch
if ! git push origin HEAD:${TARGET_BRANCH} --follow-tags; then
echo "❌ Error: Failed to push to git repository"
echo "This could be due to:"
echo " - Protected branch restrictions"
echo " - Network issues"
echo " - Permission problems"
echo ""
echo "The version has been updated locally but not published."
echo "Manual intervention required to resolve the git push issue."
exit 1
fi
else
# For other triggers, push to current branch
if ! git push origin HEAD --follow-tags; then
echo "❌ Error: Failed to push to git repository"
echo "This could be due to:"
echo " - Protected branch restrictions"
echo " - Network issues"
echo " - Permission problems"
echo ""
echo "The version has been updated locally but not published."
echo "Manual intervention required to resolve the git push issue."
exit 1
fi
fi
echo "✅ Successfully pushed version changes and tags to git"
# Build packages with correct version numbers
# Only happens AFTER git operations succeed
- name: Build packages
run: |
echo "Building packages with version v${{ steps.version.outputs.version }}..."
bun run build
# Publish to NPM (only after git operations succeed)
- name: Publish to NPM
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
DIST_TAG="${{ steps.release_type.outputs.dist_tag }}"
# Configure npm for authentication
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ~/.npmrc
# Publish with appropriate dist-tag
if ! bunx lerna publish from-package \
--dist-tag ${DIST_TAG} \
--force-publish \
--yes \
--no-verify-access; then
echo "❌ Error: Failed to publish to NPM"
echo ""
echo "Git has been updated with version v${{ steps.version.outputs.version }}"
echo "but the packages were not published to NPM."
echo ""
echo "To recover:"
echo " 1. Fix the NPM publishing issue"
echo " 2. Run 'npm run release:${DIST_TAG}' locally with proper credentials"
echo " 3. Or re-run this workflow"
exit 1
fi
echo "✅ Successfully published to NPM with dist-tag: ${DIST_TAG}"
# Create GitHub Release for alpha/beta (not for production, as it already exists)
- name: Create GitHub release
if: github.event_name != 'release' && steps.release_type.outputs.type != 'latest' && steps.tag.outputs.tag_created == 'true'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.tag.outputs.tag_name }}
name: ${{ steps.tag.outputs.tag_name }}
body: |
${{ steps.release_type.outputs.type == 'alpha' && '🚀 Alpha Release' || '🔵 Beta Release' }}
**Version:** `${{ steps.tag.outputs.tag_name }}`
**Channel:** `${{ steps.release_type.outputs.dist_tag }}`
### Quick Start
Install the CLI globally to get started:
```bash
bun i -g @elizaos/cli@${{ steps.release_type.outputs.dist_tag }}
```
Or add packages to your project:
```bash
bun add @elizaos/core@${{ steps.release_type.outputs.dist_tag }}
bun add @elizaos/plugin-bootstrap@${{ steps.release_type.outputs.dist_tag }}
```
---
> **Note:** This is a ${{ steps.release_type.outputs.type }} release. Production releases use the `latest` tag and are triggered by GitHub releases on tags matching `v*.*.*`.
draft: false
prerelease: true
- name: Summary
if: always()
run: |
echo "# 📦 Release Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version**: v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Type**: ${{ steps.release_type.outputs.type }}" >> $GITHUB_STEP_SUMMARY
echo "- **Dist Tag**: ${{ steps.release_type.outputs.dist_tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Trigger**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Branch**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
if [[ "${{ steps.commit.outputs.has_changes }}" == "true" ]]; then
echo "- **Commit SHA**: ${{ steps.commit.outputs.commit_sha }}" >> $GITHUB_STEP_SUMMARY
fi
if [[ "${{ steps.tag.outputs.tag_created }}" == "true" ]]; then
echo "- **Tag**: ${{ steps.tag.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Quick Start" >> $GITHUB_STEP_SUMMARY
echo "Install the CLI globally:" >> $GITHUB_STEP_SUMMARY
echo '```bash' >> $GITHUB_STEP_SUMMARY
echo "bun i -g @elizaos/cli@${{ steps.release_type.outputs.dist_tag }}" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Or add to your project:" >> $GITHUB_STEP_SUMMARY
echo '```bash' >> $GITHUB_STEP_SUMMARY
echo "bun add @elizaos/core@${{ steps.release_type.outputs.dist_tag }}" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
# Sync version back to develop after production release
- name: Sync version to develop branch
if: github.event_name == 'release' && success()
continue-on-error: true # Don't fail the release if sync fails
run: |
echo "📤 Syncing production release back to develop branch..."
# Get the released version
RELEASED_VERSION="${{ steps.version.outputs.version }}"
BASE_VERSION=$(echo "$RELEASED_VERSION" | sed 's/-.*$//')
echo "Released version: ${RELEASED_VERSION}"
echo "Base version: ${BASE_VERSION}"
# Fetch latest develop
git fetch origin develop:refs/remotes/origin/develop || {
echo "⚠️ Could not fetch develop branch, skipping sync"
exit 0
}
# Get current develop version
git checkout origin/develop -- lerna.json 2>/dev/null || true
DEVELOP_VERSION=$(node -p "require('./lerna.json').version" 2>/dev/null || echo "unknown")
# Restore our lerna.json
git checkout HEAD -- lerna.json
echo "Current develop version: ${DEVELOP_VERSION}"
# Extract base versions for comparison
DEVELOP_BASE=$(echo "$DEVELOP_VERSION" | sed 's/-.*$//')
# Compare versions and auto-advance if needed
if [[ "$DEVELOP_BASE" < "$BASE_VERSION" ]]; then
# Develop is behind - update it to match production with alpha suffix
NEXT_ALPHA="${BASE_VERSION}-alpha.0"
echo "Develop is behind: ${DEVELOP_VERSION} → ${NEXT_ALPHA}"
# Create a branch for the sync (using release/ prefix for lerna)
git checkout -b release/sync-develop-${BASE_VERSION} origin/develop
# Update version to match production base with alpha suffix
bunx lerna version ${NEXT_ALPHA} \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push \
--allow-branch release/*
# Update lockfile
bun install --no-frozen-lockfile
# Commit
git add -A
git commit -m "chore: sync to v${NEXT_ALPHA} after v${BASE_VERSION} release [skip ci]" \
-m "Automated version sync from production release"
# Push to develop
if git push origin HEAD:develop; then
echo "✅ Successfully synced develop to ${NEXT_ALPHA}"
else
echo "⚠️ Could not push to develop (may be protected or already updated)"
fi
elif [[ "$DEVELOP_BASE" == "$BASE_VERSION" ]]; then
# Develop is on same base as release - auto-advance to next patch
echo "Develop matches release base, auto-advancing to next patch version..."
# Calculate next patch version
IFS='.' read -r major minor patch <<< "$BASE_VERSION"
NEXT_PATCH="${major}.${minor}.$((patch + 1))"
NEXT_ALPHA="${NEXT_PATCH}-alpha.0"
echo "Auto-advancing: ${DEVELOP_VERSION} → ${NEXT_ALPHA}"
# Create a branch for the sync (using release/ prefix for lerna)
git checkout -b release/sync-develop-${BASE_VERSION} origin/develop
# Update version to next patch with alpha suffix
bunx lerna version ${NEXT_ALPHA} \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push \
--allow-branch release/*
# Update lockfile
bun install --no-frozen-lockfile
# Commit
git add -A
git commit -m "chore: bump to v${NEXT_ALPHA} after v${BASE_VERSION} release [skip ci]" \
-m "Automated patch version bump from production release"
# Push to develop
if git push origin HEAD:develop; then
echo "✅ Successfully auto-advanced develop to ${NEXT_ALPHA}"
else
echo "⚠️ Could not push to develop (may be protected or already updated)"
fi
else
echo "✅ Develop (${DEVELOP_VERSION}) is already ahead of release (${RELEASED_VERSION})"
# Develop is ahead - this is fine, means a new version is being worked on
# Don't touch it - developer has manually set the next version
fi
# Also sync main branch to match production release
echo "🔄 Syncing main branch to production release..."
# Fetch latest main
git fetch origin main:refs/remotes/origin/main || {
echo "⚠️ Could not fetch main branch, skipping main sync"
exit 0
}
# Get current main version
git checkout origin/main -- lerna.json 2>/dev/null || true
MAIN_VERSION=$(node -p "require('./lerna.json').version" 2>/dev/null || echo "unknown")
# Restore our lerna.json
git checkout HEAD -- lerna.json
echo "Current main version: ${MAIN_VERSION}"
MAIN_BASE=$(echo "$MAIN_VERSION" | sed 's/-.*$//')
# Main should follow develop's base version
# First, get the new develop version that was just set
git fetch origin develop:refs/remotes/origin/develop || {
echo "⚠️ Could not fetch updated develop branch"
NEW_DEVELOP_BASE="$BASE_VERSION"
}
if [[ -z "${NEW_DEVELOP_BASE}" ]]; then
git checkout origin/develop -- lerna.json 2>/dev/null || true
NEW_DEVELOP_VERSION=$(node -p "require('./lerna.json').version" 2>/dev/null || echo "${BASE_VERSION}-alpha.0")
NEW_DEVELOP_BASE=$(echo "$NEW_DEVELOP_VERSION" | sed 's/-.*$//')
git checkout HEAD -- lerna.json
fi
echo "New develop base: ${NEW_DEVELOP_BASE}"
echo "Current main base: ${MAIN_BASE}"
# Main should match develop's base version
if [[ "$MAIN_BASE" != "$NEW_DEVELOP_BASE" ]]; then
NEXT_BETA="${NEW_DEVELOP_BASE}-beta.0"
echo "Updating main to match develop base: ${MAIN_VERSION} → ${NEXT_BETA}"
# Create a branch for the sync (using release/ prefix for lerna)
git checkout -b release/sync-main-${BASE_VERSION} origin/main
# Update version
bunx lerna version ${NEXT_BETA} \
--force-publish \
--yes \
--no-private \
--no-git-tag-version \
--no-push \
--allow-branch release/*
# Update lockfile
bun install --no-frozen-lockfile
# Commit
git add -A
git commit -m "chore: sync to v${NEXT_BETA} after v${BASE_VERSION} release [skip ci]" \
-m "Automated version sync from production release (following develop)"
# Push to main
if git push origin HEAD:main; then
echo "✅ Successfully synced main to ${NEXT_BETA}"
else
echo "⚠️ Could not push to main (may be protected or already updated)"
fi
else
echo "✅ Main base version (${MAIN_BASE}) already matches develop base (${NEW_DEVELOP_BASE})"
fi
# Handle failure - create issue if the workflow failed
- name: Create issue content file
if: failure() && steps.version.outputs.version
run: |
cat > /tmp/issue-content.md << 'EOF'
The release workflow failed for version v${{ steps.version.outputs.version }}.
**Details:**
- Release Type: ${{ steps.release_type.outputs.type }}
- Workflow Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- Triggered By: ${{ github.event_name }}
- Git Changes Committed: ${{ steps.commit.outputs.has_changes || 'false' }}
- Git Tag Created: ${{ steps.tag.outputs.tag_created || 'false' }}
**Recovery Steps:**
If git operations succeeded but NPM publish failed:
- The version is already in git
- Fix the NPM issue and run `npm run release:${{ steps.release_type.outputs.dist_tag }}` locally
- Or re-run this workflow (it will skip git operations if no changes)
If git operations failed:
- No packages were published to NPM (safe state)
- Fix the git issue (permissions, network, etc.)
- Re-run the workflow
**Action Required:**
- Check the workflow logs for the specific failure point
- Follow the appropriate recovery steps above
EOF
- name: Create failure issue
if: failure() && steps.version.outputs.version
uses: peter-evans/create-issue-from-file@v5
with:
token: ${{ secrets.GITHUB_TOKEN }}
title: 'Release Failed: v${{ steps.version.outputs.version }}'
content-filepath: /tmp/issue-content.md
labels: |
bug
release
automated issue