fix(workflow): improve script validation in auto-publish workflow #7
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 using Node.js | |
node -e " | |
const pkg = require('./package.json'); | |
const scripts = pkg.scripts || {}; | |
if (!scripts['test:ci']) { | |
console.error('❌ Missing test:ci script in package.json'); | |
process.exit(1); | |
} | |
if (!scripts['dist']) { | |
console.error('❌ Missing dist script in package.json'); | |
process.exit(1); | |
} | |
console.log('✅ Required scripts found:'); | |
console.log(' - test:ci:', scripts['test:ci']); | |
console.log(' - dist:', scripts['dist']); | |
" | |
- 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 | |
}); |