Release #5
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
| # Release workflow automates the following: | |
| # - Manual releases (via workflow_dispatch) | |
| # - Automatic releases on tag pushes (v*.*.* tags) | |
| # - Actions on PR merges to main branch affecting release-monitored packages or workflow files | |
| # | |
| # **Phases (may be run independently):** | |
| # - full-release: Complete release sequence (version bump → changelog update/generation → build → publish) | |
| # - version-bump: Only bump version and create git tag | |
| # - changelog-update: Update unreleased changelog sections only | |
| # - changelog-generate: Generate changelogs for a specific tag | |
| # - build-only: Build packages without publishing | |
| # - publish-only: Publish to npm for an existing tag | |
| # - version-bump: Only bump versions and create tag | |
| # - changelog-update: Only update unreleased changelogs | |
| # - changelog-generate: Generate changelogs for specific tag | |
| # - build-only: Only build packages | |
| # - publish-only: Only publish to npm (requires tag) | |
| # | |
| # **Triggers:** | |
| # 1. Manual workflow_dispatch - Full control via inputs | |
| # 2. Tag push v*.*.* - Automatic publish phase | |
| # 3. PR merge to main - Auto version-bump and changelog-update (if packages changed) | |
| # | |
| # **First Release (NPM_TOKEN):** | |
| # - Requires NPM_TOKEN secret in GitHub repository | |
| # - Create token: npmjs.com → Account Settings → Access Tokens → Generate New Token (Automation) | |
| # - Add secret: GitHub repo → Settings → Secrets and variables → Actions → New repository secret | |
| # - Name: NPM_TOKEN | |
| # | |
| # **Subsequent Releases (OIDC - Recommended):** | |
| # - After first release, set up OIDC trusted publishing on npm | |
| # - Once OIDC is configured, NPM_TOKEN is no longer needed | |
| # - More secure than token-based authentication | |
| # - Automatic token rotation | |
| name: Release | |
| on: | |
| workflow_dispatch: {} | |
| push: | |
| tags: | |
| - v*.*.* # Matches v1.2.3 - Auto-trigger publish phase | |
| pull_request: | |
| types: [closed] | |
| branches: [main] | |
| paths: | |
| - packages/opennextjs-cli/** | |
| - packages/opennextjs-mcp/** | |
| - .github/workflows/release.yml | |
| - scripts/bump-version.ts | |
| permissions: read-all | |
| jobs: | |
| # Determine which phases to run based on trigger and inputs | |
| determine-phases: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| run_version_bump: ${{ steps.determine.outputs.run_version_bump }} | |
| run_changelog_update: ${{ steps.determine.outputs.run_changelog_update }} | |
| run_changelog_generate: ${{ steps.determine.outputs.run_changelog_generate }} | |
| run_build: ${{ steps.determine.outputs.run_build }} | |
| run_publish: ${{ steps.determine.outputs.run_publish }} | |
| phase: ${{ steps.determine.outputs.phase }} | |
| version: ${{ steps.determine.outputs.version }} | |
| bump_type: ${{ steps.determine.outputs.bump_type }} | |
| steps: | |
| - name: Determine phases to run | |
| id: determine | |
| run: | | |
| set -e | |
| # Determine trigger type | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| # Manual trigger defaults to full-release | |
| PHASE="full-release" | |
| VERSION="" | |
| BUMP_TYPE="auto" | |
| echo "🚀 Manual trigger - Phase: $PHASE" | |
| elif [ "${{ github.event_name }}" == "push" ] && [[ "${{ github.ref }}" == refs/tags/v*.*.* ]]; then | |
| PHASE="publish-only" | |
| VERSION="" | |
| BUMP_TYPE="auto" | |
| echo "🚀 Tag push trigger - Phase: $PHASE" | |
| elif [ "${{ github.event_name }}" == "pull_request" ] && [ "${{ github.event.pull_request.merged }}" == "true" ]; then | |
| PHASE="version-bump" | |
| VERSION="" | |
| BUMP_TYPE="auto" | |
| echo "🚀 PR merge trigger - Phase: $PHASE (with changelog-update)" | |
| else | |
| echo "❌ Unsupported trigger: ${{ github.event_name }}" >&2 | |
| exit 1 | |
| fi | |
| # Determine which phases to run | |
| if [ "$PHASE" == "full-release" ]; then | |
| RUN_VERSION_BUMP="true" | |
| RUN_CHANGELOG_UPDATE="true" | |
| RUN_CHANGELOG_GENERATE="true" | |
| RUN_BUILD="true" | |
| RUN_PUBLISH="true" | |
| elif [ "$PHASE" == "version-bump" ]; then | |
| RUN_VERSION_BUMP="true" | |
| RUN_CHANGELOG_UPDATE="false" | |
| RUN_CHANGELOG_GENERATE="false" | |
| RUN_BUILD="false" | |
| RUN_PUBLISH="false" | |
| # PR merge also runs changelog-update | |
| if [ "${{ github.event_name }}" == "pull_request" ]; then | |
| RUN_CHANGELOG_UPDATE="true" | |
| fi | |
| elif [ "$PHASE" == "changelog-update" ]; then | |
| RUN_VERSION_BUMP="false" | |
| RUN_CHANGELOG_UPDATE="true" | |
| RUN_CHANGELOG_GENERATE="false" | |
| RUN_BUILD="false" | |
| RUN_PUBLISH="false" | |
| elif [ "$PHASE" == "changelog-generate" ]; then | |
| RUN_VERSION_BUMP="false" | |
| RUN_CHANGELOG_UPDATE="false" | |
| RUN_CHANGELOG_GENERATE="true" | |
| RUN_BUILD="false" | |
| RUN_PUBLISH="false" | |
| elif [ "$PHASE" == "build-only" ]; then | |
| RUN_VERSION_BUMP="false" | |
| RUN_CHANGELOG_UPDATE="false" | |
| RUN_CHANGELOG_GENERATE="false" | |
| RUN_BUILD="true" | |
| RUN_PUBLISH="false" | |
| elif [ "$PHASE" == "publish-only" ]; then | |
| RUN_VERSION_BUMP="false" | |
| RUN_CHANGELOG_UPDATE="false" | |
| RUN_CHANGELOG_GENERATE="true" | |
| RUN_BUILD="true" | |
| RUN_PUBLISH="true" | |
| else | |
| echo "❌ Invalid phase: $PHASE" >&2 | |
| exit 1 | |
| fi | |
| echo "run_version_bump=$RUN_VERSION_BUMP" >> "$GITHUB_OUTPUT" | |
| echo "run_changelog_update=$RUN_CHANGELOG_UPDATE" >> "$GITHUB_OUTPUT" | |
| echo "run_changelog_generate=$RUN_CHANGELOG_GENERATE" >> "$GITHUB_OUTPUT" | |
| echo "run_build=$RUN_BUILD" >> "$GITHUB_OUTPUT" | |
| echo "run_publish=$RUN_PUBLISH" >> "$GITHUB_OUTPUT" | |
| echo "phase=$PHASE" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "bump_type=$BUMP_TYPE" >> "$GITHUB_OUTPUT" | |
| echo "✅ Phase determination complete:" | |
| echo " - Version Bump: $RUN_VERSION_BUMP" | |
| echo " - Changelog Update: $RUN_CHANGELOG_UPDATE" | |
| echo " - Changelog Generate: $RUN_CHANGELOG_GENERATE" | |
| echo " - Build: $RUN_BUILD" | |
| echo " - Publish: $RUN_PUBLISH" | |
| # Phase 1: Version Bump | |
| version-bump: | |
| needs: determine-phases | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| outputs: | |
| NEW_VERSION: ${{ steps.new_version.outputs.NEW_VERSION != '' && steps.new_version.outputs.NEW_VERSION || steps.init.outputs.NEW_VERSION || '' }} | |
| permissions: | |
| actions: read | |
| checks: read | |
| contents: write # Required to commit and push tags | |
| deployments: none | |
| issues: none | |
| packages: none | |
| pull-requests: read | |
| repository-projects: none | |
| security-events: none | |
| statuses: read | |
| id-token: none | |
| steps: | |
| - name: Initialize outputs | |
| id: init | |
| run: | | |
| echo "NEW_VERSION=" >> "$GITHUB_OUTPUT" | |
| - name: Check if version bump is needed | |
| id: check | |
| run: | | |
| if [ "${{ needs.determine-phases.outputs.run_version_bump }}" != "true" ]; then | |
| echo "SKIP=true" >> "$GITHUB_OUTPUT" | |
| echo "⏭️ Skipping version bump phase" | |
| exit 0 | |
| fi | |
| echo "SKIP=false" >> "$GITHUB_OUTPUT" | |
| - name: Starting phase - Version Bump | |
| if: steps.check.outputs.SKIP != 'true' | |
| run: 'echo "🚀 Starting phase: Version Bump"' | |
| - name: 📋 Step - Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 # Full history for git-cliff | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: 📋 Step - Setup pnpm | |
| uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 | |
| - name: 📋 Step - Setup Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: '22' | |
| cache: pnpm | |
| - name: 📋 Step - Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: 📋 Step - Setup git-cliff | |
| uses: kenji-miyake/setup-git-cliff@e4913b34dd9c321f11f2ef3c3866f800714a1d2e # v1 | |
| with: | |
| version: latest | |
| - name: 📋 Step - Get last tag | |
| id: last_tag | |
| run: | | |
| set -e | |
| echo "⏳ Checking for existing tags..." | |
| LAST_TAG=$(git describe --tags --match 'v*.*.*' --abbrev=0 2>/dev/null || echo "") | |
| if [ -z "$LAST_TAG" ]; then | |
| echo "📋 No previous tag found, starting from v0.1.0" | |
| CURRENT_VERSION="0.1.0" | |
| TAG="" | |
| else | |
| CURRENT_VERSION="${LAST_TAG#v}" | |
| TAG="$LAST_TAG" | |
| echo "📋 Found last tag: $TAG (version: $CURRENT_VERSION)" | |
| fi | |
| echo "CURRENT_VERSION=$CURRENT_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "TAG=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: 📋 Step - Check for changes | |
| id: changes | |
| run: | | |
| set -e | |
| if [ -z "${{ steps.last_tag.outputs.TAG }}" ]; then | |
| echo "📋 No previous tag, assuming we have changes" | |
| echo "HAS_CHANGES=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| COMMIT_COUNT=$(git rev-list --count "${{ steps.last_tag.outputs.TAG }}"..HEAD 2>/dev/null || echo "0") | |
| if [ "$COMMIT_COUNT" -gt 0 ]; then | |
| echo "📋 Found $COMMIT_COUNT commits since ${{ steps.last_tag.outputs.TAG }}" | |
| echo "HAS_CHANGES=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "📋 No new commits since last tag" | |
| echo "HAS_CHANGES=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: 📋 Step - Calculate new version | |
| id: version_calc | |
| if: steps.changes.outputs.HAS_CHANGES == 'true' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -e | |
| echo "⏳ Calculating new version..." | |
| VERSION_INPUT="${{ needs.determine-phases.outputs.version }}" | |
| BUMP_TYPE="${{ needs.determine-phases.outputs.bump_type }}" | |
| CURRENT_VERSION="${{ steps.last_tag.outputs.CURRENT_VERSION }}" | |
| LAST_TAG="${{ steps.last_tag.outputs.TAG }}" | |
| if [ -n "$VERSION_INPUT" ]; then | |
| # Use provided version | |
| NEW_VERSION="$VERSION_INPUT" | |
| echo "📋 Using provided version: $NEW_VERSION" | |
| elif [ "$BUMP_TYPE" == "auto" ]; then | |
| # Auto-calculate from commits | |
| if [ -z "$LAST_TAG" ]; then | |
| NEW_VERSION="0.1.0" | |
| echo "📋 No previous tag, using initial version: $NEW_VERSION" | |
| else | |
| NEW_VERSION_TAG=$(git-cliff --config cliff.toml --bumped-version 2>&1 | tail -1 || echo "") | |
| NEW_VERSION="${NEW_VERSION_TAG#v}" | |
| echo "📋 Auto-calculated version from commits: $NEW_VERSION" | |
| fi | |
| else | |
| # Manual bump type | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" | |
| if [ "$BUMP_TYPE" == "major" ]; then | |
| NEW_VERSION="$((MAJOR + 1)).0.0" | |
| elif [ "$BUMP_TYPE" == "minor" ]; then | |
| NEW_VERSION="$MAJOR.$((MINOR + 1)).0" | |
| elif [ "$BUMP_TYPE" == "patch" ]; then | |
| NEW_VERSION="$MAJOR.$MINOR.$((PATCH + 1))" | |
| else | |
| echo "❌ Invalid bump type: $BUMP_TYPE" >&2 | |
| exit 1 | |
| fi | |
| echo "📋 Manual bump ($BUMP_TYPE): $CURRENT_VERSION → $NEW_VERSION" | |
| fi | |
| # Validate version | |
| if ! echo "$NEW_VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then | |
| echo "❌ Error: Invalid version '$NEW_VERSION'" >&2 | |
| exit 1 | |
| fi | |
| # For first release (no previous tag), always create tag even if version matches | |
| # For subsequent releases, skip if version hasn't changed | |
| if [ -z "$LAST_TAG" ]; then | |
| echo "📋 First release: will create tag v$NEW_VERSION even though version matches package.json" | |
| # Don't skip - proceed with tag creation | |
| elif [ "$NEW_VERSION" == "$CURRENT_VERSION" ]; then | |
| echo "⚠️ No version bump needed (already at $CURRENT_VERSION)" | |
| echo "SKIP_RELEASE=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "NEW_VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "✅ Version calculated: $CURRENT_VERSION → $NEW_VERSION" | |
| - name: 📋 Step - Bump version in both packages | |
| id: new_version | |
| if: steps.changes.outputs.HAS_CHANGES == 'true' && steps.version_calc.outputs.SKIP_RELEASE != 'true' | |
| run: | | |
| set -e | |
| echo "⏳ Updating package.json files..." | |
| NEW_VERSION="${{ steps.version_calc.outputs.NEW_VERSION }}" | |
| node -e " | |
| const fs = require('fs'); | |
| // Update CLI package.json | |
| const cliPath = 'packages/opennextjs-cli/package.json'; | |
| const cliPkg = JSON.parse(fs.readFileSync(cliPath, 'utf8')); | |
| cliPkg.version = '$NEW_VERSION'; | |
| fs.writeFileSync(cliPath, JSON.stringify(cliPkg, null, 2) + '\n'); | |
| console.log('✅ Updated @jsonbored/opennextjs-cli to version $NEW_VERSION'); | |
| // Update MCP package.json | |
| const mcpPath = 'packages/opennextjs-mcp/package.json'; | |
| const mcpPkg = JSON.parse(fs.readFileSync(mcpPath, 'utf8')); | |
| mcpPkg.version = '$NEW_VERSION'; | |
| // Update MCP's dependency on CLI | |
| if (mcpPkg.dependencies && mcpPkg.dependencies['@jsonbored/opennextjs-cli']) { | |
| mcpPkg.dependencies['@jsonbored/opennextjs-cli'] = '^$NEW_VERSION'; | |
| } | |
| fs.writeFileSync(mcpPath, JSON.stringify(mcpPkg, null, 2) + '\n'); | |
| console.log('✅ Updated @jsonbored/opennextjs-mcp to version $NEW_VERSION'); | |
| console.log('✅ Updated MCP dependency on CLI to ^$NEW_VERSION'); | |
| " | |
| echo "NEW_VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT" | |
| echo "✅ Both packages updated to version $NEW_VERSION" | |
| - name: 📋 Step - Configure git | |
| if: steps.changes.outputs.HAS_CHANGES == 'true' && steps.version_calc.outputs.SKIP_RELEASE != 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: 📋 Step - Commit and tag | |
| if: steps.changes.outputs.HAS_CHANGES == 'true' && steps.version_calc.outputs.SKIP_RELEASE != 'true' | |
| run: | | |
| set -e | |
| echo "⏳ Committing version changes and creating tag..." | |
| NEW_VERSION="${{ steps.new_version.outputs.NEW_VERSION }}" | |
| # Commit version changes | |
| git add packages/opennextjs-cli/package.json packages/opennextjs-mcp/package.json | |
| if git diff --staged --quiet; then | |
| echo "⚠️ No version changes to commit" | |
| else | |
| git commit -m "chore: bump version to $NEW_VERSION" || exit 0 | |
| echo "✅ Version changes committed" | |
| fi | |
| # Create tag | |
| git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION" | |
| echo "✅ Tag v$NEW_VERSION created" | |
| # Push commit and tag | |
| git push origin main "v$NEW_VERSION" | |
| echo "✅ Committed and tagged v$NEW_VERSION" | |
| echo "✅ Phase: Version Bump - Complete" | |
| # Phase 2: Changelog Update (unreleased) | |
| changelog-update: | |
| needs: determine-phases | |
| if: needs.determine-phases.outputs.run_changelog_update == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| actions: read | |
| checks: read | |
| contents: write # Required to commit changelog updates | |
| deployments: none | |
| issues: none | |
| packages: none | |
| pull-requests: read | |
| repository-projects: none | |
| security-events: none | |
| statuses: read | |
| id-token: none | |
| steps: | |
| - name: 🚀 Starting phase - Changelog Update | |
| run: 'echo "🚀 Starting phase: Changelog Update (Unreleased)"' | |
| - name: 📋 Step - Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 # Full history for git-cliff | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: 📋 Step - Setup pnpm | |
| uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 | |
| - name: 📋 Step - Setup Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: '22' | |
| cache: pnpm | |
| - name: 📋 Step - Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: 📋 Step - Setup git-cliff | |
| uses: kenji-miyake/setup-git-cliff@e4913b34dd9c321f11f2ef3c3866f800714a1d2e # v1 | |
| with: | |
| version: latest | |
| - name: 📋 Step - Generate unreleased changelog | |
| id: changelog | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -e | |
| echo "⏳ Checking for unreleased commits..." | |
| LAST_TAG=$(git describe --tags --match v*.*.* --abbrev=0 2>/dev/null || echo "") | |
| if [ -z "$LAST_TAG" ]; then | |
| echo "📋 No previous tag found, skipping changelog update" | |
| echo "HAS_CHANGES=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| COMMIT_COUNT=$(git rev-list --count "$LAST_TAG"..HEAD 2>/dev/null || echo "0") | |
| if [ "$COMMIT_COUNT" -eq 0 ]; then | |
| echo "📋 No new commits since last tag, skipping changelog update" | |
| echo "HAS_CHANGES=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "📋 Found $COMMIT_COUNT commits since $LAST_TAG" | |
| echo "HAS_CHANGES=true" >> "$GITHUB_OUTPUT" | |
| # Generate CLI changelog (writes directly to packages/opennextjs-cli/CHANGELOG.md) | |
| echo "⏳ Generating CLI changelog (unreleased)..." | |
| pnpm changelog:package opennextjs-cli --unreleased || { | |
| echo "⚠️ CLI changelog generation failed" >&2 | |
| exit 1 | |
| } | |
| echo "✅ CLI changelog generated" | |
| # Generate MCP changelog (writes directly to packages/opennextjs-mcp/CHANGELOG.md) | |
| echo "⏳ Generating MCP changelog (unreleased)..." | |
| pnpm changelog:package opennextjs-mcp --unreleased || { | |
| echo "⚠️ MCP changelog generation failed" >&2 | |
| exit 1 | |
| } | |
| echo "✅ MCP changelog generated" | |
| echo "✅ Changelogs updated for both packages" | |
| - name: 📋 Step - Configure git | |
| if: steps.changelog.outputs.HAS_CHANGES == 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: 📋 Step - Commit and push changelogs | |
| if: steps.changelog.outputs.HAS_CHANGES == 'true' | |
| run: | | |
| set -e | |
| echo "⏳ Committing changelog updates..." | |
| git add packages/opennextjs-cli/CHANGELOG.md packages/opennextjs-mcp/CHANGELOG.md | |
| if git diff --staged --quiet; then | |
| echo "📋 No changelog changes to commit" | |
| exit 0 | |
| fi | |
| git commit -m "docs: update package changelogs with unreleased changes [skip ci]" | |
| git push origin main | |
| echo "✅ Changelogs committed and pushed" | |
| echo "✅ Phase: Changelog Update - Complete" | |
| # Phase 3: Changelog Generate (for specific tag) | |
| changelog-generate: | |
| needs: [determine-phases, version-bump] | |
| if: | | |
| needs.determine-phases.outputs.run_changelog_generate == 'true' && | |
| (needs.version-bump.result == 'success' || needs.version-bump.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| actions: read | |
| checks: read | |
| contents: read | |
| deployments: none | |
| issues: none | |
| packages: none | |
| pull-requests: read | |
| repository-projects: none | |
| security-events: none | |
| statuses: read | |
| id-token: none | |
| steps: | |
| - name: 🚀 Starting phase - Changelog Generate | |
| run: 'echo "🚀 Starting phase: Changelog Generate (for tag)"' | |
| - name: 📋 Step - Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 # Full history for changelog generation | |
| - name: 📋 Step - Setup pnpm | |
| uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 | |
| - name: 📋 Step - Setup Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: '22' | |
| cache: pnpm | |
| - name: 📋 Step - Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: 📋 Step - Setup git-cliff | |
| uses: kenji-miyake/setup-git-cliff@e4913b34dd9c321f11f2ef3c3866f800714a1d2e # v1 | |
| with: | |
| version: latest | |
| - name: 📋 Step - Determine tag to use | |
| id: tag_info | |
| run: | | |
| set -e | |
| # Get tag from version-bump job if it ran, otherwise from push event or determine version | |
| if [ "${{ github.event_name }}" == "push" ] && [[ "${{ github.ref }}" == refs/tags/v*.*.* ]]; then | |
| TAG_NAME="${{ github.ref_name }}" | |
| VERSION="${TAG_NAME#v}" | |
| echo "📋 Using tag from push event: $TAG_NAME" | |
| elif [ "${{ needs.version-bump.outputs.NEW_VERSION }}" != "" ]; then | |
| VERSION="${{ needs.version-bump.outputs.NEW_VERSION }}" | |
| TAG_NAME="v$VERSION" | |
| echo "📋 Using tag from version-bump: $TAG_NAME" | |
| elif [ -n "${{ needs.determine-phases.outputs.version }}" ]; then | |
| VERSION="${{ needs.determine-phases.outputs.version }}" | |
| TAG_NAME="v$VERSION" | |
| echo "📋 Using tag from input: $TAG_NAME" | |
| else | |
| # Try to get from package.json | |
| VERSION=$(node -p "require('./packages/opennextjs-cli/package.json').version") | |
| TAG_NAME="v$VERSION" | |
| echo "📋 Using tag from package.json: $TAG_NAME" | |
| fi | |
| # Check if tag exists | |
| if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then | |
| echo "TAG_EXISTS=true" >> "$GITHUB_OUTPUT" | |
| echo "✅ Tag verified: $TAG_NAME (version: $VERSION)" | |
| else | |
| echo "TAG_EXISTS=false" >> "$GITHUB_OUTPUT" | |
| echo "⚠️ Tag '$TAG_NAME' does not exist yet (first release)" | |
| echo " Will use --unreleased flag for changelog generation" | |
| fi | |
| echo "TAG=$TAG_NAME" >> "$GITHUB_OUTPUT" | |
| echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" | |
| - name: 📋 Step - Generate changelogs for tag | |
| id: changelog | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -e | |
| TAG="${{ steps.tag_info.outputs.TAG }}" | |
| VERSION="${{ steps.tag_info.outputs.VERSION }}" | |
| TAG_EXISTS="${{ steps.tag_info.outputs.TAG_EXISTS }}" | |
| # Use --unreleased if tag doesn't exist (first release), otherwise use --tag | |
| if [ "$TAG_EXISTS" == "true" ]; then | |
| echo "⏳ Generating changelogs for tag $TAG..." | |
| CLI_FLAG="--tag $TAG" | |
| MCP_FLAG="--tag $TAG" | |
| else | |
| echo "⏳ Generating changelogs for unreleased commits (first release)..." | |
| CLI_FLAG="--unreleased" | |
| MCP_FLAG="--unreleased" | |
| fi | |
| # Generate CLI changelog | |
| echo "⏳ Generating CLI changelog..." | |
| pnpm changelog:package opennextjs-cli $CLI_FLAG || { | |
| echo "⚠️ CLI changelog generation failed" >&2 | |
| exit 1 | |
| } | |
| echo "✅ CLI changelog generated" | |
| # Generate MCP changelog | |
| echo "⏳ Generating MCP changelog..." | |
| pnpm changelog:package opennextjs-mcp $MCP_FLAG || { | |
| echo "⚠️ MCP changelog generation failed" >&2 | |
| exit 1 | |
| } | |
| echo "✅ MCP changelog generated" | |
| # Extract changelog sections for GitHub Release notes | |
| echo "⏳ Extracting changelog sections for release notes..." | |
| # Start with version header | |
| echo "## [$VERSION] - $(date +%Y-%m-%d)" > /tmp/changelog-section.md | |
| echo "" >> /tmp/changelog-section.md | |
| # Extract CLI section | |
| if [ -f "packages/opennextjs-cli/CHANGELOG.md" ]; then | |
| CLI_SECTION=$(awk "/^## \[$VERSION\]/,/^## \[|^<!-- generated/" "packages/opennextjs-cli/CHANGELOG.md" | head -n -1 || true) | |
| if [ -n "$CLI_SECTION" ]; then | |
| echo "### @jsonbored/opennextjs-cli" >> /tmp/changelog-section.md | |
| echo "" >> /tmp/changelog-section.md | |
| echo "$CLI_SECTION" | sed '/^## \[/d' >> /tmp/changelog-section.md || true | |
| echo "" >> /tmp/changelog-section.md | |
| fi | |
| fi | |
| # Extract MCP section | |
| if [ -f "packages/opennextjs-mcp/CHANGELOG.md" ]; then | |
| MCP_SECTION=$(awk "/^## \[$VERSION\]/,/^## \[|^<!-- generated/" "packages/opennextjs-mcp/CHANGELOG.md" | head -n -1 || true) | |
| if [ -n "$MCP_SECTION" ]; then | |
| echo "### @jsonbored/opennextjs-mcp" >> /tmp/changelog-section.md | |
| echo "" >> /tmp/changelog-section.md | |
| echo "$MCP_SECTION" | sed '/^## \[/d' >> /tmp/changelog-section.md || true | |
| echo "" >> /tmp/changelog-section.md | |
| fi | |
| fi | |
| # If no package-specific sections, create a generic one | |
| if [ ! -s /tmp/changelog-section.md ] || [ "$(wc -l < /tmp/changelog-section.md)" -lt 5 ]; then | |
| echo "Initial release of OpenNext.js CLI and MCP server packages." >> /tmp/changelog-section.md | |
| echo "" >> /tmp/changelog-section.md | |
| fi | |
| # Set changelog section for GitHub release | |
| echo "CHANGELOG_SECTION<<EOF" >> "$GITHUB_ENV" | |
| cat /tmp/changelog-section.md >> "$GITHUB_ENV" | |
| echo "EOF" >> "$GITHUB_ENV" | |
| echo "✅ Changelogs generated successfully" | |
| echo " - packages/opennextjs-cli/CHANGELOG.md" | |
| echo " - packages/opennextjs-mcp/CHANGELOG.md" | |
| - name: 📋 Step - Configure git | |
| if: steps.changelog.outputs.HAS_CHANGES == 'true' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: 📋 Step - Commit changelogs (if needed) | |
| if: steps.changelog.outputs.HAS_CHANGES == 'true' | |
| run: | | |
| set -e | |
| echo "⏳ Checking if changelogs need to be committed..." | |
| git add packages/opennextjs-cli/CHANGELOG.md packages/opennextjs-mcp/CHANGELOG.md | |
| if git diff --staged --quiet; then | |
| echo "📋 No changelog changes to commit" | |
| else | |
| TAG="${{ steps.tag_info.outputs.TAG }}" | |
| git commit -m "chore: update changelogs for $TAG release" || exit 0 | |
| git push origin main || exit 0 | |
| echo "✅ Changelogs committed and pushed" | |
| fi | |
| echo "✅ Phase: Changelog Generate - Complete" | |
| # Phase 4: Build | |
| build: | |
| needs: [determine-phases, changelog-generate, version-bump] | |
| if: | | |
| needs.determine-phases.outputs.run_build == 'true' && | |
| (needs.changelog-generate.result == 'success' || needs.changelog-generate.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| actions: read | |
| checks: read | |
| contents: read | |
| deployments: none | |
| issues: none | |
| packages: none | |
| pull-requests: read | |
| repository-projects: none | |
| security-events: none | |
| statuses: read | |
| id-token: none | |
| steps: | |
| - name: 🚀 Starting phase - Build | |
| run: 'echo "🚀 Starting phase: Build"' | |
| - name: 📋 Step - Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: 📋 Step - Setup pnpm | |
| uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 | |
| - name: 📋 Step - Setup Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: '22' | |
| cache: pnpm | |
| - name: 📋 Step - Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: 📋 Step - Lint | |
| run: | | |
| echo "⏳ Running linters..." | |
| pnpm lint | |
| echo "✅ Linting passed" | |
| - name: 📋 Step - Type check | |
| run: | | |
| echo "⏳ Running type check..." | |
| pnpm type-check | |
| echo "✅ Type check passed" | |
| - name: 📋 Step - Test | |
| run: | | |
| echo "⏳ Running tests..." | |
| pnpm test | |
| echo "✅ Tests passed" | |
| - name: 📋 Step - Build CLI package | |
| working-directory: packages/opennextjs-cli | |
| run: | | |
| echo "⏳ Building @jsonbored/opennextjs-cli..." | |
| pnpm build | |
| echo "✅ CLI package built successfully" | |
| - name: 📋 Step - Build MCP package | |
| working-directory: packages/opennextjs-mcp | |
| run: | | |
| echo "⏳ Building @jsonbored/opennextjs-mcp..." | |
| pnpm build | |
| echo "✅ MCP package built successfully" | |
| - name: ✅ Phase complete - Build | |
| run: 'echo "✅ Phase: Build - Complete"' | |
| # Phase 5: Publish | |
| publish: | |
| needs: [determine-phases, build, changelog-generate, version-bump] | |
| if: | | |
| needs.determine-phases.outputs.run_publish == 'true' && | |
| (needs.build.result == 'success' || needs.build.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| actions: read | |
| checks: read | |
| contents: write # Required to create GitHub releases | |
| deployments: none | |
| id-token: write # Required for npm publish (OIDC) | |
| issues: none | |
| packages: none | |
| pull-requests: read | |
| repository-projects: none | |
| security-events: none | |
| statuses: read | |
| steps: | |
| - name: 🚀 Starting phase - Publish | |
| run: 'echo "🚀 Starting phase: Publish"' | |
| - name: 📋 Step - Checkout repository | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 # Full history for changelog generation | |
| - name: 📋 Step - Setup pnpm | |
| uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 | |
| - name: 📋 Step - Setup Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: '22' | |
| registry-url: https://registry.npmjs.org | |
| cache: pnpm | |
| - name: 📋 Step - Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: 📋 Step - Determine version and tag | |
| id: version | |
| run: | | |
| set -e | |
| echo "⏳ Determining version and tag..." | |
| # Get version from various sources | |
| if [ "${{ github.event_name }}" == "push" ] && [[ "${{ github.ref }}" == refs/tags/v*.*.* ]]; then | |
| TAG_NAME="${{ github.ref_name }}" | |
| VERSION="${TAG_NAME#v}" | |
| echo "📋 Using tag from push event: $TAG_NAME" | |
| elif [ "${{ needs.version-bump.outputs.NEW_VERSION }}" != "" ]; then | |
| VERSION="${{ needs.version-bump.outputs.NEW_VERSION }}" | |
| TAG_NAME="v$VERSION" | |
| echo "📋 Using version from version-bump: $VERSION" | |
| elif [ -n "${{ needs.determine-phases.outputs.version }}" ]; then | |
| VERSION="${{ needs.determine-phases.outputs.version }}" | |
| TAG_NAME="v$VERSION" | |
| echo "📋 Using version from input: $VERSION" | |
| else | |
| VERSION=$(node -p "require('./packages/opennextjs-cli/package.json').version") | |
| TAG_NAME="v$VERSION" | |
| echo "📋 Using version from package.json: $VERSION" | |
| fi | |
| echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "TAG=$TAG_NAME" >> "$GITHUB_OUTPUT" | |
| echo "✅ Version determined: $VERSION (tag: $TAG_NAME)" | |
| - name: 📋 Step - Verify package.json versions match | |
| run: | | |
| set -e | |
| echo "⏳ Verifying package.json versions match tag..." | |
| TAG_VERSION="${{ steps.version.outputs.VERSION }}" | |
| # Verify CLI version | |
| CLI_VERSION=$(node -p "require('./packages/opennextjs-cli/package.json').version") | |
| if [ "$CLI_VERSION" != "$TAG_VERSION" ]; then | |
| echo "❌ CLI version mismatch: package.json ($CLI_VERSION) != tag ($TAG_VERSION)" >&2 | |
| exit 1 | |
| fi | |
| echo "✅ CLI version match: $CLI_VERSION" | |
| # Verify MCP version | |
| MCP_VERSION=$(node -p "require('./packages/opennextjs-mcp/package.json').version") | |
| if [ "$MCP_VERSION" != "$TAG_VERSION" ]; then | |
| echo "❌ MCP version mismatch: package.json ($MCP_VERSION) != tag ($TAG_VERSION)" >&2 | |
| exit 1 | |
| fi | |
| echo "✅ MCP version match: $MCP_VERSION" | |
| - name: 📋 Step - Build CLI package | |
| working-directory: packages/opennextjs-cli | |
| run: | | |
| echo "⏳ Building @jsonbored/opennextjs-cli..." | |
| pnpm build | |
| echo "✅ CLI package built" | |
| - name: 📋 Step - Build MCP package | |
| working-directory: packages/opennextjs-mcp | |
| run: | | |
| echo "⏳ Building @jsonbored/opennextjs-mcp..." | |
| pnpm build | |
| echo "✅ MCP package built" | |
| - name: 📋 Step - Setup git-cliff for changelog extraction | |
| uses: kenji-miyake/setup-git-cliff@e4913b34dd9c321f11f2ef3c3866f800714a1d2e # v1 | |
| with: | |
| version: latest | |
| - name: 📋 Step - Extract changelog for GitHub Release | |
| id: changelog_extract | |
| if: needs.changelog-generate.result == 'success' | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -e | |
| VERSION="${{ steps.version.outputs.VERSION }}" | |
| echo "⏳ Extracting changelog sections for GitHub Release..." | |
| # Use changelog from changelog-generate phase if available | |
| if [ -n "${{ env.CHANGELOG_SECTION }}" ]; then | |
| echo "📋 Using changelog from changelog-generate phase" | |
| echo "${{ env.CHANGELOG_SECTION }}" > /tmp/changelog-section.md | |
| else | |
| # Fallback: extract from existing changelog files | |
| echo "## [$VERSION] - $(date +%Y-%m-%d)" > /tmp/changelog-section.md | |
| echo "" >> /tmp/changelog-section.md | |
| # Extract CLI section | |
| if [ -f "packages/opennextjs-cli/CHANGELOG.md" ]; then | |
| CLI_SECTION=$(awk "/^## \[$VERSION\]/,/^## \[|^<!-- generated/" "packages/opennextjs-cli/CHANGELOG.md" | head -n -1 || true) | |
| if [ -n "$CLI_SECTION" ]; then | |
| echo "### @jsonbored/opennextjs-cli" >> /tmp/changelog-section.md | |
| echo "" >> /tmp/changelog-section.md | |
| echo "$CLI_SECTION" | sed '/^## \[/d' >> /tmp/changelog-section.md || true | |
| echo "" >> /tmp/changelog-section.md | |
| fi | |
| fi | |
| # Extract MCP section | |
| if [ -f "packages/opennextjs-mcp/CHANGELOG.md" ]; then | |
| MCP_SECTION=$(awk "/^## \[$VERSION\]/,/^## \[|^<!-- generated/" "packages/opennextjs-mcp/CHANGELOG.md" | head -n -1 || true) | |
| if [ -n "$MCP_SECTION" ]; then | |
| echo "### @jsonbored/opennextjs-mcp" >> /tmp/changelog-section.md | |
| echo "" >> /tmp/changelog-section.md | |
| echo "$MCP_SECTION" | sed '/^## \[/d' >> /tmp/changelog-section.md || true | |
| echo "" >> /tmp/changelog-section.md | |
| fi | |
| fi | |
| # If no sections, create generic one | |
| if [ ! -s /tmp/changelog-section.md ] || [ "$(wc -l < /tmp/changelog-section.md)" -lt 5 ]; then | |
| echo "Release of OpenNext.js CLI and MCP server packages." >> /tmp/changelog-section.md | |
| echo "" >> /tmp/changelog-section.md | |
| fi | |
| fi | |
| # Set for GitHub release | |
| echo "CHANGELOG_SECTION<<EOF" >> "$GITHUB_ENV" | |
| cat /tmp/changelog-section.md >> "$GITHUB_ENV" | |
| echo "EOF" >> "$GITHUB_ENV" | |
| echo "✅ Changelog extracted for GitHub Release" | |
| - name: 📋 Step - Publish CLI to npm (try OIDC first) | |
| id: publish-cli-oidc | |
| working-directory: packages/opennextjs-cli | |
| run: | | |
| set -e | |
| echo "⏳ Publishing @jsonbored/opennextjs-cli to npm (trying OIDC first)..." | |
| # Try publishing with OIDC (if configured) | |
| if npm publish --access public 2>&1; then | |
| echo "✅ Published @jsonbored/opennextjs-cli@${{ steps.version.outputs.VERSION }} to npm (via OIDC)" | |
| echo "SUCCESS=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "⚠️ OIDC publish failed, will try NPM_TOKEN fallback" | |
| echo "SUCCESS=false" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| fi | |
| continue-on-error: true | |
| - name: 📋 Step - Publish CLI to npm (fallback to NPM_TOKEN) | |
| if: steps.publish-cli-oidc.outputs.SUCCESS != 'true' | |
| working-directory: packages/opennextjs-cli | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -e | |
| echo "⏳ Publishing @jsonbored/opennextjs-cli to npm (using NPM_TOKEN)..." | |
| if [ -z "$NODE_AUTH_TOKEN" ]; then | |
| echo "❌ NPM_TOKEN secret not found. For first release, you need to:" >&2 | |
| echo " 1. Create npm automation token: https://www.npmjs.com/settings/JSONbored/tokens" >&2 | |
| echo " 2. Add NPM_TOKEN secret to GitHub: Settings → Secrets and variables → Actions" >&2 | |
| echo " 3. Name the secret: NPM_TOKEN" >&2 | |
| exit 1 | |
| fi | |
| # Configure npm to use token | |
| echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" > ~/.npmrc | |
| npm publish --access public | |
| echo "✅ Published @jsonbored/opennextjs-cli@${{ steps.version.outputs.VERSION }} to npm (via NPM_TOKEN)" | |
| - name: 📋 Step - Publish MCP to npm (try OIDC first) | |
| id: publish-mcp-oidc | |
| working-directory: packages/opennextjs-mcp | |
| run: | | |
| set -e | |
| echo "⏳ Publishing @jsonbored/opennextjs-mcp to npm (trying OIDC first)..." | |
| # Try publishing with OIDC (if configured) | |
| if npm publish --access public 2>&1; then | |
| echo "✅ Published @jsonbored/opennextjs-mcp@${{ steps.version.outputs.VERSION }} to npm (via OIDC)" | |
| echo "SUCCESS=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "⚠️ OIDC publish failed, will try NPM_TOKEN fallback" | |
| echo "SUCCESS=false" >> "$GITHUB_OUTPUT" | |
| exit 1 | |
| fi | |
| continue-on-error: true | |
| - name: 📋 Step - Publish MCP to npm (fallback to NPM_TOKEN) | |
| if: steps.publish-mcp-oidc.outputs.SUCCESS != 'true' | |
| working-directory: packages/opennextjs-mcp | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| set -e | |
| echo "⏳ Publishing @jsonbored/opennextjs-mcp to npm (using NPM_TOKEN)..." | |
| if [ -z "$NODE_AUTH_TOKEN" ]; then | |
| echo "❌ NPM_TOKEN secret not found" >&2 | |
| exit 1 | |
| fi | |
| # Configure npm to use token (if not already configured) | |
| if [ ! -f ~/.npmrc ]; then | |
| echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" > ~/.npmrc | |
| fi | |
| npm publish --access public | |
| echo "✅ Published @jsonbored/opennextjs-mcp@${{ steps.version.outputs.VERSION }} to npm (via NPM_TOKEN)" | |
| echo "" >&2 | |
| echo "💡 After first release, set up OIDC trusted publishing for both packages:" >&2 | |
| echo " 1. Go to: https://www.npmjs.com/settings/JSONbored/automation" >&2 | |
| echo " 2. Add trusted publisher for @jsonbored/opennextjs-cli" >&2 | |
| echo " 3. Add trusted publisher for @jsonbored/opennextjs-mcp" >&2 | |
| echo " 4. Both use: repository JSONbored/opennextjs-cli, workflow .github/workflows/release.yml" >&2 | |
| echo " Then you can remove the NPM_TOKEN secret." >&2 | |
| - name: 📋 Step - Create GitHub Release | |
| uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 | |
| with: | |
| tag_name: ${{ steps.version.outputs.TAG }} | |
| name: Release ${{ steps.version.outputs.VERSION }} | |
| body: | | |
| ## Packages Released | |
| - `@jsonbored/opennextjs-cli@${{ steps.version.outputs.VERSION }}` | |
| - `@jsonbored/opennextjs-mcp@${{ steps.version.outputs.VERSION }}` | |
| ${{ env.CHANGELOG_SECTION }} | |
| draft: false | |
| prerelease: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: ✅ Phase complete - Publish | |
| run: | | |
| echo "✅ Phase: Publish - Complete" | |
| echo "✅ Both packages published to npm" | |
| echo "✅ GitHub Release created" |