feat: Enhance OpenNext.js CLI with project root detection and testing… #1
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
| # Package Release Workflow | ||
| # | ||
| # Automatically releases both @jsonbored/opennextjs-cli and @jsonbored/opennextjs-mcp | ||
| # packages to npm when a version tag is pushed. Supports both NPM_TOKEN (for first release) | ||
| # and OIDC (for subsequent releases). Both packages are released with synchronized versions. | ||
| # | ||
| # **What it does:** | ||
| # 1. Builds both packages (CLI and MCP) | ||
| # 2. Extracts version from tag (e.g., v1.0.0 → 1.0.0) | ||
| # 3. Verifies both package.json versions match tag version | ||
| # 4. Generates changelog automatically using git-cliff | ||
| # 5. Publishes both packages to npm (tries OIDC first, falls back to NPM_TOKEN) | ||
| # 6. Creates GitHub Release with changelog notes | ||
| # | ||
| # **Trigger:** | ||
| # Push a version tag: | ||
| # ```bash | ||
| # git tag v1.0.0 | ||
| # git push origin main --tags | ||
| # ``` | ||
| # | ||
| # **Prerequisites:** | ||
| # - package.json version must match tag version (e.g., 1.0.0) | ||
| # | ||
| # **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 | ||
| # - Value: Your npm automation token | ||
| # | ||
| # **Subsequent Releases (OIDC - Recommended):** | ||
| # - After first release, set up OIDC trusted publishing: | ||
| # 1. Go to npmjs.com → Account Settings → Access Tokens → Automation | ||
| # 2. Click "Add GitHub Actions" or "Configure" next to "Trusted Publishers" | ||
| # 3. Select repository: JSONbored/opennextjs-cli | ||
| # 4. Select workflow: .github/workflows/release.yml | ||
| # 5. Save | ||
| # - Once OIDC is configured, NPM_TOKEN is no longer needed | ||
| # - More secure than token-based authentication | ||
| # - Automatic token rotation | ||
| name: Release | ||
| on: | ||
| push: | ||
| tags: | ||
| - 'v*.*.*' # Matches v1.2.3 | ||
| workflow_dispatch: | ||
| inputs: | ||
| version: | ||
| description: 'Version to release (e.g., 0.1.0). Leave empty to use package.json version.' | ||
| required: false | ||
| type: string | ||
| jobs: | ||
| release: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
| permissions: | ||
| contents: write # Required to create GitHub releases | ||
| id-token: write # Required for npm publish (OIDC) | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 # Full history for changelog generation | ||
| - name: Setup pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: '22' | ||
| registry-url: 'https://registry.npmjs.org' | ||
| cache: 'pnpm' | ||
| - name: Install dependencies | ||
| run: pnpm install --frozen-lockfile | ||
| - name: Build CLI package | ||
| working-directory: packages/opennextjs-cli | ||
| run: pnpm build | ||
| - name: Build MCP package | ||
| working-directory: packages/opennextjs-mcp | ||
| run: pnpm build | ||
| - name: Extract version from tag or input | ||
| id: version | ||
| run: | | ||
| # For workflow_dispatch, use input version or package.json version | ||
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | ||
| if [ -n "${{ github.event.inputs.version }}" ]; then | ||
| VERSION="${{ github.event.inputs.version }}" | ||
| else | ||
| # Read from CLI package.json | ||
| VERSION=$(node -p "require('./packages/opennextjs-cli/package.json').version") | ||
| fi | ||
| else | ||
| # Extract version from tag (e.g., v1.0.0 → 1.0.0) | ||
| TAG_NAME="${{ github.ref_name }}" | ||
| VERSION="${TAG_NAME#v}" | ||
| fi | ||
| echo "VERSION=$VERSION" >> $GITHUB_OUTPUT | ||
| echo "Version: $VERSION" | ||
| - name: Verify CLI package.json version matches tag | ||
| working-directory: packages/opennextjs-cli | ||
| run: | | ||
| PACKAGE_VERSION=$(node -p "require('./package.json').version") | ||
| TAG_VERSION="${{ steps.version.outputs.VERSION }}" | ||
| if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then | ||
| echo "❌ CLI version mismatch: package.json ($PACKAGE_VERSION) != tag ($TAG_VERSION)" >&2 | ||
| exit 1 | ||
| fi | ||
| echo "✅ CLI version match: $PACKAGE_VERSION" | ||
| - name: Verify MCP package.json version matches tag | ||
| working-directory: packages/opennextjs-mcp | ||
| run: | | ||
| PACKAGE_VERSION=$(node -p "require('./package.json').version") | ||
| TAG_VERSION="${{ steps.version.outputs.VERSION }}" | ||
| if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then | ||
| echo "❌ MCP version mismatch: package.json ($PACKAGE_VERSION) != tag ($TAG_VERSION)" >&2 | ||
| exit 1 | ||
| fi | ||
| echo "✅ MCP version match: $PACKAGE_VERSION" | ||
| - name: Check if git-cliff is installed | ||
| id: git-cliff-check | ||
| run: | | ||
| if command -v git-cliff &> /dev/null; then | ||
| echo "installed=true" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "installed=false" >> $GITHUB_OUTPUT | ||
| fi | ||
| - name: Install git-cliff (if needed) | ||
| if: steps.git-cliff-check.outputs.installed == 'false' | ||
| run: | | ||
| # Install git-cliff on Ubuntu | ||
| sudo apt-get update && sudo apt-get install -y git-cliff || \ | ||
| echo "⚠️ git-cliff installation failed, will skip changelog generation" >&2 | ||
| - name: Generate package-specific changelog sections | ||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| run: | | ||
| set -e | ||
| TAG_VERSION="${{ steps.version.outputs.VERSION }}" | ||
| TAG="v$TAG_VERSION" | ||
| # Generate CLI changelog section (only commits affecting packages/opennextjs-cli/**) | ||
| echo "📝 Generating CLI changelog section..." | ||
| git-cliff \ | ||
| --config cliff.toml \ | ||
| --include-path "packages/opennextjs-cli/**" \ | ||
| --tag "$TAG" \ | ||
| --latest \ | ||
| --unreleased \ | ||
| --output /tmp/cli-changelog.md \ | ||
| --verbose || \ | ||
| echo "⚠️ CLI changelog generation failed" >&2 | ||
| # Generate MCP changelog section (only commits affecting packages/opennextjs-mcp/**) | ||
| echo "📝 Generating MCP changelog section..." | ||
| git-cliff \ | ||
| --config cliff.toml \ | ||
| --include-path "packages/opennextjs-mcp/**" \ | ||
| --tag "$TAG" \ | ||
| --latest \ | ||
| --unreleased \ | ||
| --output /tmp/mcp-changelog.md \ | ||
| --verbose || \ | ||
| echo "⚠️ MCP changelog generation failed" >&2 | ||
| # Combine changelog sections | ||
| echo "📝 Combining changelog sections..." | ||
| # Start with version header | ||
| echo "## [$TAG_VERSION] - $(date +%Y-%m-%d)" > /tmp/changelog-section.md | ||
| echo "" >> /tmp/changelog-section.md | ||
| # Extract CLI section (skip header, get body) | ||
| if [ -f /tmp/cli-changelog.md ] && [ -s /tmp/cli-changelog.md ]; then | ||
| echo "### @jsonbored/opennextjs-cli" >> /tmp/changelog-section.md | ||
| echo "" >> /tmp/changelog-section.md | ||
| # Skip the header and version line, get the body | ||
| tail -n +2 /tmp/cli-changelog.md | sed '/^## \[/,$d' >> /tmp/changelog-section.md || true | ||
| echo "" >> /tmp/changelog-section.md | ||
| fi | ||
| # Extract MCP section (skip header, get body) | ||
| if [ -f /tmp/mcp-changelog.md ] && [ -s /tmp/mcp-changelog.md ]; then | ||
| echo "### @jsonbored/opennextjs-mcp" >> /tmp/changelog-section.md | ||
| echo "" >> /tmp/changelog-section.md | ||
| # Skip the header and version line, get the body | ||
| tail -n +2 /tmp/mcp-changelog.md | sed '/^## \[/,$d' >> /tmp/changelog-section.md || true | ||
| echo "" >> /tmp/changelog-section.md | ||
| fi | ||
| # If no package-specific sections, create a generic one | ||
| if [ ! -s /tmp/cli-changelog.md ] && [ ! -s /tmp/mcp-changelog.md ]; then | ||
| echo "Initial release of OpenNext.js CLI and MCP server packages." >> /tmp/changelog-section.md | ||
| echo "" >> /tmp/changelog-section.md | ||
| fi | ||
| # Update main CHANGELOG.md (prepend new section) | ||
| if [ -f CHANGELOG.md ]; then | ||
| # Prepend new section to existing changelog | ||
| cat /tmp/changelog-section.md CHANGELOG.md > /tmp/combined-changelog.md | ||
| mv /tmp/combined-changelog.md CHANGELOG.md | ||
| else | ||
| # Create new changelog with header | ||
| cat > CHANGELOG.md << 'EOF' | ||
| # Changelog | ||
| All notable changes to this project will be documented in this file. | ||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
| EOF | ||
| cat /tmp/changelog-section.md >> CHANGELOG.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 "✅ Changelog generated successfully" | ||
| - name: Publish CLI to npm (try OIDC first) | ||
| id: publish-cli-oidc | ||
| working-directory: packages/opennextjs-cli | ||
| run: | | ||
| # Try publishing with OIDC (if configured) | ||
| npm publish --access public && \ | ||
| echo "✅ Published @jsonbored/opennextjs-cli@${{ steps.version.outputs.VERSION }} to npm (via OIDC)" || \ | ||
| echo "⚠️ OIDC publish failed, will try NPM_TOKEN fallback" | ||
| continue-on-error: true | ||
| - name: Publish CLI to npm (fallback to NPM_TOKEN) | ||
| if: steps.publish-cli-oidc.outcome == 'failure' | ||
| working-directory: packages/opennextjs-cli | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| run: | | ||
| 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: Publish MCP to npm (try OIDC first) | ||
| id: publish-mcp-oidc | ||
| working-directory: packages/opennextjs-mcp | ||
| run: | | ||
| # Try publishing with OIDC (if configured) | ||
| npm publish --access public && \ | ||
| echo "✅ Published @jsonbored/opennextjs-mcp@${{ steps.version.outputs.VERSION }} to npm (via OIDC)" || \ | ||
| echo "⚠️ OIDC publish failed, will try NPM_TOKEN fallback" | ||
| continue-on-error: true | ||
| - name: Publish MCP to npm (fallback to NPM_TOKEN) | ||
| if: steps.publish-mcp-oidc.outcome == 'failure' | ||
| working-directory: packages/opennextjs-mcp | ||
| env: | ||
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||
| run: | | ||
| 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: Create GitHub Release | ||
| uses: softprops/action-gh-release@v2 | ||
| with: | ||
| tag_name: ${{ github.ref_name }} | ||
| 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 }} | ||