Publish Release #21
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: Publish Release | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| release_type: | |
| description: "Release type" | |
| required: true | |
| type: choice | |
| options: | |
| - stable | |
| - rc | |
| - beta | |
| default: stable | |
| pre_release_number: | |
| description: "Pre-release number (for rc/beta, leave empty for auto-increment)" | |
| required: false | |
| type: string | |
| dry_run: | |
| description: "Dry run (skip actual publish)" | |
| required: false | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| concurrency: | |
| group: publish-release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| publish: | |
| runs-on: ubuntu-latest | |
| environment: release | |
| outputs: | |
| version: ${{ steps.version.outputs.version }} | |
| is_prerelease: ${{ steps.version.outputs.is_prerelease }} | |
| branch: ${{ steps.context.outputs.branch }} | |
| env: | |
| NX_DAEMON: "false" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Validate branch | |
| id: context | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # Get current branch | |
| BRANCH="${GITHUB_REF#refs/heads/}" | |
| echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" | |
| # Validate branch is a release branch | |
| if [[ ! "$BRANCH" =~ ^release/[0-9]+\.[0-9]+\.x$ ]]; then | |
| echo "::error::This workflow must be run from a release/X.Y.x branch. Current branch: $BRANCH" | |
| exit 1 | |
| fi | |
| # Extract release line (X.Y) from release/X.Y.x | |
| RELEASE_LINE=$(echo "$BRANCH" | sed 's/release\/\([0-9]*\.[0-9]*\).*/\1/') | |
| echo "release_line=$RELEASE_LINE" >> "$GITHUB_OUTPUT" | |
| echo "Branch: $BRANCH" | |
| echo "Release line: $RELEASE_LINE" | |
| - name: Setup Node + Yarn | |
| uses: ./.github/actions/setup-node-yarn | |
| with: | |
| node-version-file: ".nvmrc" | |
| registry-url: "https://registry.npmjs.org/" | |
| # Do NOT run an immutable install here: this is a long-lived, | |
| # auto-incrementing release branch. Its committed yarn.lock may lag the | |
| # bumped package.json versions, and this job rewrites the lock anyway. | |
| # An immutable install would fail before the version-bump/sync steps run. | |
| install: "false" | |
| - name: Install dependencies | |
| # Mutable install: installs deps for nx/build AND self-heals any lockfile | |
| # drift left by a previous release before this run bumps the version. | |
| run: yarn install --no-immutable | |
| - name: Update npm CLI for trusted publishing | |
| run: npm install -g npm@latest | |
| - name: Compute version | |
| id: version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| RELEASE_LINE="${{ steps.context.outputs.release_line }}" | |
| RELEASE_TYPE="${{ inputs.release_type }}" | |
| PRE_RELEASE_NUM="${{ inputs.pre_release_number }}" | |
| # Fetch all tags | |
| git fetch --tags | |
| # Use compute-next-patch script | |
| if [ -n "$PRE_RELEASE_NUM" ]; then | |
| VERSION=$(node scripts/compute-next-patch.mjs "$RELEASE_LINE" "$RELEASE_TYPE" "$PRE_RELEASE_NUM") | |
| else | |
| VERSION=$(node scripts/compute-next-patch.mjs "$RELEASE_LINE" "$RELEASE_TYPE") | |
| fi | |
| # Determine if this is a pre-release | |
| IS_PRERELEASE="false" | |
| NPM_TAG="latest" | |
| if [[ "$VERSION" == *"-rc."* ]]; then | |
| IS_PRERELEASE="true" | |
| NPM_TAG="rc" | |
| elif [[ "$VERSION" == *"-beta."* ]]; then | |
| IS_PRERELEASE="true" | |
| NPM_TAG="beta" | |
| fi | |
| # Check if unified tag already exists | |
| if git rev-parse "v$VERSION" >/dev/null 2>&1; then | |
| echo "::error::Tag v$VERSION already exists!" | |
| exit 1 | |
| fi | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "release_type=$RELEASE_TYPE" >> "$GITHUB_OUTPUT" | |
| echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" | |
| echo "npm_tag=$NPM_TAG" >> "$GITHUB_OUTPUT" | |
| echo "Version: $VERSION" | |
| echo "Release type: $RELEASE_TYPE" | |
| echo "Is prerelease: $IS_PRERELEASE" | |
| echo "NPM tag: $NPM_TAG" | |
| - name: Configure git | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Get previous version | |
| id: prev_version | |
| run: | | |
| RELEASE_LINE="${{ steps.context.outputs.release_line }}" | |
| # Get the latest stable tag for this release line | |
| PREV_TAG=$(git tag --list "v${RELEASE_LINE}.*" --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1) | |
| if [ -z "$PREV_TAG" ]; then | |
| # No previous tag in this line, try previous minor | |
| PREV_TAG=$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1) | |
| fi | |
| echo "prev_tag=$PREV_TAG" >> "$GITHUB_OUTPUT" | |
| echo "Previous tag: $PREV_TAG" | |
| - name: Generate diff | |
| id: diff | |
| run: | | |
| PREV_TAG="${{ steps.prev_version.outputs.prev_tag }}" | |
| if [ -n "$PREV_TAG" ]; then | |
| DIFF=$(git diff "$PREV_TAG"..HEAD \ | |
| --stat --patch \ | |
| -- '*.ts' '*.js' '*.json' ':!package-lock.json' ':!*.test.ts' ':!*.spec.ts' \ | |
| | head -c 50000) | |
| else | |
| DIFF="Initial release - no previous version to compare" | |
| fi | |
| # Use file to avoid shell escaping issues | |
| echo "$DIFF" > /tmp/diff.txt | |
| - name: Generate AI changelog | |
| id: ai_changelog | |
| if: ${{ inputs.dry_run != true && inputs.release_type == 'stable' }} | |
| continue-on-error: true | |
| uses: actions/github-script@v7 | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | |
| OPENAI_MODEL: ${{ vars.OPENAI_MODEL }} | |
| VERSION: v${{ steps.version.outputs.version }} | |
| VERSION_MINOR: ${{ steps.context.outputs.release_line }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| // Skip if API key is missing | |
| if (!process.env.OPENAI_API_KEY) { | |
| core.warning('OPENAI_API_KEY missing; skipping AI changelog'); | |
| core.setOutput('changelog', ''); | |
| core.setOutput('has_card_mdx', 'false'); | |
| return; | |
| } | |
| try { | |
| const diff = fs.readFileSync('/tmp/diff.txt', 'utf8'); | |
| const releaseDate = new Date().toISOString().split('T')[0]; | |
| const version = process.env.VERSION; | |
| const versionNum = version.replace('v', ''); | |
| const prompt = `You are a technical writer for Enclave, a production-ready JavaScript sandbox for AI agent code execution. | |
| The Enclave ecosystem includes: | |
| - @enclave-vm/ast: AST security guard with CVE protection | |
| - @enclave-vm/core: Secure AgentScript execution environment | |
| - @enclave-vm/types: Protocol types and Zod schemas | |
| - @enclave-vm/stream: NDJSON streaming with encryption | |
| - @enclave-vm/broker: Tool broker with session management | |
| - @enclave-vm/client: Browser and Node.js client SDK | |
| - @enclave-vm/react: React hooks and components | |
| - @enclave-vm/runtime: Standalone deployable runtime | |
| Version: ${version} | |
| Release Date: ${releaseDate} | |
| Git diff: | |
| \`\`\` | |
| ${diff.substring(0, 40000)} | |
| \`\`\` | |
| Generate two outputs: | |
| 1. CHANGELOG entry (Keep a Changelog format): | |
| ## [${versionNum}] - ${releaseDate} | |
| ### Added/Changed/Fixed/Security (only include relevant sections) | |
| - Concise description of changes | |
| 2. A SINGLE Mintlify <Card> component (NOT the full file, just the Card): | |
| <Card | |
| title="Enclave ${version}: Brief title" | |
| href="https://github.com/agentfront/enclave/releases/tag/${version}" | |
| cta="View full changelog" | |
| > | |
| **Feature** – Description. | |
| - Details if needed | |
| </Card> | |
| IMPORTANT: For cardMdx, output ONLY the <Card>...</Card> component, nothing else. | |
| Output ONLY valid JSON: {"changelog": "...", "cardMdx": "<Card...>...</Card>"}`; | |
| const response = await fetch('https://api.openai.com/v1/chat/completions', { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| model: process.env.OPENAI_MODEL || 'gpt-4o', | |
| messages: [{ role: 'user', content: prompt }], | |
| response_format: { type: 'json_object' } | |
| }) | |
| }); | |
| if (!response.ok) throw new Error(`OpenAI API error: ${response.status}`); | |
| const data = await response.json(); | |
| const result = JSON.parse(data.choices[0].message.content); | |
| // Write card MDX to file to avoid shell escaping issues | |
| fs.writeFileSync('/tmp/card-mdx.txt', result.cardMdx); | |
| core.setOutput('changelog', result.changelog); | |
| core.setOutput('has_card_mdx', result.cardMdx ? 'true' : 'false'); | |
| } catch (err) { | |
| core.warning(`AI changelog skipped: ${err.message}`); | |
| core.setOutput('changelog', ''); | |
| core.setOutput('has_card_mdx', 'false'); | |
| } | |
| - name: Update package versions | |
| if: ${{ inputs.dry_run != true }} | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| echo "Setting version $VERSION for all libs" | |
| npx nx release version "$VERSION" --git-commit=false --git-tag=false | |
| - name: Update demo app dependency versions | |
| if: ${{ inputs.dry_run != true }} | |
| shell: bash | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| echo "Updating @enclave-vm/* dependencies in apps to $VERSION" | |
| for pkg in apps/*/package.json; do | |
| [ -f "$pkg" ] || continue | |
| # Update any @enclave-vm/* dependency versions | |
| node -e " | |
| const fs = require('fs'); | |
| const p = JSON.parse(fs.readFileSync('$pkg', 'utf8')); | |
| let changed = false; | |
| for (const section of ['dependencies', 'devDependencies', 'peerDependencies']) { | |
| if (!p[section]) continue; | |
| for (const [name, ver] of Object.entries(p[section])) { | |
| if (name.startsWith('@enclave-vm/') && ver !== '$VERSION') { | |
| p[section][name] = '$VERSION'; | |
| changed = true; | |
| } | |
| } | |
| } | |
| if (changed) { | |
| fs.writeFileSync('$pkg', JSON.stringify(p, null, 2) + '\n'); | |
| console.log('Updated: $pkg'); | |
| } else { | |
| console.log('No changes: $pkg'); | |
| } | |
| " | |
| done | |
| - name: Sync lockfile to bumped versions | |
| if: ${{ inputs.dry_run != true }} | |
| # Regenerate yarn.lock so it matches the just-bumped package.json versions | |
| # and prunes orphaned old-version entries. Without this the committed | |
| # lockfile drifts, and the next `yarn install --immutable` (e.g. this job's | |
| # setup step on the following release) fails with: | |
| # "The lockfile would have been modified by this install, which is forbidden." | |
| run: yarn install --no-immutable | |
| - name: Commit version bump | |
| if: ${{ inputs.dry_run != true }} | |
| run: | | |
| if [ -n "$(git status --porcelain)" ]; then | |
| git add -A | |
| git commit -m "chore(release): v${{ steps.version.outputs.version }}" | |
| git push origin HEAD | |
| fi | |
| - name: Build packages | |
| run: | | |
| echo "Building all lib packages..." | |
| yarn nx run-many --targets=build --projects='libs/*' --parallel | |
| - name: Publish to npm | |
| if: ${{ inputs.dry_run != true }} | |
| shell: bash | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| NPM_TAG="${{ steps.version.outputs.npm_tag }}" | |
| echo "Publishing all libs with tag $NPM_TAG..." | |
| npx nx release publish --tag="$NPM_TAG" | |
| echo "Successfully published version ${{ steps.version.outputs.version }}" | |
| - name: Create and push git tag | |
| if: ${{ inputs.dry_run != true }} | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| TAG="v$VERSION" | |
| BRANCH="${{ steps.context.outputs.branch }}" | |
| # Fetch latest to ensure we tag the committed version | |
| git fetch origin "$BRANCH" | |
| git checkout "$BRANCH" | |
| git pull origin "$BRANCH" | |
| git tag -a "$TAG" -m "Release $TAG" | |
| git push origin "$TAG" | |
| echo "Created and pushed tag: $TAG" | |
| - name: Prepare release body | |
| id: release_body | |
| env: | |
| CHANGELOG: ${{ steps.ai_changelog.outputs.changelog }} | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| RELEASE_TYPE="${{ steps.version.outputs.release_type }}" | |
| RELEASE_LINE="${{ steps.context.outputs.release_line }}" | |
| BRANCH="${{ steps.context.outputs.branch }}" | |
| IS_PRERELEASE="${{ steps.version.outputs.is_prerelease }}" | |
| PROJECTS="core,types,stream,broker,client,react,runtime,ast" | |
| # Start building the release body | |
| { | |
| echo "## Release v${VERSION}" | |
| echo "" | |
| echo "**Release type:** ${RELEASE_TYPE}" | |
| echo "**Release line:** ${RELEASE_LINE}.x" | |
| echo "**Branch:** ${BRANCH}" | |
| echo "" | |
| echo "### Published Packages" | |
| echo "" | |
| } > /tmp/release-body.md | |
| # List published packages with npm links | |
| IFS=',' read -ra LIBS <<< "$PROJECTS" | |
| for lib in "${LIBS[@]}"; do | |
| # Get npm package name from package.json | |
| if [ -f "libs/$lib/package.json" ]; then | |
| NPM_NAME=$(node -p "require('./libs/$lib/package.json').name") | |
| echo "- [\`${NPM_NAME}@${VERSION}\`](https://www.npmjs.com/package/${NPM_NAME}/v/${VERSION})" >> /tmp/release-body.md | |
| fi | |
| done | |
| # Add AI-generated changelog if available | |
| if [ -f /tmp/card-mdx.txt ] && [ -s /tmp/card-mdx.txt ] && [ -n "$CHANGELOG" ]; then | |
| echo "" >> /tmp/release-body.md | |
| echo "$CHANGELOG" >> /tmp/release-body.md | |
| fi | |
| # Add pre-release note if applicable | |
| if [ "$IS_PRERELEASE" = "true" ]; then | |
| echo "" >> /tmp/release-body.md | |
| echo "> **Note:** This is a pre-release version." >> /tmp/release-body.md | |
| fi | |
| # Add Card MDX as hidden comment for docs sync (only for stable releases) | |
| # NOTE: Content is sanitized to prevent --> from breaking the HTML comment. | |
| # Consumer must reverse: replace "-->" with "-->" after extraction. | |
| if [ -f /tmp/card-mdx.txt ] && [ -s /tmp/card-mdx.txt ]; then | |
| echo "" >> /tmp/release-body.md | |
| echo "<!--" >> /tmp/release-body.md | |
| echo "CARD_MDX_START" >> /tmp/release-body.md | |
| sed 's/-->/--\>/g' /tmp/card-mdx.txt >> /tmp/release-body.md | |
| echo "CARD_MDX_END" >> /tmp/release-body.md | |
| echo "-->" >> /tmp/release-body.md | |
| fi | |
| - name: Create GitHub Release | |
| if: ${{ inputs.dry_run != true }} | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ steps.version.outputs.version }} | |
| name: v${{ steps.version.outputs.version }} | |
| prerelease: ${{ steps.version.outputs.is_prerelease }} | |
| generate_release_notes: false | |
| body_path: /tmp/release-body.md | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Trigger docs sync | |
| if: ${{ inputs.dry_run != true && steps.version.outputs.is_prerelease == 'false' }} | |
| continue-on-error: true | |
| uses: actions/github-script@v7 | |
| env: | |
| VERSION: v${{ steps.version.outputs.version }} | |
| VERSION_MINOR: ${{ steps.context.outputs.release_line }} | |
| with: | |
| github-token: ${{ secrets.DOCS_SYNC_TOKEN }} | |
| script: | | |
| const tag = process.env.VERSION; | |
| const versionMinor = process.env.VERSION_MINOR; | |
| const sha = context.sha; | |
| console.log(`Triggering docs sync for enclave`); | |
| console.log(` Tag: ${tag}`); | |
| console.log(` SHA: ${sha}`); | |
| console.log(` Version minor: ${versionMinor}`); | |
| try { | |
| await github.rest.repos.createDispatchEvent({ | |
| owner: 'agentfront', | |
| repo: 'docs', | |
| event_type: 'sync-docs', | |
| client_payload: { | |
| repo: 'enclave', | |
| sha: sha, | |
| tag: tag, | |
| version_minor: versionMinor | |
| } | |
| }); | |
| console.log(`Successfully triggered docs sync for ${tag}`); | |
| } catch (error) { | |
| console.error(`Failed to trigger docs sync: ${error.message}`); | |
| // Don't fail the release for docs sync issues | |
| } | |
| - name: Summary | |
| run: | | |
| if [ "${{ inputs.dry_run }}" = "true" ]; then | |
| echo "## Dry Run Summary" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "> **This was a dry run. No packages were published.**" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "## Release Complete" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Property | Value |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|----------|-------|" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Version | \`${{ steps.version.outputs.version }}\` |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Tag | \`v${{ steps.version.outputs.version }}\` |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Release type | ${{ steps.version.outputs.release_type }} |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| NPM tag | \`${{ steps.version.outputs.npm_tag }}\` |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Pre-release | ${{ steps.version.outputs.is_prerelease }} |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Branch | \`${{ steps.context.outputs.branch }}\` |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Packages | All libs/* |" >> "$GITHUB_STEP_SUMMARY" | |
| cherry-pick-version-to-main: | |
| needs: publish | |
| if: > | |
| inputs.dry_run != true && | |
| needs.publish.outputs.is_prerelease == 'false' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Check if latest semver | |
| id: check | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ needs.publish.outputs.version }}" | |
| git fetch --tags | |
| # Get all stable version tags, sort by semver, pick highest | |
| LATEST=$(git tag --list 'v*' \ | |
| | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ | |
| | sort -V \ | |
| | tail -1 \ | |
| | sed 's/^v//') | |
| echo "Released version: $VERSION" | |
| echo "Latest stable tag: $LATEST" | |
| if [ "$VERSION" = "$LATEST" ]; then | |
| echo "is_latest=true" >> "$GITHUB_OUTPUT" | |
| echo "This is the latest version — will cherry-pick to main" | |
| else | |
| echo "is_latest=false" >> "$GITHUB_OUTPUT" | |
| echo "Skipping: v$VERSION is not the latest (v$LATEST is newer)" | |
| fi | |
| - name: Setup Node + Yarn | |
| if: steps.check.outputs.is_latest == 'true' | |
| uses: ./.github/actions/setup-node-yarn | |
| with: | |
| node-version-file: ".nvmrc" | |
| - name: Configure git | |
| if: steps.check.outputs.is_latest == 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Cherry-pick version bump to main | |
| if: steps.check.outputs.is_latest == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| VERSION="${{ needs.publish.outputs.version }}" | |
| RELEASE_BRANCH="${{ needs.publish.outputs.branch }}" | |
| DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" | |
| # Find the version bump commit on the release branch | |
| VERSION_COMMIT=$(git log "origin/$RELEASE_BRANCH" \ | |
| --grep="chore(release): v${VERSION}" \ | |
| --format="%H" -1) | |
| if [ -z "$VERSION_COMMIT" ]; then | |
| echo "::warning::Could not find version bump commit for v${VERSION}" | |
| exit 0 | |
| fi | |
| echo "Found version bump commit: $VERSION_COMMIT" | |
| git fetch origin "$DEFAULT_BRANCH" | |
| # Skip if the version bump is already on the default branch | |
| if git merge-base --is-ancestor "$VERSION_COMMIT" "origin/$DEFAULT_BRANCH"; then | |
| echo "Version bump commit $VERSION_COMMIT is already on $DEFAULT_BRANCH — skipping cherry-pick" | |
| exit 0 | |
| fi | |
| # Prepare cherry-pick branch | |
| CHERRY_BRANCH="cherry-pick/v${VERSION}-version-to-main" | |
| git checkout "$DEFAULT_BRANCH" | |
| git pull origin "$DEFAULT_BRANCH" | |
| # Clean up existing remote branch if any | |
| git push origin --delete "$CHERRY_BRANCH" 2>/dev/null || true | |
| git checkout -b "$CHERRY_BRANCH" | |
| # Attempt cherry-pick (may partially apply if main diverged from release branch) | |
| git cherry-pick "$VERSION_COMMIT" --no-commit || { | |
| echo "Cherry-pick had conflicts — resetting and using sync script instead" | |
| git cherry-pick --abort 2>/dev/null || true | |
| git checkout -- . 2>/dev/null || true | |
| } | |
| # Force-sync all package versions regardless of cherry-pick result | |
| echo "Running version sync to ensure all packages are at $VERSION..." | |
| node scripts/sync-versions.mjs "$VERSION" | |
| # Update yarn.lock to reflect dependency version changes. | |
| # --no-immutable is required: Yarn 4 enables immutable installs by | |
| # default in CI, but this step intentionally rewrites yarn.lock. | |
| yarn install --no-immutable | |
| # Stage all version-related changes | |
| git add libs/*/package.json apps/*/package.json yarn.lock | |
| # Check if there are actual changes to commit | |
| if [ -z "$(git diff --cached --name-only)" ]; then | |
| echo "No version changes needed — $DEFAULT_BRANCH is already at v$VERSION" | |
| exit 0 | |
| fi | |
| git commit -m "$(cat <<EOF | |
| chore: sync version to $VERSION | |
| Cherry-picked from $RELEASE_BRANCH (release v$VERSION) | |
| Original commit: $VERSION_COMMIT | |
| EOF | |
| )" | |
| git push origin "$CHERRY_BRANCH" | |
| gh pr create \ | |
| --base "$DEFAULT_BRANCH" \ | |
| --head "$CHERRY_BRANCH" \ | |
| --title "chore: sync version to v${VERSION}" \ | |
| --label "cherry-pick" \ | |
| --label "auto-cherry-pick" \ | |
| --body "$(cat <<EOF | |
| ## Version sync to main | |
| Updates all \`@enclave-vm/*\` package versions to \`${VERSION}\` on \`${DEFAULT_BRANCH}\`. | |
| This cherry-pick was automatically created because \`v${VERSION}\` is the **latest stable release**. | |
| **Source:** \`${RELEASE_BRANCH}\` release v${VERSION} | |
| --- | |
| _Auto-generated by the publish-release workflow._ | |
| EOF | |
| )" | |
| echo "Cherry-pick PR created successfully" |