feat: add automated release workflow for npm publishing
- Add release.yml GitHub workflow for automated npm releases - Add prepare-release.js script for version bumping and changelog - Add extract-changelog.js for release notes extraction - Add test-release-automation.js for testing the workflow - Add documentation for automated releases This enables automatic npm publishing when tags are pushed, fixing the issue where releases were created but npm packages were not published. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
513
.github/workflows/release.yml
vendored
Normal file
513
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
name: Automated Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'package.json'
|
||||||
|
- 'package.runtime.json'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
# Prevent concurrent releases
|
||||||
|
concurrency:
|
||||||
|
group: release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect-version-change:
|
||||||
|
name: Detect Version Change
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version-changed: ${{ steps.check.outputs.changed }}
|
||||||
|
new-version: ${{ steps.check.outputs.version }}
|
||||||
|
previous-version: ${{ steps.check.outputs.previous-version }}
|
||||||
|
is-prerelease: ${{ steps.check.outputs.is-prerelease }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Check for version change
|
||||||
|
id: check
|
||||||
|
run: |
|
||||||
|
# Get current version from package.json
|
||||||
|
CURRENT_VERSION=$(node -e "console.log(require('./package.json').version)")
|
||||||
|
|
||||||
|
# Get previous version from git history safely
|
||||||
|
PREVIOUS_VERSION=$(git show HEAD~1:package.json 2>/dev/null | node -e "
|
||||||
|
try {
|
||||||
|
const data = require('fs').readFileSync(0, 'utf8');
|
||||||
|
const pkg = JSON.parse(data);
|
||||||
|
console.log(pkg.version || '0.0.0');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('0.0.0');
|
||||||
|
}
|
||||||
|
" || echo "0.0.0")
|
||||||
|
|
||||||
|
echo "Previous version: $PREVIOUS_VERSION"
|
||||||
|
echo "Current version: $CURRENT_VERSION"
|
||||||
|
|
||||||
|
# Check if version changed
|
||||||
|
if [ "$CURRENT_VERSION" != "$PREVIOUS_VERSION" ]; then
|
||||||
|
echo "changed=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "previous-version=$PREVIOUS_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Check if it's a prerelease (contains alpha, beta, rc, dev)
|
||||||
|
if echo "$CURRENT_VERSION" | grep -E "(alpha|beta|rc|dev)" > /dev/null; then
|
||||||
|
echo "is-prerelease=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "is-prerelease=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🎉 Version changed from $PREVIOUS_VERSION to $CURRENT_VERSION"
|
||||||
|
else
|
||||||
|
echo "changed=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "previous-version=$PREVIOUS_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "is-prerelease=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "ℹ️ No version change detected"
|
||||||
|
fi
|
||||||
|
|
||||||
|
extract-changelog:
|
||||||
|
name: Extract Changelog
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: detect-version-change
|
||||||
|
if: needs.detect-version-change.outputs.version-changed == 'true'
|
||||||
|
outputs:
|
||||||
|
release-notes: ${{ steps.extract.outputs.notes }}
|
||||||
|
has-notes: ${{ steps.extract.outputs.has-notes }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Extract changelog for version
|
||||||
|
id: extract
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.detect-version-change.outputs.new-version }}"
|
||||||
|
CHANGELOG_FILE="docs/CHANGELOG.md"
|
||||||
|
|
||||||
|
if [ ! -f "$CHANGELOG_FILE" ]; then
|
||||||
|
echo "Changelog file not found at $CHANGELOG_FILE"
|
||||||
|
echo "has-notes=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "notes=No changelog entries found for version $VERSION" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use the extracted changelog script
|
||||||
|
if NOTES=$(node scripts/extract-changelog.js "$VERSION" "$CHANGELOG_FILE" 2>/dev/null); then
|
||||||
|
echo "has-notes=true" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Use heredoc to properly handle multiline content
|
||||||
|
{
|
||||||
|
echo "notes<<EOF"
|
||||||
|
echo "$NOTES"
|
||||||
|
echo "EOF"
|
||||||
|
} >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
echo "✅ Successfully extracted changelog for version $VERSION"
|
||||||
|
else
|
||||||
|
echo "has-notes=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "notes=No changelog entries found for version $VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "⚠️ Could not extract changelog for version $VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
create-release:
|
||||||
|
name: Create GitHub Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [detect-version-change, extract-changelog]
|
||||||
|
if: needs.detect-version-change.outputs.version-changed == 'true'
|
||||||
|
outputs:
|
||||||
|
release-id: ${{ steps.create.outputs.id }}
|
||||||
|
upload-url: ${{ steps.create.outputs.upload_url }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Create Git Tag
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.detect-version-change.outputs.new-version }}"
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
# Create annotated tag
|
||||||
|
git tag -a "v$VERSION" -m "Release v$VERSION"
|
||||||
|
git push origin "v$VERSION"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
id: create
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.detect-version-change.outputs.new-version }}"
|
||||||
|
IS_PRERELEASE="${{ needs.detect-version-change.outputs.is-prerelease }}"
|
||||||
|
|
||||||
|
# Create release body
|
||||||
|
cat > release_body.md << 'EOF'
|
||||||
|
# Release v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
|
||||||
|
${{ needs.extract-changelog.outputs.release-notes }}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### NPM Package
|
||||||
|
```bash
|
||||||
|
# Install globally
|
||||||
|
npm install -g n8n-mcp
|
||||||
|
|
||||||
|
# Or run directly
|
||||||
|
npx n8n-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
```bash
|
||||||
|
# Standard image
|
||||||
|
docker run -p 3000:3000 ghcr.io/czlonkowski/n8n-mcp:v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
|
||||||
|
# Railway optimized
|
||||||
|
docker run -p 3000:3000 ghcr.io/czlonkowski/n8n-mcp-railway:v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
- [Installation Guide](https://github.com/czlonkowski/n8n-mcp#installation)
|
||||||
|
- [Docker Deployment](https://github.com/czlonkowski/n8n-mcp/blob/main/docs/DOCKER_README.md)
|
||||||
|
- [n8n Integration](https://github.com/czlonkowski/n8n-mcp/blob/main/docs/N8N_DEPLOYMENT.md)
|
||||||
|
- [Complete Changelog](https://github.com/czlonkowski/n8n-mcp/blob/main/docs/CHANGELOG.md)
|
||||||
|
|
||||||
|
🤖 *Generated with [Claude Code](https://claude.ai/code)*
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Create release using gh CLI
|
||||||
|
if [ "$IS_PRERELEASE" = "true" ]; then
|
||||||
|
PRERELEASE_FLAG="--prerelease"
|
||||||
|
else
|
||||||
|
PRERELEASE_FLAG=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
gh release create "v$VERSION" \
|
||||||
|
--title "Release v$VERSION" \
|
||||||
|
--notes-file release_body.md \
|
||||||
|
$PRERELEASE_FLAG
|
||||||
|
|
||||||
|
# Output release info for next jobs
|
||||||
|
RELEASE_ID=$(gh release view "v$VERSION" --json id --jq '.id')
|
||||||
|
echo "id=$RELEASE_ID" >> $GITHUB_OUTPUT
|
||||||
|
echo "upload_url=https://uploads.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID/assets{?name,label}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
build-and-test:
|
||||||
|
name: Build and Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: detect-version-change
|
||||||
|
if: needs.detect-version-change.outputs.version-changed == 'true'
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Rebuild database
|
||||||
|
run: npm run rebuild
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm test
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
|
- name: Run type checking
|
||||||
|
run: npm run typecheck
|
||||||
|
|
||||||
|
publish-npm:
|
||||||
|
name: Publish to NPM
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [detect-version-change, build-and-test, create-release]
|
||||||
|
if: needs.detect-version-change.outputs.version-changed == 'true'
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: 'npm'
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Rebuild database
|
||||||
|
run: npm run rebuild
|
||||||
|
|
||||||
|
- name: Sync runtime version
|
||||||
|
run: npm run sync:runtime-version
|
||||||
|
|
||||||
|
- name: Prepare package for publishing
|
||||||
|
run: |
|
||||||
|
# Create publish directory
|
||||||
|
PUBLISH_DIR="npm-publish-temp"
|
||||||
|
rm -rf $PUBLISH_DIR
|
||||||
|
mkdir -p $PUBLISH_DIR
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
cp -r dist $PUBLISH_DIR/
|
||||||
|
cp -r data $PUBLISH_DIR/
|
||||||
|
cp README.md $PUBLISH_DIR/
|
||||||
|
cp LICENSE $PUBLISH_DIR/
|
||||||
|
cp .env.example $PUBLISH_DIR/
|
||||||
|
|
||||||
|
# Use runtime package.json as base
|
||||||
|
cp package.runtime.json $PUBLISH_DIR/package.json
|
||||||
|
|
||||||
|
cd $PUBLISH_DIR
|
||||||
|
|
||||||
|
# Update package.json with complete metadata
|
||||||
|
node -e "
|
||||||
|
const pkg = require('./package.json');
|
||||||
|
pkg.name = 'n8n-mcp';
|
||||||
|
pkg.description = 'Integration between n8n workflow automation and Model Context Protocol (MCP)';
|
||||||
|
pkg.bin = { 'n8n-mcp': './dist/mcp/index.js' };
|
||||||
|
pkg.repository = { type: 'git', url: 'git+https://github.com/czlonkowski/n8n-mcp.git' };
|
||||||
|
pkg.keywords = ['n8n', 'mcp', 'model-context-protocol', 'ai', 'workflow', 'automation'];
|
||||||
|
pkg.author = 'Romuald Czlonkowski @ www.aiadvisors.pl/en';
|
||||||
|
pkg.license = 'MIT';
|
||||||
|
pkg.bugs = { url: 'https://github.com/czlonkowski/n8n-mcp/issues' };
|
||||||
|
pkg.homepage = 'https://github.com/czlonkowski/n8n-mcp#readme';
|
||||||
|
pkg.files = ['dist/**/*', 'data/nodes.db', '.env.example', 'README.md', 'LICENSE'];
|
||||||
|
delete pkg.private;
|
||||||
|
require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "Package prepared for publishing:"
|
||||||
|
echo "Name: $(node -e "console.log(require('./package.json').name)")"
|
||||||
|
echo "Version: $(node -e "console.log(require('./package.json').version)")"
|
||||||
|
|
||||||
|
- name: Publish to NPM with retry
|
||||||
|
uses: nick-invision/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_minutes: 5
|
||||||
|
max_attempts: 3
|
||||||
|
command: |
|
||||||
|
cd npm-publish-temp
|
||||||
|
npm publish --access public
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
- name: Clean up
|
||||||
|
if: always()
|
||||||
|
run: rm -rf npm-publish-temp
|
||||||
|
|
||||||
|
build-docker:
|
||||||
|
name: Build and Push Docker Images
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [detect-version-change, build-and-test]
|
||||||
|
if: needs.detect-version-change.outputs.version-changed == 'true'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
|
||||||
|
- name: Check disk space
|
||||||
|
run: |
|
||||||
|
echo "Disk usage before Docker build:"
|
||||||
|
df -h
|
||||||
|
|
||||||
|
# Check available space (require at least 2GB)
|
||||||
|
AVAILABLE_GB=$(df / --output=avail --block-size=1G | tail -1)
|
||||||
|
if [ "$AVAILABLE_GB" -lt 2 ]; then
|
||||||
|
echo "❌ Insufficient disk space: ${AVAILABLE_GB}GB available, 2GB required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Sufficient disk space: ${AVAILABLE_GB}GB available"
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for standard image
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}},value=v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
type=semver,pattern={{major}},value=v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push standard Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Extract metadata for Railway image
|
||||||
|
id: meta-railway
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-railway
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}},value=v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
type=semver,pattern={{major}},value=v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
|
- name: Build and push Railway Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile.railway
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta-railway.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-railway.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
update-documentation:
|
||||||
|
name: Update Documentation
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [detect-version-change, create-release, publish-npm, build-docker]
|
||||||
|
if: needs.detect-version-change.outputs.version-changed == 'true' && !failure()
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Update version badges in README
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.detect-version-change.outputs.new-version }}"
|
||||||
|
|
||||||
|
# Update README version badges
|
||||||
|
if [ -f "README.md" ]; then
|
||||||
|
# Update npm version badge
|
||||||
|
sed -i.bak "s|npm/v/n8n-mcp/[^)]*|npm/v/n8n-mcp/$VERSION|g" README.md
|
||||||
|
|
||||||
|
# Update any other version references
|
||||||
|
sed -i.bak "s|version-[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*|version-$VERSION|g" README.md
|
||||||
|
|
||||||
|
# Clean up backup file
|
||||||
|
rm -f README.md.bak
|
||||||
|
|
||||||
|
echo "✅ Updated version badges in README.md to $VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit documentation updates
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
if git diff --quiet; then
|
||||||
|
echo "No documentation changes to commit"
|
||||||
|
else
|
||||||
|
git add README.md
|
||||||
|
git commit -m "docs: update version badges to v${{ needs.detect-version-change.outputs.new-version }}
|
||||||
|
|
||||||
|
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||||
|
|
||||||
|
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||||
|
git push
|
||||||
|
echo "✅ Committed documentation updates"
|
||||||
|
fi
|
||||||
|
|
||||||
|
notify-completion:
|
||||||
|
name: Notify Release Completion
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [detect-version-change, create-release, publish-npm, build-docker, update-documentation]
|
||||||
|
if: always() && needs.detect-version-change.outputs.version-changed == 'true'
|
||||||
|
steps:
|
||||||
|
- name: Create release summary
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.detect-version-change.outputs.new-version }}"
|
||||||
|
RELEASE_URL="https://github.com/${{ github.repository }}/releases/tag/v$VERSION"
|
||||||
|
|
||||||
|
echo "## 🎉 Release v$VERSION Published Successfully!" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### ✅ Completed Tasks:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Check job statuses
|
||||||
|
if [ "${{ needs.create-release.result }}" = "success" ]; then
|
||||||
|
echo "- ✅ GitHub Release created: [$RELEASE_URL]($RELEASE_URL)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "- ❌ GitHub Release creation failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ needs.publish-npm.result }}" = "success" ]; then
|
||||||
|
echo "- ✅ NPM package published: [npmjs.com/package/n8n-mcp](https://www.npmjs.com/package/n8n-mcp)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "- ❌ NPM publishing failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ needs.build-docker.result }}" = "success" ]; then
|
||||||
|
echo "- ✅ Docker images built and pushed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo " - Standard: \`ghcr.io/czlonkowski/n8n-mcp:v$VERSION\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo " - Railway: \`ghcr.io/czlonkowski/n8n-mcp-railway:v$VERSION\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "- ❌ Docker image building failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{ needs.update-documentation.result }}" = "success" ]; then
|
||||||
|
echo "- ✅ Documentation updated" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "- ⚠️ Documentation update skipped or failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### 📦 Installation:" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "# NPM" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "npx n8n-mcp" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "# Docker" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "docker run -p 3000:3000 ghcr.io/czlonkowski/n8n-mcp:v$VERSION" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
echo "🎉 Release automation completed for v$VERSION!"
|
||||||
384
docs/AUTOMATED_RELEASES.md
Normal file
384
docs/AUTOMATED_RELEASES.md
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
# Automated Release Process
|
||||||
|
|
||||||
|
This document describes the automated release system for n8n-mcp, which handles version detection, changelog parsing, and multi-artifact publishing.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The automated release system is triggered when the version in `package.json` is updated and pushed to the main branch. It handles:
|
||||||
|
|
||||||
|
- 🏷️ **GitHub Releases**: Creates releases with changelog content
|
||||||
|
- 📦 **NPM Publishing**: Publishes optimized runtime package
|
||||||
|
- 🐳 **Docker Images**: Builds and pushes multi-platform images
|
||||||
|
- 📚 **Documentation**: Updates version badges automatically
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### For Maintainers
|
||||||
|
|
||||||
|
Use the prepared release script for a guided experience:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run prepare:release
|
||||||
|
```
|
||||||
|
|
||||||
|
This script will:
|
||||||
|
1. Prompt for the new version
|
||||||
|
2. Update `package.json` and `package.runtime.json`
|
||||||
|
3. Update the changelog
|
||||||
|
4. Run tests and build
|
||||||
|
5. Create a git commit
|
||||||
|
6. Optionally push to trigger the release
|
||||||
|
|
||||||
|
### Manual Process
|
||||||
|
|
||||||
|
1. **Update the version**:
|
||||||
|
```bash
|
||||||
|
# Edit package.json version field
|
||||||
|
vim package.json
|
||||||
|
|
||||||
|
# Sync to runtime package
|
||||||
|
npm run sync:runtime-version
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update the changelog**:
|
||||||
|
```bash
|
||||||
|
# Edit docs/CHANGELOG.md
|
||||||
|
vim docs/CHANGELOG.md
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Test and commit**:
|
||||||
|
```bash
|
||||||
|
# Ensure everything works
|
||||||
|
npm test
|
||||||
|
npm run build
|
||||||
|
npm run rebuild
|
||||||
|
|
||||||
|
# Commit changes
|
||||||
|
git add package.json package.runtime.json docs/CHANGELOG.md
|
||||||
|
git commit -m "chore: release vX.Y.Z"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow Details
|
||||||
|
|
||||||
|
### Version Detection
|
||||||
|
|
||||||
|
The workflow monitors pushes to the main branch and detects when `package.json` version changes:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
paths:
|
||||||
|
- 'package.json'
|
||||||
|
- 'package.runtime.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changelog Parsing
|
||||||
|
|
||||||
|
Automatically extracts release notes from `docs/CHANGELOG.md` using the version header format:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [2.10.0] - 2025-08-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New feature descriptions
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Changed feature descriptions
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Bug fix descriptions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release Artifacts
|
||||||
|
|
||||||
|
#### GitHub Release
|
||||||
|
- Created with extracted changelog content
|
||||||
|
- Tagged with `vX.Y.Z` format
|
||||||
|
- Includes installation instructions
|
||||||
|
- Links to documentation
|
||||||
|
|
||||||
|
#### NPM Package
|
||||||
|
- Published as `n8n-mcp` on npmjs.com
|
||||||
|
- Uses runtime-only dependencies (8 packages vs 50+ dev deps)
|
||||||
|
- Optimized for `npx` usage
|
||||||
|
- ~50MB vs 1GB+ with dev dependencies
|
||||||
|
|
||||||
|
#### Docker Images
|
||||||
|
- **Standard**: `ghcr.io/czlonkowski/n8n-mcp:vX.Y.Z`
|
||||||
|
- **Railway**: `ghcr.io/czlonkowski/n8n-mcp-railway:vX.Y.Z`
|
||||||
|
- Multi-platform: linux/amd64, linux/arm64
|
||||||
|
- Semantic version tags: `vX.Y.Z`, `vX.Y`, `vX`, `latest`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Required Secrets
|
||||||
|
|
||||||
|
Set these in GitHub repository settings → Secrets:
|
||||||
|
|
||||||
|
| Secret | Description | Required |
|
||||||
|
|--------|-------------|----------|
|
||||||
|
| `NPM_TOKEN` | NPM authentication token for publishing | ✅ Yes |
|
||||||
|
| `GITHUB_TOKEN` | Automatically provided by GitHub Actions | ✅ Auto |
|
||||||
|
|
||||||
|
### NPM Token Setup
|
||||||
|
|
||||||
|
1. Login to [npmjs.com](https://www.npmjs.com)
|
||||||
|
2. Go to Account Settings → Access Tokens
|
||||||
|
3. Create a new **Automation** token
|
||||||
|
4. Add as `NPM_TOKEN` secret in GitHub
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Release Automation
|
||||||
|
|
||||||
|
Validate the release system without triggering a release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:release-automation
|
||||||
|
```
|
||||||
|
|
||||||
|
This checks:
|
||||||
|
- ✅ File existence and structure
|
||||||
|
- ✅ Version detection logic
|
||||||
|
- ✅ Changelog parsing
|
||||||
|
- ✅ Build process
|
||||||
|
- ✅ NPM package preparation
|
||||||
|
- ✅ Docker configuration
|
||||||
|
- ✅ Workflow syntax
|
||||||
|
- ✅ Environment setup
|
||||||
|
|
||||||
|
### Local Testing
|
||||||
|
|
||||||
|
Test individual components:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test version detection
|
||||||
|
node -e "console.log(require('./package.json').version)"
|
||||||
|
|
||||||
|
# Test changelog parsing
|
||||||
|
node scripts/test-release-automation.js
|
||||||
|
|
||||||
|
# Test npm package preparation
|
||||||
|
npm run prepare:publish
|
||||||
|
|
||||||
|
# Test Docker build
|
||||||
|
docker build -t test-image .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow Jobs
|
||||||
|
|
||||||
|
### 1. Version Detection
|
||||||
|
- Compares current vs previous version in git history
|
||||||
|
- Determines if it's a prerelease (alpha, beta, rc, dev)
|
||||||
|
- Outputs version information for other jobs
|
||||||
|
|
||||||
|
### 2. Changelog Extraction
|
||||||
|
- Parses `docs/CHANGELOG.md` for the current version
|
||||||
|
- Extracts content between version headers
|
||||||
|
- Provides formatted release notes
|
||||||
|
|
||||||
|
### 3. GitHub Release Creation
|
||||||
|
- Creates annotated git tag
|
||||||
|
- Creates GitHub release with changelog content
|
||||||
|
- Handles prerelease flag for alpha/beta versions
|
||||||
|
|
||||||
|
### 4. Build and Test
|
||||||
|
- Installs dependencies
|
||||||
|
- Runs full test suite
|
||||||
|
- Builds TypeScript
|
||||||
|
- Rebuilds node database
|
||||||
|
- Type checking
|
||||||
|
|
||||||
|
### 5. NPM Publishing
|
||||||
|
- Prepares optimized package structure
|
||||||
|
- Uses `package.runtime.json` for dependencies
|
||||||
|
- Publishes to npmjs.com registry
|
||||||
|
- Automatic cleanup
|
||||||
|
|
||||||
|
### 6. Docker Building
|
||||||
|
- Multi-platform builds (amd64, arm64)
|
||||||
|
- Two image variants (standard, railway)
|
||||||
|
- Semantic versioning tags
|
||||||
|
- GitHub Container Registry
|
||||||
|
|
||||||
|
### 7. Documentation Updates
|
||||||
|
- Updates version badges in README
|
||||||
|
- Commits documentation changes
|
||||||
|
- Automatic push back to repository
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### GitHub Actions
|
||||||
|
Monitor releases at: https://github.com/czlonkowski/n8n-mcp/actions
|
||||||
|
|
||||||
|
### Release Status
|
||||||
|
- **GitHub Releases**: https://github.com/czlonkowski/n8n-mcp/releases
|
||||||
|
- **NPM Package**: https://www.npmjs.com/package/n8n-mcp
|
||||||
|
- **Docker Images**: https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
The workflow provides comprehensive summaries:
|
||||||
|
- ✅ Success notifications with links
|
||||||
|
- ❌ Failure notifications with error details
|
||||||
|
- 📊 Artifact information and installation commands
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### NPM Publishing Fails
|
||||||
|
```
|
||||||
|
Error: 401 Unauthorized
|
||||||
|
```
|
||||||
|
**Solution**: Check NPM_TOKEN secret is valid and has publishing permissions.
|
||||||
|
|
||||||
|
#### Docker Build Fails
|
||||||
|
```
|
||||||
|
Error: failed to solve: could not read from registry
|
||||||
|
```
|
||||||
|
**Solution**: Check GitHub Container Registry permissions and GITHUB_TOKEN.
|
||||||
|
|
||||||
|
#### Changelog Parsing Fails
|
||||||
|
```
|
||||||
|
No changelog entries found for version X.Y.Z
|
||||||
|
```
|
||||||
|
**Solution**: Ensure changelog follows the correct format:
|
||||||
|
```markdown
|
||||||
|
## [X.Y.Z] - YYYY-MM-DD
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Version Detection Fails
|
||||||
|
```
|
||||||
|
Version not incremented
|
||||||
|
```
|
||||||
|
**Solution**: Ensure new version is greater than the previous version.
|
||||||
|
|
||||||
|
### Recovery Steps
|
||||||
|
|
||||||
|
#### Failed NPM Publish
|
||||||
|
1. Check if version was already published
|
||||||
|
2. If not, manually publish:
|
||||||
|
```bash
|
||||||
|
npm run prepare:publish
|
||||||
|
cd npm-publish-temp
|
||||||
|
npm publish
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Failed Docker Build
|
||||||
|
1. Build locally to test:
|
||||||
|
```bash
|
||||||
|
docker build -t test-build .
|
||||||
|
```
|
||||||
|
2. Re-trigger workflow or push a fix
|
||||||
|
|
||||||
|
#### Incomplete Release
|
||||||
|
1. Delete the created tag if needed:
|
||||||
|
```bash
|
||||||
|
git tag -d vX.Y.Z
|
||||||
|
git push --delete origin vX.Y.Z
|
||||||
|
```
|
||||||
|
2. Fix issues and push again
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### Secrets Management
|
||||||
|
- NPM_TOKEN has limited scope (publish only)
|
||||||
|
- GITHUB_TOKEN has automatic scoping
|
||||||
|
- No secrets are logged or exposed
|
||||||
|
|
||||||
|
### Package Security
|
||||||
|
- Runtime package excludes development dependencies
|
||||||
|
- No build tools or test frameworks in published package
|
||||||
|
- Minimal attack surface (~50MB vs 1GB+)
|
||||||
|
|
||||||
|
### Docker Security
|
||||||
|
- Multi-stage builds
|
||||||
|
- Non-root user execution
|
||||||
|
- Minimal base images
|
||||||
|
- Security scanning enabled
|
||||||
|
|
||||||
|
## Changelog Format
|
||||||
|
|
||||||
|
The automated system expects changelog entries in [Keep a Changelog](https://keepachangelog.com/) format:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New features for next release
|
||||||
|
|
||||||
|
## [2.10.0] - 2025-08-02
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Automated release system
|
||||||
|
- Multi-platform Docker builds
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improved version detection
|
||||||
|
- Enhanced error handling
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed changelog parsing edge cases
|
||||||
|
- Fixed Docker build optimization
|
||||||
|
|
||||||
|
## [2.9.1] - 2025-08-01
|
||||||
|
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Strategy
|
||||||
|
|
||||||
|
### Semantic Versioning
|
||||||
|
- **MAJOR** (X.0.0): Breaking changes
|
||||||
|
- **MINOR** (X.Y.0): New features, backward compatible
|
||||||
|
- **PATCH** (X.Y.Z): Bug fixes, backward compatible
|
||||||
|
|
||||||
|
### Prerelease Versions
|
||||||
|
- **Alpha**: `X.Y.Z-alpha.N` - Early development
|
||||||
|
- **Beta**: `X.Y.Z-beta.N` - Feature complete, testing
|
||||||
|
- **RC**: `X.Y.Z-rc.N` - Release candidate
|
||||||
|
|
||||||
|
Prerelease versions are automatically detected and marked appropriately.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Before Releasing
|
||||||
|
1. ✅ Run `npm run test:release-automation`
|
||||||
|
2. ✅ Update changelog with meaningful descriptions
|
||||||
|
3. ✅ Test locally with `npm test && npm run build`
|
||||||
|
4. ✅ Review breaking changes
|
||||||
|
5. ✅ Consider impact on users
|
||||||
|
|
||||||
|
### Version Bumping
|
||||||
|
- Use `npm run prepare:release` for guided process
|
||||||
|
- Follow semantic versioning strictly
|
||||||
|
- Document breaking changes clearly
|
||||||
|
- Consider backward compatibility
|
||||||
|
|
||||||
|
### Changelog Writing
|
||||||
|
- Be specific about changes
|
||||||
|
- Include migration notes for breaking changes
|
||||||
|
- Credit contributors
|
||||||
|
- Use consistent formatting
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### For Maintainers
|
||||||
|
1. Use automated tools: `npm run prepare:release`
|
||||||
|
2. Follow semantic versioning
|
||||||
|
3. Update changelog thoroughly
|
||||||
|
4. Test before releasing
|
||||||
|
|
||||||
|
### For Contributors
|
||||||
|
- Breaking changes require MAJOR version bump
|
||||||
|
- New features require MINOR version bump
|
||||||
|
- Bug fixes require PATCH version bump
|
||||||
|
- Update changelog in PR descriptions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🤖 *This automated release system was designed with [Claude Code](https://claude.ai/code)*
|
||||||
84
scripts/extract-changelog.js
Executable file
84
scripts/extract-changelog.js
Executable file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract changelog content for a specific version
|
||||||
|
* Used by GitHub Actions to extract release notes
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function extractChangelog(version, changelogPath) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(changelogPath)) {
|
||||||
|
console.error(`Changelog file not found at ${changelogPath}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = fs.readFileSync(changelogPath, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
// Find the start of this version's section
|
||||||
|
const versionHeaderRegex = new RegExp(`^## \\[${version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`);
|
||||||
|
let startIndex = -1;
|
||||||
|
let endIndex = -1;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (versionHeaderRegex.test(lines[i])) {
|
||||||
|
startIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIndex === -1) {
|
||||||
|
console.error(`No changelog entries found for version ${version}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the end of this version's section (next version or end of file)
|
||||||
|
for (let i = startIndex + 1; i < lines.length; i++) {
|
||||||
|
if (lines[i].startsWith('## [') && !lines[i].includes('Unreleased')) {
|
||||||
|
endIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endIndex === -1) {
|
||||||
|
endIndex = lines.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the section content
|
||||||
|
const sectionLines = lines.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// Remove the version header and any trailing empty lines
|
||||||
|
let contentLines = sectionLines.slice(1);
|
||||||
|
while (contentLines.length > 0 && contentLines[contentLines.length - 1].trim() === '') {
|
||||||
|
contentLines.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentLines.length === 0) {
|
||||||
|
console.error(`No content found for version ${version}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseNotes = contentLines.join('\n').trim();
|
||||||
|
|
||||||
|
// Write to stdout for GitHub Actions
|
||||||
|
console.log(releaseNotes);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error extracting changelog: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
const version = process.argv[2];
|
||||||
|
const changelogPath = process.argv[3];
|
||||||
|
|
||||||
|
if (!version || !changelogPath) {
|
||||||
|
console.error('Usage: extract-changelog.js <version> <changelog-path>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
extractChangelog(version, changelogPath);
|
||||||
400
scripts/prepare-release.js
Executable file
400
scripts/prepare-release.js
Executable file
@@ -0,0 +1,400 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-release preparation script
|
||||||
|
* Validates and prepares everything needed for a successful release
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync, spawnSync } = require('child_process');
|
||||||
|
const readline = require('readline');
|
||||||
|
|
||||||
|
// Color codes
|
||||||
|
const colors = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
magenta: '\x1b[35m',
|
||||||
|
cyan: '\x1b[36m'
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(message, color = 'reset') {
|
||||||
|
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(message) {
|
||||||
|
log(`✅ ${message}`, 'green');
|
||||||
|
}
|
||||||
|
|
||||||
|
function warning(message) {
|
||||||
|
log(`⚠️ ${message}`, 'yellow');
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message) {
|
||||||
|
log(`❌ ${message}`, 'red');
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(message) {
|
||||||
|
log(`ℹ️ ${message}`, 'blue');
|
||||||
|
}
|
||||||
|
|
||||||
|
function header(title) {
|
||||||
|
log(`\n${'='.repeat(60)}`, 'cyan');
|
||||||
|
log(`🚀 ${title}`, 'cyan');
|
||||||
|
log(`${'='.repeat(60)}`, 'cyan');
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReleasePreparation {
|
||||||
|
constructor() {
|
||||||
|
this.rootDir = path.resolve(__dirname, '..');
|
||||||
|
this.rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async askQuestion(question) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.rl.question(question, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current version and ask for new version
|
||||||
|
*/
|
||||||
|
async getVersionInfo() {
|
||||||
|
const packageJson = require(path.join(this.rootDir, 'package.json'));
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
|
||||||
|
log(`\nCurrent version: ${currentVersion}`, 'blue');
|
||||||
|
|
||||||
|
const newVersion = await this.askQuestion('\nEnter new version (e.g., 2.10.0): ');
|
||||||
|
|
||||||
|
if (!newVersion || !this.isValidSemver(newVersion)) {
|
||||||
|
error('Invalid semantic version format');
|
||||||
|
throw new Error('Invalid version');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.compareVersions(newVersion, currentVersion) <= 0) {
|
||||||
|
error('New version must be greater than current version');
|
||||||
|
throw new Error('Version not incremented');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { currentVersion, newVersion };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate semantic version format (strict semver compliance)
|
||||||
|
*/
|
||||||
|
isValidSemver(version) {
|
||||||
|
// Strict semantic versioning regex
|
||||||
|
const semverRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
||||||
|
return semverRegex.test(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two semantic versions
|
||||||
|
*/
|
||||||
|
compareVersions(v1, v2) {
|
||||||
|
const parseVersion = (v) => v.split('-')[0].split('.').map(Number);
|
||||||
|
const [v1Parts, v2Parts] = [parseVersion(v1), parseVersion(v2)];
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (v1Parts[i] > v2Parts[i]) return 1;
|
||||||
|
if (v1Parts[i] < v2Parts[i]) return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update version in package files
|
||||||
|
*/
|
||||||
|
updateVersions(newVersion) {
|
||||||
|
log('\n📝 Updating version in package files...', 'blue');
|
||||||
|
|
||||||
|
// Update package.json
|
||||||
|
const packageJsonPath = path.join(this.rootDir, 'package.json');
|
||||||
|
const packageJson = require(packageJsonPath);
|
||||||
|
packageJson.version = newVersion;
|
||||||
|
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||||
|
success('Updated package.json');
|
||||||
|
|
||||||
|
// Sync to runtime package
|
||||||
|
try {
|
||||||
|
execSync('npm run sync:runtime-version', { cwd: this.rootDir, stdio: 'pipe' });
|
||||||
|
success('Synced package.runtime.json');
|
||||||
|
} catch (err) {
|
||||||
|
warning('Could not sync runtime version automatically');
|
||||||
|
|
||||||
|
// Manual sync
|
||||||
|
const runtimeJsonPath = path.join(this.rootDir, 'package.runtime.json');
|
||||||
|
if (fs.existsSync(runtimeJsonPath)) {
|
||||||
|
const runtimeJson = require(runtimeJsonPath);
|
||||||
|
runtimeJson.version = newVersion;
|
||||||
|
fs.writeFileSync(runtimeJsonPath, JSON.stringify(runtimeJson, null, 2) + '\n');
|
||||||
|
success('Manually synced package.runtime.json');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update changelog
|
||||||
|
*/
|
||||||
|
async updateChangelog(newVersion) {
|
||||||
|
const changelogPath = path.join(this.rootDir, 'docs/CHANGELOG.md');
|
||||||
|
|
||||||
|
if (!fs.existsSync(changelogPath)) {
|
||||||
|
warning('Changelog file not found, skipping update');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('\n📋 Updating changelog...', 'blue');
|
||||||
|
|
||||||
|
const content = fs.readFileSync(changelogPath, 'utf8');
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Check if version already exists in changelog
|
||||||
|
const versionRegex = new RegExp(`^## \\[${newVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`, 'm');
|
||||||
|
if (versionRegex.test(content)) {
|
||||||
|
info(`Version ${newVersion} already exists in changelog`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the Unreleased section
|
||||||
|
const unreleasedMatch = content.match(/^## \[Unreleased\]\s*\n([\s\S]*?)(?=\n## \[|$)/m);
|
||||||
|
|
||||||
|
if (unreleasedMatch) {
|
||||||
|
const unreleasedContent = unreleasedMatch[1].trim();
|
||||||
|
|
||||||
|
if (unreleasedContent) {
|
||||||
|
log('\nFound content in Unreleased section:', 'blue');
|
||||||
|
log(unreleasedContent.substring(0, 200) + '...', 'yellow');
|
||||||
|
|
||||||
|
const moveContent = await this.askQuestion('\nMove this content to the new version? (y/n): ');
|
||||||
|
|
||||||
|
if (moveContent.toLowerCase() === 'y') {
|
||||||
|
// Move unreleased content to new version
|
||||||
|
const newVersionSection = `## [${newVersion}] - ${today}\n\n${unreleasedContent}\n\n`;
|
||||||
|
const updatedContent = content.replace(
|
||||||
|
/^## \[Unreleased\]\s*\n[\s\S]*?(?=\n## \[)/m,
|
||||||
|
`## [Unreleased]\n\n${newVersionSection}## [`
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(changelogPath, updatedContent);
|
||||||
|
success(`Moved unreleased content to version ${newVersion}`);
|
||||||
|
} else {
|
||||||
|
// Just add empty version section
|
||||||
|
const newVersionSection = `## [${newVersion}] - ${today}\n\n### Added\n- \n\n### Changed\n- \n\n### Fixed\n- \n\n`;
|
||||||
|
const updatedContent = content.replace(
|
||||||
|
/^## \[Unreleased\]\s*\n/m,
|
||||||
|
`## [Unreleased]\n\n${newVersionSection}`
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(changelogPath, updatedContent);
|
||||||
|
warning(`Added empty version section for ${newVersion} - please fill in the changes`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add empty version section
|
||||||
|
const newVersionSection = `## [${newVersion}] - ${today}\n\n### Added\n- \n\n### Changed\n- \n\n### Fixed\n- \n\n`;
|
||||||
|
const updatedContent = content.replace(
|
||||||
|
/^## \[Unreleased\]\s*\n/m,
|
||||||
|
`## [Unreleased]\n\n${newVersionSection}`
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(changelogPath, updatedContent);
|
||||||
|
warning(`Added empty version section for ${newVersion} - please fill in the changes`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warning('Could not find Unreleased section in changelog');
|
||||||
|
}
|
||||||
|
|
||||||
|
info('Please review and edit the changelog before committing');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run tests and build
|
||||||
|
*/
|
||||||
|
async runChecks() {
|
||||||
|
log('\n🧪 Running pre-release checks...', 'blue');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Run tests
|
||||||
|
log('Running tests...', 'blue');
|
||||||
|
execSync('npm test', { cwd: this.rootDir, stdio: 'inherit' });
|
||||||
|
success('All tests passed');
|
||||||
|
|
||||||
|
// Run build
|
||||||
|
log('Building project...', 'blue');
|
||||||
|
execSync('npm run build', { cwd: this.rootDir, stdio: 'inherit' });
|
||||||
|
success('Build completed');
|
||||||
|
|
||||||
|
// Rebuild database
|
||||||
|
log('Rebuilding database...', 'blue');
|
||||||
|
execSync('npm run rebuild', { cwd: this.rootDir, stdio: 'inherit' });
|
||||||
|
success('Database rebuilt');
|
||||||
|
|
||||||
|
// Run type checking
|
||||||
|
log('Type checking...', 'blue');
|
||||||
|
execSync('npm run typecheck', { cwd: this.rootDir, stdio: 'inherit' });
|
||||||
|
success('Type checking passed');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
error('Pre-release checks failed');
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create git commit
|
||||||
|
*/
|
||||||
|
async createCommit(newVersion) {
|
||||||
|
log('\n📝 Creating git commit...', 'blue');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check git status
|
||||||
|
const status = execSync('git status --porcelain', {
|
||||||
|
cwd: this.rootDir,
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!status.trim()) {
|
||||||
|
info('No changes to commit');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show what will be committed
|
||||||
|
log('\nFiles to be committed:', 'blue');
|
||||||
|
execSync('git diff --name-only', { cwd: this.rootDir, stdio: 'inherit' });
|
||||||
|
|
||||||
|
const commit = await this.askQuestion('\nCreate commit for release? (y/n): ');
|
||||||
|
|
||||||
|
if (commit.toLowerCase() === 'y') {
|
||||||
|
// Add files
|
||||||
|
execSync('git add package.json package.runtime.json docs/CHANGELOG.md', {
|
||||||
|
cwd: this.rootDir,
|
||||||
|
stdio: 'pipe'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create commit
|
||||||
|
const commitMessage = `chore: release v${newVersion}
|
||||||
|
|
||||||
|
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||||
|
|
||||||
|
Co-Authored-By: Claude <noreply@anthropic.com>`;
|
||||||
|
|
||||||
|
const result = spawnSync('git', ['commit', '-m', commitMessage], {
|
||||||
|
cwd: this.rootDir,
|
||||||
|
stdio: 'pipe',
|
||||||
|
encoding: 'utf8'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error || result.status !== 0) {
|
||||||
|
throw new Error(`Git commit failed: ${result.stderr || result.error?.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
success(`Created commit for v${newVersion}`);
|
||||||
|
|
||||||
|
const push = await this.askQuestion('\nPush to trigger release workflow? (y/n): ');
|
||||||
|
|
||||||
|
if (push.toLowerCase() === 'y') {
|
||||||
|
// Add confirmation for destructive operation
|
||||||
|
warning('\n⚠️ DESTRUCTIVE OPERATION WARNING ⚠️');
|
||||||
|
warning('This will trigger a PUBLIC RELEASE that cannot be undone!');
|
||||||
|
warning('The following will happen automatically:');
|
||||||
|
warning('• Create GitHub release with tag');
|
||||||
|
warning('• Publish package to NPM registry');
|
||||||
|
warning('• Build and push Docker images');
|
||||||
|
warning('• Update documentation');
|
||||||
|
|
||||||
|
const confirmation = await this.askQuestion('\nType "RELEASE" (all caps) to confirm: ');
|
||||||
|
|
||||||
|
if (confirmation === 'RELEASE') {
|
||||||
|
execSync('git push', { cwd: this.rootDir, stdio: 'inherit' });
|
||||||
|
success('Pushed to remote repository');
|
||||||
|
log('\n🎉 Release workflow will be triggered automatically!', 'green');
|
||||||
|
log('Monitor progress at: https://github.com/czlonkowski/n8n-mcp/actions', 'blue');
|
||||||
|
} else {
|
||||||
|
warning('Release cancelled. Commit created but not pushed.');
|
||||||
|
info('You can push manually later to trigger the release.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info('Commit created but not pushed. Push manually to trigger release.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
error(`Git operations failed: ${err.message}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display final instructions
|
||||||
|
*/
|
||||||
|
displayInstructions(newVersion) {
|
||||||
|
header('Release Preparation Complete');
|
||||||
|
|
||||||
|
log('📋 What happens next:', 'blue');
|
||||||
|
log(`1. The GitHub Actions workflow will detect the version change to v${newVersion}`, 'green');
|
||||||
|
log('2. It will automatically:', 'green');
|
||||||
|
log(' • Create a GitHub release with changelog content', 'green');
|
||||||
|
log(' • Publish the npm package', 'green');
|
||||||
|
log(' • Build and push Docker images', 'green');
|
||||||
|
log(' • Update documentation badges', 'green');
|
||||||
|
log('\n🔍 Monitor the release at:', 'blue');
|
||||||
|
log(' • GitHub Actions: https://github.com/czlonkowski/n8n-mcp/actions', 'blue');
|
||||||
|
log(' • NPM Package: https://www.npmjs.com/package/n8n-mcp', 'blue');
|
||||||
|
log(' • Docker Images: https://github.com/czlonkowski/n8n-mcp/pkgs/container/n8n-mcp', 'blue');
|
||||||
|
|
||||||
|
log('\n✅ Release preparation completed successfully!', 'green');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main execution flow
|
||||||
|
*/
|
||||||
|
async run() {
|
||||||
|
try {
|
||||||
|
header('n8n-MCP Release Preparation');
|
||||||
|
|
||||||
|
// Get version information
|
||||||
|
const { currentVersion, newVersion } = await this.getVersionInfo();
|
||||||
|
|
||||||
|
log(`\n🔄 Preparing release: ${currentVersion} → ${newVersion}`, 'magenta');
|
||||||
|
|
||||||
|
// Update versions
|
||||||
|
this.updateVersions(newVersion);
|
||||||
|
|
||||||
|
// Update changelog
|
||||||
|
await this.updateChangelog(newVersion);
|
||||||
|
|
||||||
|
// Run pre-release checks
|
||||||
|
await this.runChecks();
|
||||||
|
|
||||||
|
// Create git commit
|
||||||
|
await this.createCommit(newVersion);
|
||||||
|
|
||||||
|
// Display final instructions
|
||||||
|
this.displayInstructions(newVersion);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
error(`Release preparation failed: ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
this.rl.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
if (require.main === module) {
|
||||||
|
const preparation = new ReleasePreparation();
|
||||||
|
preparation.run().catch(err => {
|
||||||
|
console.error('Release preparation failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ReleasePreparation;
|
||||||
560
scripts/test-release-automation.js
Executable file
560
scripts/test-release-automation.js
Executable file
@@ -0,0 +1,560 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test script for release automation
|
||||||
|
* Validates the release workflow components locally
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
// Color codes for output
|
||||||
|
const colors = {
|
||||||
|
reset: '\x1b[0m',
|
||||||
|
red: '\x1b[31m',
|
||||||
|
green: '\x1b[32m',
|
||||||
|
yellow: '\x1b[33m',
|
||||||
|
blue: '\x1b[34m',
|
||||||
|
magenta: '\x1b[35m',
|
||||||
|
cyan: '\x1b[36m'
|
||||||
|
};
|
||||||
|
|
||||||
|
function log(message, color = 'reset') {
|
||||||
|
console.log(`${colors[color]}${message}${colors.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function header(title) {
|
||||||
|
log(`\n${'='.repeat(60)}`, 'cyan');
|
||||||
|
log(`🧪 ${title}`, 'cyan');
|
||||||
|
log(`${'='.repeat(60)}`, 'cyan');
|
||||||
|
}
|
||||||
|
|
||||||
|
function section(title) {
|
||||||
|
log(`\n📋 ${title}`, 'blue');
|
||||||
|
log(`${'-'.repeat(40)}`, 'blue');
|
||||||
|
}
|
||||||
|
|
||||||
|
function success(message) {
|
||||||
|
log(`✅ ${message}`, 'green');
|
||||||
|
}
|
||||||
|
|
||||||
|
function warning(message) {
|
||||||
|
log(`⚠️ ${message}`, 'yellow');
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message) {
|
||||||
|
log(`❌ ${message}`, 'red');
|
||||||
|
}
|
||||||
|
|
||||||
|
function info(message) {
|
||||||
|
log(`ℹ️ ${message}`, 'blue');
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReleaseAutomationTester {
|
||||||
|
constructor() {
|
||||||
|
this.rootDir = path.resolve(__dirname, '..');
|
||||||
|
this.errors = [];
|
||||||
|
this.warnings = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if required files exist
|
||||||
|
*/
|
||||||
|
testFileExistence() {
|
||||||
|
section('Testing File Existence');
|
||||||
|
|
||||||
|
const requiredFiles = [
|
||||||
|
'package.json',
|
||||||
|
'package.runtime.json',
|
||||||
|
'docs/CHANGELOG.md',
|
||||||
|
'.github/workflows/release.yml',
|
||||||
|
'scripts/sync-runtime-version.js',
|
||||||
|
'scripts/publish-npm.sh'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of requiredFiles) {
|
||||||
|
const filePath = path.join(this.rootDir, file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
success(`Found: ${file}`);
|
||||||
|
} else {
|
||||||
|
error(`Missing: ${file}`);
|
||||||
|
this.errors.push(`Missing required file: ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test version detection logic
|
||||||
|
*/
|
||||||
|
testVersionDetection() {
|
||||||
|
section('Testing Version Detection');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageJson = require(path.join(this.rootDir, 'package.json'));
|
||||||
|
const runtimeJson = require(path.join(this.rootDir, 'package.runtime.json'));
|
||||||
|
|
||||||
|
success(`Package.json version: ${packageJson.version}`);
|
||||||
|
success(`Runtime package version: ${runtimeJson.version}`);
|
||||||
|
|
||||||
|
if (packageJson.version === runtimeJson.version) {
|
||||||
|
success('Version sync: Both versions match');
|
||||||
|
} else {
|
||||||
|
warning('Version sync: Versions do not match - run sync:runtime-version');
|
||||||
|
this.warnings.push('Package versions are not synchronized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test semantic version format
|
||||||
|
const semverRegex = /^\d+\.\d+\.\d+(?:-[\w\.-]+)?(?:\+[\w\.-]+)?$/;
|
||||||
|
if (semverRegex.test(packageJson.version)) {
|
||||||
|
success(`Version format: Valid semantic version (${packageJson.version})`);
|
||||||
|
} else {
|
||||||
|
error(`Version format: Invalid semantic version (${packageJson.version})`);
|
||||||
|
this.errors.push('Invalid semantic version format');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
error(`Version detection failed: ${err.message}`);
|
||||||
|
this.errors.push(`Version detection error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test changelog parsing
|
||||||
|
*/
|
||||||
|
testChangelogParsing() {
|
||||||
|
section('Testing Changelog Parsing');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const changelogPath = path.join(this.rootDir, 'docs/CHANGELOG.md');
|
||||||
|
|
||||||
|
if (!fs.existsSync(changelogPath)) {
|
||||||
|
error('Changelog file not found');
|
||||||
|
this.errors.push('Missing changelog file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changelogContent = fs.readFileSync(changelogPath, 'utf8');
|
||||||
|
const packageJson = require(path.join(this.rootDir, 'package.json'));
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
|
||||||
|
// Check if current version exists in changelog
|
||||||
|
const versionRegex = new RegExp(`^## \\[${currentVersion.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`, 'm');
|
||||||
|
|
||||||
|
if (versionRegex.test(changelogContent)) {
|
||||||
|
success(`Changelog entry found for version ${currentVersion}`);
|
||||||
|
|
||||||
|
// Test extraction logic (simplified version of the GitHub Actions script)
|
||||||
|
const lines = changelogContent.split('\n');
|
||||||
|
let startIndex = -1;
|
||||||
|
let endIndex = -1;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
if (versionRegex.test(lines[i])) {
|
||||||
|
startIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIndex !== -1) {
|
||||||
|
// Find the end of this version's section
|
||||||
|
for (let i = startIndex + 1; i < lines.length; i++) {
|
||||||
|
if (lines[i].startsWith('## [') && !lines[i].includes('Unreleased')) {
|
||||||
|
endIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endIndex === -1) {
|
||||||
|
endIndex = lines.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionLines = lines.slice(startIndex + 1, endIndex);
|
||||||
|
const contentLines = sectionLines.filter(line => line.trim() !== '');
|
||||||
|
|
||||||
|
if (contentLines.length > 0) {
|
||||||
|
success(`Changelog content extracted: ${contentLines.length} lines`);
|
||||||
|
info(`Preview: ${contentLines[0].substring(0, 100)}...`);
|
||||||
|
} else {
|
||||||
|
warning('Changelog section appears to be empty');
|
||||||
|
this.warnings.push(`Empty changelog section for version ${currentVersion}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
warning(`No changelog entry found for current version ${currentVersion}`);
|
||||||
|
this.warnings.push(`Missing changelog entry for version ${currentVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check changelog format
|
||||||
|
if (changelogContent.includes('## [Unreleased]')) {
|
||||||
|
success('Changelog format: Contains Unreleased section');
|
||||||
|
} else {
|
||||||
|
warning('Changelog format: Missing Unreleased section');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changelogContent.includes('Keep a Changelog')) {
|
||||||
|
success('Changelog format: Follows Keep a Changelog format');
|
||||||
|
} else {
|
||||||
|
warning('Changelog format: Does not reference Keep a Changelog');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
error(`Changelog parsing failed: ${err.message}`);
|
||||||
|
this.errors.push(`Changelog parsing error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test build process
|
||||||
|
*/
|
||||||
|
testBuildProcess() {
|
||||||
|
section('Testing Build Process');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if dist directory exists
|
||||||
|
const distPath = path.join(this.rootDir, 'dist');
|
||||||
|
if (fs.existsSync(distPath)) {
|
||||||
|
success('Build output: dist directory exists');
|
||||||
|
|
||||||
|
// Check for key build files
|
||||||
|
const keyFiles = [
|
||||||
|
'dist/index.js',
|
||||||
|
'dist/mcp/index.js',
|
||||||
|
'dist/mcp/server.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of keyFiles) {
|
||||||
|
const filePath = path.join(this.rootDir, file);
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
success(`Build file: ${file} exists`);
|
||||||
|
} else {
|
||||||
|
warning(`Build file: ${file} missing - run 'npm run build'`);
|
||||||
|
this.warnings.push(`Missing build file: ${file}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
warning('Build output: dist directory missing - run "npm run build"');
|
||||||
|
this.warnings.push('Missing build output');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check database
|
||||||
|
const dbPath = path.join(this.rootDir, 'data/nodes.db');
|
||||||
|
if (fs.existsSync(dbPath)) {
|
||||||
|
const stats = fs.statSync(dbPath);
|
||||||
|
success(`Database: nodes.db exists (${Math.round(stats.size / 1024 / 1024)}MB)`);
|
||||||
|
} else {
|
||||||
|
warning('Database: nodes.db missing - run "npm run rebuild"');
|
||||||
|
this.warnings.push('Missing database file');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
error(`Build process test failed: ${err.message}`);
|
||||||
|
this.errors.push(`Build process error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test npm publish preparation
|
||||||
|
*/
|
||||||
|
testNpmPublishPrep() {
|
||||||
|
section('Testing NPM Publish Preparation');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageJson = require(path.join(this.rootDir, 'package.json'));
|
||||||
|
const runtimeJson = require(path.join(this.rootDir, 'package.runtime.json'));
|
||||||
|
|
||||||
|
// Check package.json fields
|
||||||
|
const requiredFields = ['name', 'version', 'description', 'main', 'bin'];
|
||||||
|
for (const field of requiredFields) {
|
||||||
|
if (packageJson[field]) {
|
||||||
|
success(`Package field: ${field} is present`);
|
||||||
|
} else {
|
||||||
|
error(`Package field: ${field} is missing`);
|
||||||
|
this.errors.push(`Missing package.json field: ${field}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check runtime dependencies
|
||||||
|
if (runtimeJson.dependencies) {
|
||||||
|
const depCount = Object.keys(runtimeJson.dependencies).length;
|
||||||
|
success(`Runtime dependencies: ${depCount} packages`);
|
||||||
|
|
||||||
|
// List key dependencies
|
||||||
|
const keyDeps = ['@modelcontextprotocol/sdk', 'express', 'sql.js'];
|
||||||
|
for (const dep of keyDeps) {
|
||||||
|
if (runtimeJson.dependencies[dep]) {
|
||||||
|
success(`Key dependency: ${dep} (${runtimeJson.dependencies[dep]})`);
|
||||||
|
} else {
|
||||||
|
warning(`Key dependency: ${dep} is missing`);
|
||||||
|
this.warnings.push(`Missing key dependency: ${dep}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
error('Runtime package has no dependencies');
|
||||||
|
this.errors.push('Missing runtime dependencies');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check files array
|
||||||
|
if (packageJson.files && Array.isArray(packageJson.files)) {
|
||||||
|
success(`Package files: ${packageJson.files.length} patterns specified`);
|
||||||
|
info(`Files: ${packageJson.files.join(', ')}`);
|
||||||
|
} else {
|
||||||
|
warning('Package files: No files array specified');
|
||||||
|
this.warnings.push('No files array in package.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
error(`NPM publish prep test failed: ${err.message}`);
|
||||||
|
this.errors.push(`NPM publish prep error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test Docker configuration
|
||||||
|
*/
|
||||||
|
testDockerConfig() {
|
||||||
|
section('Testing Docker Configuration');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dockerfiles = ['Dockerfile', 'Dockerfile.railway'];
|
||||||
|
|
||||||
|
for (const dockerfile of dockerfiles) {
|
||||||
|
const dockerfilePath = path.join(this.rootDir, dockerfile);
|
||||||
|
if (fs.existsSync(dockerfilePath)) {
|
||||||
|
success(`Dockerfile: ${dockerfile} exists`);
|
||||||
|
|
||||||
|
const content = fs.readFileSync(dockerfilePath, 'utf8');
|
||||||
|
|
||||||
|
// Check for key instructions
|
||||||
|
if (content.includes('FROM node:')) {
|
||||||
|
success(`${dockerfile}: Uses Node.js base image`);
|
||||||
|
} else {
|
||||||
|
warning(`${dockerfile}: Does not use standard Node.js base image`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.includes('COPY dist')) {
|
||||||
|
success(`${dockerfile}: Copies build output`);
|
||||||
|
} else {
|
||||||
|
warning(`${dockerfile}: May not copy build output correctly`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
warning(`Dockerfile: ${dockerfile} not found`);
|
||||||
|
this.warnings.push(`Missing Dockerfile: ${dockerfile}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check docker-compose files
|
||||||
|
const composeFiles = ['docker-compose.yml', 'docker-compose.n8n.yml'];
|
||||||
|
for (const composeFile of composeFiles) {
|
||||||
|
const composePath = path.join(this.rootDir, composeFile);
|
||||||
|
if (fs.existsSync(composePath)) {
|
||||||
|
success(`Docker Compose: ${composeFile} exists`);
|
||||||
|
} else {
|
||||||
|
info(`Docker Compose: ${composeFile} not found (optional)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
error(`Docker config test failed: ${err.message}`);
|
||||||
|
this.errors.push(`Docker config error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test workflow file syntax
|
||||||
|
*/
|
||||||
|
testWorkflowSyntax() {
|
||||||
|
section('Testing Workflow Syntax');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const workflowPath = path.join(this.rootDir, '.github/workflows/release.yml');
|
||||||
|
|
||||||
|
if (!fs.existsSync(workflowPath)) {
|
||||||
|
error('Release workflow file not found');
|
||||||
|
this.errors.push('Missing release workflow file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowContent = fs.readFileSync(workflowPath, 'utf8');
|
||||||
|
|
||||||
|
// Basic YAML structure checks
|
||||||
|
if (workflowContent.includes('name: Automated Release')) {
|
||||||
|
success('Workflow: Has correct name');
|
||||||
|
} else {
|
||||||
|
warning('Workflow: Name may be incorrect');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflowContent.includes('on:') && workflowContent.includes('push:')) {
|
||||||
|
success('Workflow: Has push trigger');
|
||||||
|
} else {
|
||||||
|
error('Workflow: Missing push trigger');
|
||||||
|
this.errors.push('Workflow missing push trigger');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflowContent.includes('branches: [main]')) {
|
||||||
|
success('Workflow: Configured for main branch');
|
||||||
|
} else {
|
||||||
|
warning('Workflow: May not be configured for main branch');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for required jobs
|
||||||
|
const requiredJobs = [
|
||||||
|
'detect-version-change',
|
||||||
|
'extract-changelog',
|
||||||
|
'create-release',
|
||||||
|
'publish-npm',
|
||||||
|
'build-docker'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const job of requiredJobs) {
|
||||||
|
if (workflowContent.includes(`${job}:`)) {
|
||||||
|
success(`Workflow job: ${job} defined`);
|
||||||
|
} else {
|
||||||
|
error(`Workflow job: ${job} missing`);
|
||||||
|
this.errors.push(`Missing workflow job: ${job}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for secrets usage
|
||||||
|
if (workflowContent.includes('${{ secrets.NPM_TOKEN }}')) {
|
||||||
|
success('Workflow: NPM_TOKEN secret configured');
|
||||||
|
} else {
|
||||||
|
warning('Workflow: NPM_TOKEN secret may be missing');
|
||||||
|
this.warnings.push('NPM_TOKEN secret may need to be configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflowContent.includes('${{ secrets.GITHUB_TOKEN }}')) {
|
||||||
|
success('Workflow: GITHUB_TOKEN secret configured');
|
||||||
|
} else {
|
||||||
|
warning('Workflow: GITHUB_TOKEN secret may be missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
error(`Workflow syntax test failed: ${err.message}`);
|
||||||
|
this.errors.push(`Workflow syntax error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test environment and dependencies
|
||||||
|
*/
|
||||||
|
testEnvironment() {
|
||||||
|
section('Testing Environment');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check Node.js version
|
||||||
|
const nodeVersion = process.version;
|
||||||
|
success(`Node.js version: ${nodeVersion}`);
|
||||||
|
|
||||||
|
// Check if npm is available
|
||||||
|
try {
|
||||||
|
const npmVersion = execSync('npm --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
||||||
|
success(`NPM version: ${npmVersion}`);
|
||||||
|
} catch (err) {
|
||||||
|
error('NPM not available');
|
||||||
|
this.errors.push('NPM not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if git is available
|
||||||
|
try {
|
||||||
|
const gitVersion = execSync('git --version', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
||||||
|
success(`Git available: ${gitVersion}`);
|
||||||
|
} catch (err) {
|
||||||
|
error('Git not available');
|
||||||
|
this.errors.push('Git not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're in a git repository
|
||||||
|
try {
|
||||||
|
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
|
||||||
|
success('Git repository: Detected');
|
||||||
|
|
||||||
|
// Check current branch
|
||||||
|
try {
|
||||||
|
const branch = execSync('git branch --show-current', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
||||||
|
info(`Current branch: ${branch}`);
|
||||||
|
} catch (err) {
|
||||||
|
info('Could not determine current branch');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
warning('Not in a git repository');
|
||||||
|
this.warnings.push('Not in a git repository');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
error(`Environment test failed: ${err.message}`);
|
||||||
|
this.errors.push(`Environment error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run all tests
|
||||||
|
*/
|
||||||
|
async runAllTests() {
|
||||||
|
header('Release Automation Test Suite');
|
||||||
|
|
||||||
|
info('Testing release automation components...');
|
||||||
|
|
||||||
|
this.testFileExistence();
|
||||||
|
this.testVersionDetection();
|
||||||
|
this.testChangelogParsing();
|
||||||
|
this.testBuildProcess();
|
||||||
|
this.testNpmPublishPrep();
|
||||||
|
this.testDockerConfig();
|
||||||
|
this.testWorkflowSyntax();
|
||||||
|
this.testEnvironment();
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
header('Test Summary');
|
||||||
|
|
||||||
|
if (this.errors.length === 0 && this.warnings.length === 0) {
|
||||||
|
log('🎉 All tests passed! Release automation is ready.', 'green');
|
||||||
|
} else {
|
||||||
|
if (this.errors.length > 0) {
|
||||||
|
log(`\n❌ ${this.errors.length} Error(s):`, 'red');
|
||||||
|
this.errors.forEach(err => log(` • ${err}`, 'red'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.warnings.length > 0) {
|
||||||
|
log(`\n⚠️ ${this.warnings.length} Warning(s):`, 'yellow');
|
||||||
|
this.warnings.forEach(warn => log(` • ${warn}`, 'yellow'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.errors.length > 0) {
|
||||||
|
log('\n🔧 Please fix the errors before running the release workflow.', 'red');
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
log('\n✅ No critical errors found. Warnings should be reviewed but won\'t prevent releases.', 'yellow');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next steps
|
||||||
|
log('\n📋 Next Steps:', 'cyan');
|
||||||
|
log('1. Ensure all secrets are configured in GitHub repository settings:', 'cyan');
|
||||||
|
log(' • NPM_TOKEN (required for npm publishing)', 'cyan');
|
||||||
|
log(' • GITHUB_TOKEN (automatically available)', 'cyan');
|
||||||
|
log('\n2. To trigger a release:', 'cyan');
|
||||||
|
log(' • Update version in package.json', 'cyan');
|
||||||
|
log(' • Update changelog in docs/CHANGELOG.md', 'cyan');
|
||||||
|
log(' • Commit and push to main branch', 'cyan');
|
||||||
|
log('\n3. Monitor the release workflow in GitHub Actions', 'cyan');
|
||||||
|
|
||||||
|
return this.errors.length === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the tests
|
||||||
|
if (require.main === module) {
|
||||||
|
const tester = new ReleaseAutomationTester();
|
||||||
|
tester.runAllTests().catch(err => {
|
||||||
|
console.error('Test suite failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ReleaseAutomationTester;
|
||||||
Reference in New Issue
Block a user