chore(ci): soften plugin-tests xprocess comment #1491
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: NPM Release | |
| # Consolidated 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 | |
| concurrency: | |
| # Release runs create and push commits/tags. Serialize them per branch so | |
| # later runs don't fail with non-fast-forward pushes while an earlier release | |
| # is still updating the branch. | |
| group: npm-release-${{ github.ref }} | |
| cancel-in-progress: false | |
| 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 | |
| issues: write | |
| id-token: write | |
| actions: read | |
| steps: | |
| - name: Wait for other workflow runs before alpha publish | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/develop' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ github.sha }} | |
| BRANCH: ${{ github.ref_name }} | |
| RUN_ID: ${{ github.run_id }} | |
| MAX_WAIT_SECONDS: "7200" | |
| POLL_SECONDS: "30" | |
| run: | | |
| set -euo pipefail | |
| echo "Waiting for all other workflow runs for ${SHA} to complete successfully before publishing alpha." | |
| sleep 30 | |
| started_at="$(date +%s)" | |
| while true; do | |
| runs_json="$(gh api \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "/repos/${REPO}/actions/runs?head_sha=${SHA}&branch=${BRANCH}&event=push&per_page=100")" | |
| other_runs="$(jq --argjson run_id "${RUN_ID}" ' | |
| [ | |
| .workflow_runs[] | |
| | select(.id != $run_id) | |
| | { | |
| id, | |
| name, | |
| status, | |
| conclusion, | |
| html_url | |
| } | |
| ] | |
| ' <<< "${runs_json}")" | |
| echo "Observed peer workflow runs:" | |
| jq -r '.[] | "- \(.name): \(.status) / \(.conclusion // "pending") \(.html_url)"' <<< "${other_runs}" | |
| failed_runs="$(jq ' | |
| [ | |
| .[] | |
| | select(.status == "completed") | |
| | select(.conclusion != "success") | |
| ] | |
| ' <<< "${other_runs}")" | |
| if [[ "$(jq 'length' <<< "${failed_runs}")" -gt 0 ]]; then | |
| echo "::error::At least one peer workflow run failed, was cancelled, or did not conclude successfully." | |
| jq -r '.[] | "::error::\(.name) concluded \(.conclusion) - \(.html_url)"' <<< "${failed_runs}" | |
| exit 1 | |
| fi | |
| pending_runs="$(jq '[.[] | select(.status != "completed")]' <<< "${other_runs}")" | |
| if [[ "$(jq 'length' <<< "${pending_runs}")" -eq 0 ]]; then | |
| echo "All peer workflow runs completed successfully." | |
| break | |
| fi | |
| elapsed="$(( $(date +%s) - started_at ))" | |
| if [[ "${elapsed}" -ge "${MAX_WAIT_SECONDS}" ]]; then | |
| echo "::error::Timed out waiting for peer workflow runs after ${elapsed}s." | |
| jq -r '.[] | select(.status != "completed") | "::error::Still waiting on \(.name): \(.status) - \(.html_url)"' <<< "${pending_runs}" | |
| exit 1 | |
| fi | |
| echo "Waiting ${POLL_SECONDS}s for peer workflows to finish..." | |
| sleep "${POLL_SECONDS}" | |
| done | |
| - name: Verify develop still points at this commit | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/develop' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| remote_sha="$(gh api \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "/repos/${REPO}/git/ref/heads/develop" \ | |
| --jq '.object.sha')" | |
| if [[ "${remote_sha}" != "${SHA}" ]]; then | |
| echo "::error::develop advanced to ${remote_sha}; refusing to publish stale alpha for ${SHA}." | |
| exit 1 | |
| fi | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| submodules: recursive | |
| - 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: Install system dependencies | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libpq-dev postgresql-client protobuf-compiler libwayland-dev libpipewire-0.3-dev libegl-dev libgbm-dev libxcb1-dev libssl-dev | |
| - name: Setup Node | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "24.x" | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.3.13" | |
| - name: Install dependencies | |
| run: | | |
| restore_package_json() { | |
| if [[ -n "${PACKAGE_JSON_BACKUP:-}" && -f "${PACKAGE_JSON_BACKUP}" ]]; then | |
| mv "${PACKAGE_JSON_BACKUP}" package.json | |
| fi | |
| } | |
| PACKAGE_JSON_BACKUP="$(mktemp)" | |
| cp package.json "${PACKAGE_JSON_BACKUP}" | |
| trap restore_package_json EXIT | |
| node - <<'NODE' | |
| const fs = require("node:fs"); | |
| const packageJsonPath = "package.json"; | |
| const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); | |
| const optionalWorkspaceEntries = [ | |
| "plugins/plugin-sql/typescript", | |
| "plugins/plugin-ollama/typescript", | |
| "plugins/plugin-local-ai/typescript", | |
| "plugins/plugin-pdf/typescript", | |
| "plugins/plugin-whatsapp/typescript", | |
| ]; | |
| if (!Array.isArray(pkg.workspaces)) { | |
| throw new Error("package.json is missing a workspaces array"); | |
| } | |
| let changed = false; | |
| for (const entry of optionalWorkspaceEntries) { | |
| if (fs.existsSync(`${entry}/package.json`) && !pkg.workspaces.includes(entry)) { | |
| pkg.workspaces.push(entry); | |
| changed = true; | |
| } | |
| } | |
| if (changed) { | |
| fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`); | |
| } | |
| NODE | |
| bun install --ignore-scripts | |
| restore_package_json | |
| trap - EXIT | |
| # --ignore-scripts prevents postinstall scripts from running, but build | |
| # tools like esbuild and bun need their postinstall to install platform binaries. | |
| # Run them explicitly so turbo and lerna prepublishOnly can spawn build processes. | |
| node node_modules/esbuild/install.js 2>/dev/null || true | |
| node node_modules/bun/install.js 2>/dev/null || true | |
| # 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..." | |
| BASE_VERSION=$(get_base_version "$CURRENT_VERSION") | |
| # Always check existing tags to find the true highest alpha version | |
| # This prevents tag conflicts when lerna.json is out of sync with tags | |
| echo "Checking for existing alpha tags for base version ${BASE_VERSION}..." | |
| git fetch --tags | |
| HIGHEST_TAG=$(git tag -l "v${BASE_VERSION}-alpha.*" 2>/dev/null | sort -V | tail -n 1) | |
| if [[ -n "$HIGHEST_TAG" ]]; then | |
| HIGHEST_VERSION=${HIGHEST_TAG#v} | |
| echo "Highest existing alpha tag: ${HIGHEST_VERSION}" | |
| # Only sync if lerna.json is BEHIND the tags (not ahead). | |
| # If lerna.json is ahead, a previous run already bumped it | |
| # but failed to create the tag — just increment from current. | |
| HIGHEST_NUM=$(echo "$HIGHEST_VERSION" | grep -o '[0-9]*$') | |
| CURRENT_NUM=$(echo "$CURRENT_VERSION" | grep -o '[0-9]*$') | |
| if [[ "$CURRENT_NUM" -lt "$HIGHEST_NUM" ]]; then | |
| echo "Syncing lerna.json from ${CURRENT_VERSION} to ${HIGHEST_VERSION} (behind tags)..." | |
| bunx lerna version ${HIGHEST_VERSION} \ | |
| --force-publish \ | |
| --yes \ | |
| --no-private \ | |
| --no-git-tag-version \ | |
| --no-push | |
| elif [[ "$CURRENT_NUM" -gt "$HIGHEST_NUM" ]]; then | |
| echo "lerna.json (${CURRENT_VERSION}) is ahead of highest tag (${HIGHEST_VERSION}) — skipping sync" | |
| fi | |
| # Now increment to the next alpha | |
| echo "Incrementing from $(node -p "require('./lerna.json').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 | |
| elif [[ "${RELEASE_TYPE}" == "beta" ]]; then | |
| echo "🔵 Beta release workflow..." | |
| BASE_VERSION=$(get_base_version "$CURRENT_VERSION") | |
| # Always check existing tags to find the true highest beta version | |
| echo "Checking for existing beta tags for base version ${BASE_VERSION}..." | |
| git fetch --tags | |
| HIGHEST_TAG=$(git tag -l "v${BASE_VERSION}-beta.*" 2>/dev/null | sort -V | tail -n 1) | |
| if [[ -n "$HIGHEST_TAG" ]]; then | |
| HIGHEST_VERSION=${HIGHEST_TAG#v} | |
| echo "Highest existing beta tag: ${HIGHEST_VERSION}" | |
| # Only sync if lerna.json is BEHIND the tags (not ahead). | |
| HIGHEST_NUM=$(echo "$HIGHEST_VERSION" | grep -o '[0-9]*$') | |
| CURRENT_NUM=$(echo "$CURRENT_VERSION" | grep -o '[0-9]*$') | |
| if [[ "$CURRENT_NUM" -lt "$HIGHEST_NUM" ]]; then | |
| echo "Syncing lerna.json from ${CURRENT_VERSION} to ${HIGHEST_VERSION} (behind tags)..." | |
| bunx lerna version ${HIGHEST_VERSION} \ | |
| --force-publish \ | |
| --yes \ | |
| --no-private \ | |
| --no-git-tag-version \ | |
| --no-push | |
| elif [[ "$CURRENT_NUM" -gt "$HIGHEST_NUM" ]]; then | |
| echo "lerna.json (${CURRENT_VERSION}) is ahead of highest tag (${HIGHEST_VERSION}) — skipping sync" | |
| fi | |
| # Now increment to the next beta | |
| echo "Incrementing from $(node -p "require('./lerna.json').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 | |
| 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 and verify it doesn't already exist on npm. | |
| # lerna publish from-package silently skips already-published versions, | |
| # so we must detect collisions here and bump again if needed. | |
| VERSION=$(node -p "require('./lerna.json').version") | |
| if [[ "${RELEASE_TYPE}" == "alpha" || "${RELEASE_TYPE}" == "beta" ]]; then | |
| MAX_RETRIES=3 | |
| for i in $(seq 1 $MAX_RETRIES); do | |
| # Check if this version already exists on npm | |
| if npm view "@elizaos/core@${VERSION}" version >/dev/null 2>&1; then | |
| echo "⚠️ Version ${VERSION} already exists on npm — bumping again (attempt ${i}/${MAX_RETRIES})" | |
| bunx lerna version prerelease \ | |
| --preid "${RELEASE_TYPE}" \ | |
| --force-publish \ | |
| --yes \ | |
| --no-private \ | |
| --no-git-tag-version \ | |
| --no-push | |
| VERSION=$(node -p "require('./lerna.json').version") | |
| sleep 1 # Brief delay for npm registry consistency | |
| else | |
| echo "✅ Version ${VERSION} is available on npm" | |
| break | |
| fi | |
| done | |
| # Final check — if still colliding after retries, fail loudly | |
| if npm view "@elizaos/core@${VERSION}" version >/dev/null 2>&1; then | |
| echo "❌ Version ${VERSION} still exists on npm after ${MAX_RETRIES} bumps — aborting" | |
| exit 1 | |
| fi | |
| fi | |
| echo "version=${VERSION}" >> $GITHUB_OUTPUT | |
| # Update lockfile after version changes | |
| - name: Update lockfile | |
| run: | | |
| bun install --no-frozen-lockfile --ignore-scripts || true | |
| # 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: | | |
| TAG_NAME="${{ steps.tag.outputs.tag_name }}" | |
| # 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..." | |
| else | |
| # For other triggers, push to current branch | |
| TARGET_BRANCH="${{ github.ref_name }}" | |
| fi | |
| retry_rebase_release_commit() { | |
| git fetch origin "${TARGET_BRANCH}" | |
| if ! git rebase "origin/${TARGET_BRANCH}"; then | |
| echo "❌ Error: Failed to rebase release commit onto origin/${TARGET_BRANCH}" | |
| return 1 | |
| fi | |
| if [[ -n "${TAG_NAME}" ]]; then | |
| if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" >/dev/null 2>&1; then | |
| echo "❌ Error: Tag ${TAG_NAME} already exists on origin after retry fetch" | |
| return 1 | |
| fi | |
| if git rev-parse "${TAG_NAME}" >/dev/null 2>&1; then | |
| git tag -d "${TAG_NAME}" | |
| fi | |
| git tag "${TAG_NAME}" | |
| fi | |
| } | |
| push_release_state() { | |
| git push origin HEAD:${TARGET_BRANCH} --follow-tags | |
| } | |
| PUSHED=false | |
| MAX_PUSH_ATTEMPTS=3 | |
| for ATTEMPT in $(seq 1 "${MAX_PUSH_ATTEMPTS}"); do | |
| if push_release_state; then | |
| PUSHED=true | |
| break | |
| fi | |
| if [[ "${ATTEMPT}" -eq "${MAX_PUSH_ATTEMPTS}" ]]; then | |
| break | |
| fi | |
| echo "⚠️ Push attempt ${ATTEMPT}/${MAX_PUSH_ATTEMPTS} failed — rebasing onto origin/${TARGET_BRANCH} and retrying..." | |
| if ! retry_rebase_release_commit; then | |
| break | |
| fi | |
| done | |
| if [[ "${PUSHED}" != "true" ]]; 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 " - The target branch changing too quickly to rebase cleanly" | |
| echo "" | |
| echo "The version has been updated locally but not published." | |
| echo "Manual intervention required to resolve the git push issue." | |
| exit 1 | |
| fi | |
| echo "✅ Successfully pushed version changes and tags to git" | |
| # Build packages with correct version numbers | |
| # Only happens AFTER git operations succeed | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: wasm32-unknown-unknown | |
| - name: Install wasm-pack | |
| run: cargo install wasm-pack --locked | |
| - name: Cache Rust dependencies | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| packages/rust/target | |
| plugins/plugin-sql/rust/target | |
| key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo-release- | |
| - name: Build WASM packages | |
| run: | | |
| echo "Building WASM packages..." | |
| # Build core WASM | |
| echo "Building @elizaos/core WASM..." | |
| cd packages/rust | |
| wasm-pack build --target web --out-dir pkg/web --features wasm --no-default-features || echo "WASM web build skipped" | |
| wasm-pack build --target nodejs --out-dir pkg/node --features wasm --no-default-features || echo "WASM node build skipped" | |
| cd ../.. | |
| # Build plugin-sql WASM (if exists) | |
| if [ -d "plugins/plugin-sql/rust" ]; then | |
| echo "Building @elizaos/plugin-sql WASM..." | |
| cd plugins/plugin-sql/rust | |
| wasm-pack build --target web --out-dir pkg/web --features wasm --no-default-features || echo "WASM web build skipped" | |
| wasm-pack build --target nodejs --out-dir pkg/node --features wasm --no-default-features || echo "WASM node build skipped" | |
| cd ../../.. | |
| fi | |
| echo "WASM builds complete" | |
| - name: Build connector plugin artifacts | |
| env: | |
| SKIP_PYTHON_BUILD: "1" | |
| run: | | |
| if [ -f "plugins/plugin-whatsapp/typescript/package.json" ]; then | |
| echo "Building @elizaos/plugin-whatsapp artifacts..." | |
| (cd plugins/plugin-whatsapp/typescript && bun run build) | |
| fi | |
| - name: Build packages | |
| env: | |
| SKIP_PYTHON_BUILD: "1" | |
| run: | | |
| echo "Building packages with version v${{ steps.version.outputs.version }}..." | |
| # Build only the publish-critical package graph so unrelated plugin cycles | |
| # do not block alpha/beta releases. | |
| RELEASE_BUILD_FILTERS=( | |
| --filter=@elizaos/agent | |
| --filter=@elizaos/app-core | |
| --filter=elizaos | |
| --filter=@elizaos/interop | |
| --filter=@elizaos/plugin-pdf | |
| --filter=@elizaos/prompts | |
| --filter=@elizaos/shared | |
| --filter=@elizaos/skills | |
| --filter=@elizaos/core | |
| --filter=@elizaos/ui | |
| ) | |
| bunx turbo run build --continue "${RELEASE_BUILD_FILTERS[@]}" || \ | |
| echo "Some packages had build errors — checking critical packages..." | |
| # Fail fast if any publish-critical package is missing its release artifacts. | |
| for artifact in \ | |
| packages/agent/dist/package.json \ | |
| packages/app-core/dist/package.json \ | |
| packages/elizaos/dist/index.js \ | |
| packages/interop/dist/index.d.ts \ | |
| packages/prompts/dist/typescript/index.ts \ | |
| packages/shared/dist/package.json \ | |
| packages/skills/dist/index.js \ | |
| packages/typescript/dist/index.node.js \ | |
| plugins/plugin-whatsapp/typescript/dist/src/index.d.ts \ | |
| packages/ui/dist/package.json; do | |
| if [ ! -f "${artifact}" ]; then | |
| echo "❌ Missing publish artifact: ${artifact}" | |
| exit 1 | |
| fi | |
| done | |
| echo "✅ All publish-critical packages built successfully" | |
| # Replace workspace:* references with actual versions before publishing | |
| # This is required because Bun workspaces use workspace:* protocol | |
| # which npm doesn't understand when the packages are published | |
| - name: Replace workspace references | |
| id: replace_workspace | |
| run: | | |
| echo "🔄 Replacing workspace:* references with actual versions..." | |
| node scripts/replace-workspace-versions.js | |
| echo "✅ Workspace references replaced" | |
| # Publish to NPM (only after git operations succeed) | |
| - name: Skip NPM publish on forks | |
| if: github.repository != 'elizaOS/eliza' | |
| run: | | |
| echo "Skipping npm publish outside elizaOS/eliza." | |
| echo "Fork workflows still validate the release build, but only the canonical upstream publishes packages." | |
| - name: Publish to NPM | |
| if: github.repository == 'elizaOS/eliza' | |
| id: publish | |
| 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 | |
| # The release build rewrites files inside plugin git submodules | |
| # (workspace refs, generated artifacts, versioned outputs). Those | |
| # changes are intentional for the publish tarballs, but lerna's | |
| # working tree check treats the parent repo as dirty unless the | |
| # submodules are ignored locally for this CI checkout. | |
| while read -r _ path _; do | |
| git config --local "submodule.${path}.ignore" dirty | |
| done < <(git submodule status --recursive) | |
| # Publish with appropriate dist-tag | |
| # Commit workspace reference changes so lerna doesn't complain about uncommitted files | |
| git add -A | |
| git diff --staged --quiet || git commit -m "chore: replace workspace references for publishing [skip ci]" | |
| if ! bunx lerna publish from-package \ | |
| --dist-tag ${DIST_TAG} \ | |
| --force-publish \ | |
| --yes \ | |
| --no-verify-access \ | |
| --no-git-reset; 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}" | |
| # Verify the dist-tag actually points to the new version. | |
| # lerna publish silently skips already-published versions, so the | |
| # dist-tag may not have moved even though lerna reported success. | |
| - name: Verify dist-tag | |
| if: steps.publish.outcome == 'success' | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| DIST_TAG="${{ steps.release_type.outputs.dist_tag }}" | |
| INITIAL_ATTEMPTS=6 | |
| FIX_ATTEMPTS=12 | |
| SLEEP_SECONDS=10 | |
| echo "Verifying dist-tag '${DIST_TAG}' points to ${VERSION}..." | |
| get_actual_tag() { | |
| npm view "@elizaos/core@${DIST_TAG}" version 2>/dev/null || echo "unknown" | |
| } | |
| wait_for_dist_tag() { | |
| local expected="$1" | |
| local attempts="$2" | |
| local phase="$3" | |
| local actual="" | |
| for attempt in $(seq 1 "${attempts}"); do | |
| actual=$(get_actual_tag) | |
| if [[ "$actual" == "$expected" ]]; then | |
| echo "✅ dist-tag '${DIST_TAG}' points to ${expected} during ${phase} check (attempt ${attempt}/${attempts})" | |
| return 0 | |
| fi | |
| if [[ "$attempt" -lt "$attempts" ]]; then | |
| echo "⏳ dist-tag '${DIST_TAG}' currently points to ${actual}; waiting ${SLEEP_SECONDS}s for ${phase} propagation (${attempt}/${attempts})..." | |
| sleep "${SLEEP_SECONDS}" | |
| fi | |
| done | |
| echo "⚠️ dist-tag '${DIST_TAG}' still points to ${actual} after ${phase} check" | |
| return 1 | |
| } | |
| if wait_for_dist_tag "${VERSION}" "${INITIAL_ATTEMPTS}" "initial"; then | |
| exit 0 | |
| fi | |
| ACTUAL=$(get_actual_tag) | |
| echo "⚠️ dist-tag '${DIST_TAG}' points to ${ACTUAL}, expected ${VERSION}" | |
| echo "Forcing dist-tag update for all published packages..." | |
| # Get list of public packages from lerna, plus @elizaos/core | |
| # which is published from packages/typescript but may not appear | |
| # in lerna ls if the workspace config changed. | |
| PACKAGES=$(bunx lerna ls --json --no-private 2>/dev/null | node -e " | |
| const pkgs = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')); | |
| const names = new Set(pkgs.map(p => p.name)); | |
| names.add('@elizaos/core'); | |
| names.forEach(n => console.log(n)); | |
| ") | |
| FIXED=0 | |
| FAILED=0 | |
| PENDING=0 | |
| for PKG in $PACKAGES; do | |
| if npm view "${PKG}@${VERSION}" version >/dev/null 2>&1; then | |
| if npm dist-tag add "${PKG}@${VERSION}" "${DIST_TAG}" 2>/dev/null; then | |
| echo " ✅ Updated dist-tag for ${PKG}" | |
| FIXED=$((FIXED + 1)) | |
| else | |
| echo " ⚠️ Failed to update dist-tag for ${PKG}" | |
| FAILED=$((FAILED + 1)) | |
| fi | |
| else | |
| echo " ⏳ ${PKG}@${VERSION} is not visible on npm yet" | |
| PENDING=$((PENDING + 1)) | |
| fi | |
| done | |
| echo "✅ Dist-tag update attempts: ${FIXED} updated, ${FAILED} failed, ${PENDING} pending visibility" | |
| if ! wait_for_dist_tag "${VERSION}" "${FIX_ATTEMPTS}" "post-fix"; then | |
| echo "❌ dist-tag still not pointing to ${VERSION} after fix attempt" | |
| exit 1 | |
| fi | |
| # Always restore workspace:* references after publish (success or failure) | |
| # This keeps the repository clean for development | |
| - name: Restore workspace references | |
| if: always() && steps.replace_workspace.outcome == 'success' | |
| run: | | |
| echo "🔄 Restoring workspace:* references..." | |
| node scripts/restore-workspace-refs.js | |
| echo "✅ Workspace references restored" | |
| # 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@v3 | |
| 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 }} | |
| ``` | |
| --- | |
| > **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 (|| true: tolerate broken postinstall scripts) | |
| bun install --no-frozen-lockfile || true | |
| # 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 (|| true: tolerate broken postinstall scripts) | |
| bun install --no-frozen-lockfile || true | |
| # 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 (|| true: tolerate broken postinstall scripts) | |
| bun install --no-frozen-lockfile || true | |
| # 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 |