Skip to content

feat: overhaul auto-publish workflow for better reliability #4

feat: overhaul auto-publish workflow for better reliability

feat: overhaul auto-publish workflow for better reliability #4

Workflow file for this run

name: Auto Publish to NPM
on:
pull_request:
types: [closed]
branches:
- main
- master
permissions:
contents: write
pull-requests: write
packages: write
actions: read
jobs:
auto-publish:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Yarn and generate lockfile
run: |
corepack enable
yarn --version
# Always run yarn install to ensure lockfile exists and is up to date
echo "📦 Running yarn install..."
yarn install
echo "✅ Dependencies installed and lockfile ready"
- name: Setup Node.js with cache
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
cache-dependency-path: yarn.lock
- name: Validate branch patterns
id: validate-branch
run: |
BRANCH_NAME="${{ github.event.pull_request.head.ref }}"
echo "Branch name: $BRANCH_NAME"
# Simplified pattern that properly handles hyphens, underscores, and dots
if [[ $BRANCH_NAME =~ ^(feat|feature|fix|bugfix|break|breaking|hotfix|chore)/[a-zA-Z0-9._-]+$ ]]; then
echo "✅ Branch pattern accepted: $BRANCH_NAME"
echo "should_publish=true" >> $GITHUB_OUTPUT
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
else
echo "❌ Branch '$BRANCH_NAME' doesn't match required patterns"
echo "should_publish=false" >> $GITHUB_OUTPUT
exit 0
fi
- name: Install dependencies
if: steps.validate-branch.outputs.should_publish == 'true'
run: yarn install --frozen-lockfile --prefer-offline
- name: Validate package integrity
if: steps.validate-branch.outputs.should_publish == 'true'
run: |
# Verify that package.json is well-formed
node -e "console.log('Package name:', require('./package.json').name)"
# Verify that required scripts exist
if ! yarn run --help | grep -q "test:ci"; then
echo "❌ Missing test:ci script in package.json"
exit 1
fi
if ! yarn run --help | grep -q "dist"; then
echo "❌ Missing dist script in package.json"
exit 1
fi
- name: Run quality checks
if: steps.validate-branch.outputs.should_publish == 'true'
run: |
# Linting
if yarn run --help | grep -q "lint"; then
echo "🔍 Running linter..."
yarn lint
fi
# Type checking
if yarn run --help | grep -q "type-check"; then
echo "🔍 Type checking..."
yarn type-check
fi
- name: Run tests
if: steps.validate-branch.outputs.should_publish == 'true'
run: |
echo "🧪 Running tests..."
yarn test:ci
# Check coverage if exists
if [ -f "coverage/lcov.info" ]; then
echo "📊 Coverage report generated"
fi
- name: Build package
if: steps.validate-branch.outputs.should_publish == 'true'
run: |
echo "🏗️ Building package..."
yarn dist
# Verify that the build generated files
if [ ! -d "dist" ] && [ ! -d "lib" ] && [ ! -d "build" ]; then
echo "❌ No build output found"
exit 1
fi
- name: Configure Git
if: steps.validate-branch.outputs.should_publish == 'true'
run: |
git config --local user.email "[email protected]"
git config --local user.name "Kubit Release Bot"
- name: Determine version bump (Enhanced)
if: steps.validate-branch.outputs.should_publish == 'true'
id: version-bump
run: |
BRANCH_NAME="${{ github.event.pull_request.head.ref }}"
PR_TITLE="${{ github.event.pull_request.title }}"
PR_BODY="${{ github.event.pull_request.body }}"
echo "🔍 Analyzing PR for version bump..."
echo "Branch: $BRANCH_NAME"
echo "Title: $PR_TITLE"
# 1. Check explicit breaking change markers
if echo "$PR_BODY" | grep -qi "BREAKING CHANGE:" || \
echo "$PR_TITLE" | grep -q "!" || \
echo "$PR_TITLE" | grep -qi "\[breaking\]" || \
[[ $BRANCH_NAME =~ ^break/ ]] || \
[[ $BRANCH_NAME =~ ^breaking/ ]]; then
VERSION_TYPE="major"
REASON="Breaking change detected"
echo "💥 MAJOR: $REASON"
# 2. Check conventional commits in title
elif echo "$PR_TITLE" | grep -Eq "^(feat|feature)(\(.+\))?!:" || \
echo "$PR_TITLE" | grep -Eq "^(fix|bugfix)(\(.+\))?!:"; then
VERSION_TYPE="major"
REASON="Breaking change in conventional commit"
echo "💥 MAJOR: $REASON"
# 3. Check features (minor)
elif echo "$PR_TITLE" | grep -Eq "^(feat|feature)(\(.+\))?:" || \
[[ $BRANCH_NAME =~ ^feat/ ]] || \
[[ $BRANCH_NAME =~ ^feature/ ]] || \
echo "$PR_TITLE" | grep -qi "\[feature\]"; then
VERSION_TYPE="minor"
REASON="New feature detected"
echo "✨ MINOR: $REASON"
# 4. Check fixes and other changes (patch)
else
VERSION_TYPE="patch"
REASON="Bug fix or other changes"
echo "🐛 PATCH: $REASON"
fi
# Get current version
CURRENT_VERSION=$(node -p "require('./package.json').version")
echo "📦 Current version: $CURRENT_VERSION"
# Calculate new version
NEW_VERSION=$(node -e "
const semver = require('semver');
const current = '$CURRENT_VERSION';
console.log(semver.inc(current, '$VERSION_TYPE'));
")
echo "🚀 New version will be: $NEW_VERSION"
echo "🎯 Decision reason: $REASON"
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "version_type=$VERSION_TYPE" >> $GITHUB_OUTPUT
echo "reason=$REASON" >> $GITHUB_OUTPUT
- name: Check if version already exists
if: steps.validate-branch.outputs.should_publish == 'true'
run: |
NEW_VERSION="${{ steps.version-bump.outputs.new_version }}"
PACKAGE_NAME=$(node -p "require('./package.json').name")
# Check if version already exists in NPM
if npm view "$PACKAGE_NAME@$NEW_VERSION" version 2>/dev/null; then
echo "❌ Version $NEW_VERSION already exists in NPM"
exit 1
fi
# Check if tag already exists
if git tag -l | grep -q "^v$NEW_VERSION$"; then
echo "❌ Tag v$NEW_VERSION already exists"
exit 1
fi
- name: Update version and commit
if: steps.validate-branch.outputs.should_publish == 'true'
run: |
NEW_VERSION="${{ steps.version-bump.outputs.new_version }}"
VERSION_TYPE="${{ steps.version-bump.outputs.version_type }}"
BRANCH_NAME="${{ steps.validate-branch.outputs.branch_name }}"
REASON="${{ steps.version-bump.outputs.reason }}"
# Update package.json
npm version $NEW_VERSION --no-git-tag-version
# Commit and tag
git add package.json yarn.lock 2>/dev/null || git add package.json
git commit -m "chore(release): $NEW_VERSION
Released from: $BRANCH_NAME
Type: $VERSION_TYPE
Reason: $REASON
[skip ci]"
git tag "v$NEW_VERSION" -m "Release v$NEW_VERSION"
- name: Dry run publish (verification)
if: steps.validate-branch.outputs.should_publish == 'true'
run: |
echo "🔍 Performing dry run..."
npm publish --dry-run --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to NPM
if: steps.validate-branch.outputs.should_publish == 'true'
id: npm-publish
run: |
NEW_VERSION="${{ steps.version-bump.outputs.new_version }}"
VERSION_TYPE="${{ steps.version-bump.outputs.version_type }}"
echo "📦 Publishing to NPM..."
if [[ "$VERSION_TYPE" == "major" ]]; then
echo "⚠️ Publishing MAJOR version $NEW_VERSION"
npm publish --access public --tag latest
else
npm publish --access public --tag latest
fi
echo "✅ Successfully published to NPM"
echo "published=true" >> $GITHUB_OUTPUT
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Push changes and tags
if: steps.npm-publish.outputs.published == 'true'
run: |
echo "📤 Pushing changes to repository..."
git push origin main
git push origin --tags
echo "✅ Changes pushed successfully"
- name: Create GitHub Release
if: steps.npm-publish.outputs.published == 'true'
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ steps.version-bump.outputs.new_version }}
name: Release v${{ steps.version-bump.outputs.new_version }}
body: |
## 🚀 Release v${{ steps.version-bump.outputs.new_version }}
**Type:** ${{ steps.version-bump.outputs.version_type }} release
**Branch:** `${{ steps.validate-branch.outputs.branch_name }}`
**Previous:** `${{ steps.version-bump.outputs.current_version }}`
### 📝 Changes
- ${{ github.event.pull_request.title }} (#${{ github.event.pull_request.number }})
### 📦 Installation
```bash
npm install @kubit-ui-web/react-components@${{ steps.version-bump.outputs.new_version }}
# or
yarn add @kubit-ui-web/react-components@${{ steps.version-bump.outputs.new_version }}
```
### 🔗 Links
- [NPM Package](https://www.npmjs.com/package/@kubit-ui-web/react-components/v/${{ steps.version-bump.outputs.new_version }})
- [Full Changelog](https://github.com/${{ github.repository }}/compare/v${{ steps.version-bump.outputs.current_version }}...v${{ steps.version-bump.outputs.new_version }})
draft: false
prerelease: ${{ steps.version-bump.outputs.version_type == 'major' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Success notification
if: steps.npm-publish.outputs.published == 'true'
uses: actions/github-script@v7
with:
script: |
const { version_type, new_version, current_version } = {
version_type: '${{ steps.version-bump.outputs.version_type }}',
new_version: '${{ steps.version-bump.outputs.new_version }}',
current_version: '${{ steps.version-bump.outputs.current_version }}'
};
const branchName = '${{ steps.validate-branch.outputs.branch_name }}';
const emoji = version_type === 'major' ? '💥' : version_type === 'minor' ? '✨' : '🐛';
const comment = `## ${emoji} Auto-publish Successful!
| Field | Value |
|-------|-------|
| **Branch** | \`${branchName}\` |
| **Type** | \`${version_type}\` |
| **Version** | \`${current_version}\` → \`${new_version}\` |
| **NPM** | [@kubit-ui-web/react-components@${new_version}](https://www.npmjs.com/package/@kubit-ui-web/react-components/v/${new_version}) |
### 📦 Installation
\`\`\`bash
npm install @kubit-ui-web/react-components@${new_version}
# or
yarn add @kubit-ui-web/react-components@${new_version}
\`\`\`
### ✅ Completed Steps
- [x] Quality checks passed
- [x] Tests passed
- [x] Build successful
- [x] Published to NPM
- [x] GitHub release created
- [x] Repository tagged
🎉 **Ready to use in production!**`;
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Failure notification
if: failure() && steps.validate-branch.outputs.should_publish == 'true'
uses: actions/github-script@v7
with:
script: |
const comment = `## ❌ Auto-publish Failed
The automatic publication process failed. Please check the [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
### 🔧 Common Solutions
- Verify NPM_TOKEN is valid and has publish permissions
- Check if version already exists
- Ensure all tests pass locally
- Verify build process completes successfully
### 📞 Next Steps
1. Fix the issue based on the error logs
2. Create a new PR with the same changes
3. Or use manual publish workflow if urgent`;
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Skip notification
if: steps.validate-branch.outputs.should_publish == 'false'
uses: actions/github-script@v7
with:
script: |
const branchName = '${{ github.event.pull_request.head.ref }}';
const comment = `## ℹ️ Auto-publish Skipped
Branch \`${branchName}\` doesn't match required patterns for auto-publishing.
### 📋 Required Patterns
| Pattern | Version Bump | Detection Method |
|---------|--------------|------------------|
| \`feat/\*\` or \`feature/\*\` | **minor** | Branch prefix or PR title |
| \`fix/\*\` or \`bugfix/\*\` | **patch** | Branch prefix or default |
| \`break/\*\` or \`breaking/\*\` | **major** | Branch prefix |
| \`hotfix/\*\` or \`chore/\*\` | **patch** | Branch prefix |
### 🎯 Advanced Version Detection
- **MAJOR**: \`BREAKING CHANGE:\` in PR body, \`!\` in title, or \`[breaking]\` tag
- **MINOR**: \`feat:\` or \`feature:\` in PR title, or \`[feature]\` tag
- **PATCH**: Default for fixes and other changes
### 🚀 To Auto-publish
Create a new PR from a branch with the appropriate prefix, or use the [manual publish workflow](https://github.com/${{ github.repository }}/actions/workflows/manual-publish.yml).`;
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});