Skip to main content
Glama
release.yml19 kB
name: Release Automation # Note: This workflow uses dynamic outputs from github-script actions. # Step outputs (should_release, release_type, changelog, version) are set at runtime # and may not be statically analyzable by linters - this is expected behavior. on: # push: # branches: [ main ] # Disabled to prevent automatic releases on protected branch workflow_dispatch: inputs: release_type: description: 'Type of release' required: true default: 'auto' type: choice options: - auto - patch - minor - major - prerelease dry_run: description: 'Dry run (no actual release)' required: false default: false type: boolean create_version_pr: description: 'Create PR to bump package.json version' required: false default: true type: boolean permissions: contents: read jobs: check-changes: name: Check for Release-worthy Changes runs-on: ubuntu-latest permissions: contents: read # Required to read git history and commits outputs: # These outputs are dynamically set by the github-script action below should_release: ${{ steps.check.outputs.should_release }} # yamllint disable-line rule:line-length release_type: ${{ steps.check.outputs.release_type }} # yamllint disable-line rule:line-length steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Check conventional commits id: check uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with: script: | const { execSync } = require('child_process'); // Get commits since last release let commits = []; try { const output = execSync('git log $(git describe --tags --abbrev=0)..HEAD --oneline --no-merges', { encoding: 'utf8' }); commits = output.trim().split('\n').filter(line => line.trim()); } catch (e) { // If no tags exist, get all commits const output = execSync('git log --oneline --no-merges', { encoding: 'utf8' }); commits = output.trim().split('\n').filter(line => line.trim()); } if (commits.length === 0 || (commits.length === 1 && !commits[0])) { console.log('No new commits found'); core.setOutput('should_release', 'false'); return; } console.log(`Analyzing ${commits.length} commits:`); commits.forEach(commit => console.log(` ${commit}`)); let hasBreaking = false; let hasFeature = false; let hasFix = false; let hasDocsOrChore = false; for (const commit of commits) { // Strip leading hash and whitespace, analyze only the subject const subject = commit.replace(/^[a-f0-9]+\s+/, ''); const msg = subject.toLowerCase(); const startsWith = (type) => msg.startsWith(`${type}:`) || msg.startsWith(`${type}(`); if (msg.includes('breaking change') || msg.includes('!:')) { hasBreaking = true; } else if (startsWith('feat') || msg.includes('feature:')) { hasFeature = true; } else if (startsWith('fix') || msg.includes('bugfix:')) { hasFix = true; } else if (startsWith('docs') || msg.includes('doc:') || startsWith('chore')) { hasDocsOrChore = true; } } let releaseType = 'none'; if (hasBreaking) { releaseType = 'major'; } else if (hasFeature) { releaseType = 'minor'; } else if (hasFix || hasDocsOrChore) { releaseType = 'patch'; } // Override with manual input if provided const manualType = '${{ github.event.inputs.release_type }}'; if (manualType && manualType !== 'auto') { releaseType = manualType; } const shouldRelease = releaseType !== 'none' || '${{ github.event.inputs.dry_run }}' === 'true'; console.log(`Release decision: ${shouldRelease ? 'YES' : 'NO'} (type: ${releaseType})`); core.setOutput('should_release', shouldRelease.toString()); core.setOutput('release_type', releaseType); release: name: Create Release runs-on: ubuntu-latest needs: check-changes if: needs.check-changes.outputs.should_release == 'true' outputs: version: ${{ steps.version.outputs.version }} changelog: ${{ steps.changelog.outputs.changelog }} permissions: # CodeQL Token-Permissions: Write permissions required for release operations # This job needs contents:write for both RELEASE_TOKEN and GITHUB_TOKEN fallback scenarios: # - git push tags (line 231): requires repository write access # - actions/create-release (line 237): requires contents write to create GitHub releases # Without write permissions, repositories lacking RELEASE_TOKEN configuration would fail # during tag creation and release publishing, breaking automated release workflows. # This is an intentional security trade-off for release pipeline functionality. contents: write # Required for fallback GITHUB_TOKEN to push tags and create releases steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 with: node-version: '18' cache: 'npm' registry-url: 'https://registry.npmjs.org' - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Run linting run: npm run lint || npx eslint . - name: Generate changelog id: changelog # This step dynamically sets the 'changelog' output uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with: script: | const { execSync } = require('child_process'); // Get current version const package = require('./package.json'); const currentVersion = package.version; // Get commits since last release let commits = []; try { const output = execSync('git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:"%h %s" --no-merges', { encoding: 'utf8' }); commits = output.trim().split('\n').filter(line => line.trim()); } catch (e) { const output = execSync('git log --pretty=format:"%h %s" --no-merges', { encoding: 'utf8' }); commits = output.trim().split('\n').filter(line => line.trim()); } // Categorize commits const features = []; const fixes = []; const breaking = []; const other = []; commits.forEach(commit => { const [hash, ...msgParts] = commit.split(' '); const msg = msgParts.join(' '); const lowerMsg = msg.toLowerCase(); if (lowerMsg.includes('breaking change') || lowerMsg.includes('!:')) { breaking.push(`- ${msg} (${hash})`); } else if (lowerMsg.startsWith('feat:') || lowerMsg.startsWith('feature:')) { features.push(`- ${msg.replace(/^feat:\s?/i, '').replace(/^feature:\s?/i, '')} (${hash})`); } else if (lowerMsg.startsWith('fix:') || lowerMsg.startsWith('bugfix:')) { fixes.push(`- ${msg.replace(/^fix:\s?/i, '').replace(/^bugfix:\s?/i, '')} (${hash})`); } else { other.push(`- ${msg} (${hash})`); } }); // Generate changelog let changelog = `## Changes\n\n`; if (breaking.length > 0) { changelog += `### 💥 Breaking Changes\n${breaking.join('\n')}\n\n`; } if (features.length > 0) { changelog += `### ✨ New Features\n${features.join('\n')}\n\n`; } if (fixes.length > 0) { changelog += `### 🐛 Bug Fixes\n${fixes.join('\n')}\n\n`; } if (other.length > 0) { changelog += `### 📝 Other Changes\n${other.join('\n')}\n\n`; } changelog += `**Full Changelog**: https://github.com/${{ github.repository }}/compare/v${currentVersion}...HEAD`; core.setOutput('changelog', changelog); return changelog; - name: Bump version id: version # This step dynamically sets the 'version' output run: | RELEASE_TYPE="${{ needs.check-changes.outputs.release_type }}" echo "Release type: $RELEASE_TYPE" if [ "$RELEASE_TYPE" = "none" ]; then echo "No version bump needed" echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT else NEW_VERSION=$(npm version $RELEASE_TYPE --no-git-tag-version) CANDIDATE=${NEW_VERSION#v} echo "Initial bumped version: v$CANDIDATE" # If the tag already exists, keep bumping patch until we find a free version while git rev-parse "v$CANDIDATE" >/dev/null 2>&1; do echo "Tag v$CANDIDATE already exists. Bumping patch..." NEW_VERSION=$(npm version patch --no-git-tag-version) CANDIDATE=${NEW_VERSION#v} done echo "version=$CANDIDATE" >> $GITHUB_OUTPUT echo "Bumped to available version: v$CANDIDATE" fi - name: Create Git tag (without committing version bump) if: ${{ github.event.inputs.dry_run != 'true' }} env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" git tag -a "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}" # Use authenticated git push with token git push https://$GITHUB_TOKEN@github.com/${{ github.repository }} "v${{ steps.version.outputs.version }}" echo "✅ Created and pushed tag v${{ steps.version.outputs.version }}" echo "ℹ️ Note: Version bump not committed to main due to branch protection" - name: Create GitHub Release if: ${{ github.event.inputs.dry_run != 'true' }} uses: actions/create-release@0cb9c9b65d5d1901c1f53e5e66eaf4afd303e70e env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }} with: tag_name: v${{ steps.version.outputs.version }} release_name: Release v${{ steps.version.outputs.version }} body: ${{ steps.changelog.outputs.changelog }} # Dynamic output from changelog step draft: false prerelease: ${{ contains(steps.version.outputs.version, '-') }} - name: Dry run summary if: ${{ github.event.inputs.dry_run == 'true' }} run: | echo "## 🧪 Dry Run Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Would create release:** v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Changelog preview:**" >> $GITHUB_STEP_SUMMARY echo '${{ steps.changelog.outputs.changelog }}' >> $GITHUB_STEP_SUMMARY # Dynamic output notify: name: Post-release Notifications runs-on: ubuntu-latest needs: [check-changes, release] if: success() && needs.check-changes.outputs.should_release == 'true' && github.event.inputs.dry_run != 'true' permissions: issues: write steps: - name: Update README badge uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with: script: | console.log('🎉 Release completed successfully!'); console.log('Consider updating documentation or notifying stakeholders.'); - name: Create follow-up issue for documentation uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with: script: | const { owner, repo } = context.repo; await github.rest.issues.create({ owner, repo, title: `📝 Update documentation for release v${{ needs.release.outputs.version }}`, body: `## Post-release Documentation Tasks A new release has been created. Please review and update: - [ ] Update CHANGELOG.md with detailed changes - [ ] Review README.md for any needed updates - [ ] Update any version-specific documentation - [ ] Notify users about breaking changes (if any) - [ ] Update examples if API changed **Release**: v${{ needs.release.outputs.version }} **Auto-created by**: Release automation workflow`, labels: ['documentation', 'automated', 'post-release'] }); version-pr: name: Create Version Bump PR runs-on: ubuntu-latest needs: [check-changes, release] if: success() && needs.check-changes.outputs.should_release == 'true' && github.event.inputs.dry_run != 'true' && github.event.inputs.create_version_pr == 'true' permissions: contents: write pull-requests: write steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 with: node-version: '18' - name: Prepare version bump commit id: bumpfile env: PUSH_TOKEN: ${{ secrets.RELEASE_PR_TOKEN || secrets.GITHUB_TOKEN }} run: | VERSION="${{ needs.release.outputs.version }}" BRANCH="chore/release/v$VERSION" echo "Using version: $VERSION" echo "Using branch: $BRANCH" git config user.email "action@github.com" git config user.name "GitHub Action" # Ensure we have latest refs git fetch origin --prune # Create or update local branch from remote if it exists if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then echo "Remote branch exists. Checking out tracking branch: $BRANCH" git checkout -B "$BRANCH" "origin/$BRANCH" else echo "Remote branch does not exist. Creating new branch: $BRANCH" git checkout -b "$BRANCH" fi # Update package.json version to match released tag node -e "const fs=require('fs'); const p=require('./package.json'); p.version='$VERSION'; fs.writeFileSync('package.json', JSON.stringify(p, null, 2)+'\n');" # Only commit if there is a change if ! git diff --quiet -- package.json; then git add package.json git commit -m "chore(release): bump package.json to v$VERSION" # Try normal push first using token to ensure CI triggers if ! git push -u "https://$PUSH_TOKEN@github.com/${{ github.repository }}" "$BRANCH"; then echo "Non-fast-forward push rejected. Rebasing and retrying with force-with-lease..." git pull --rebase origin "$BRANCH" || true git push --force-with-lease "https://$PUSH_TOKEN@github.com/${{ github.repository }}" "$BRANCH" fi else echo "No package.json version change needed" fi - name: Open pull request if: always() uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd with: github-token: ${{ secrets.RELEASE_PR_TOKEN || secrets.GITHUB_TOKEN }} script: | const version = process.env.VERSION || '${{ needs.release.outputs.version }}'; const branch = process.env.BRANCH || `chore/release/v${{ needs.release.outputs.version }}`; const { owner, repo } = context.repo; // Check if PR already exists const prs = await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', head: `${owner}:${branch}` }); if (prs.length > 0) { core.info(`PR already exists: #${prs[0].number}`); return; } const body = [ `This automated PR bumps package.json to v${version} to match the released tag.`, '', `- Version: v${version}`, `- Source workflow: Release Automation`, ].join('\n'); const pr = await github.rest.pulls.create({ owner, repo, title: `chore(release): bump package.json to v${version}`, head: branch, base: 'main', body }); core.info(`Opened PR #${pr.data.number}`); cleanup: name: Cleanup runs-on: ubuntu-latest needs: [check-changes, release] if: always() steps: - name: Workflow summary run: | echo "## 🚀 Release Workflow Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Should Release**: ${{ needs.check-changes.outputs.should_release }}" >> $GITHUB_STEP_SUMMARY echo "**Release Type**: ${{ needs.check-changes.outputs.release_type }}" >> $GITHUB_STEP_SUMMARY echo "**Dry Run**: ${{ github.event.inputs.dry_run }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ needs.release.result }}" = "success" ]; then echo "✅ **Release Status**: Completed successfully" >> $GITHUB_STEP_SUMMARY elif [ "${{ needs.check-changes.outputs.should_release }}" = "false" ]; then echo "⏭️ **Release Status**: Skipped (no release-worthy changes)" >> $GITHUB_STEP_SUMMARY else echo "❌ **Release Status**: Failed" >> $GITHUB_STEP_SUMMARY fi

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/egarcia74/warp-sql-server-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server