Skip to content

feat: add HiMarket Product supports Recommendation #69 #227

feat: add HiMarket Product supports Recommendation #69

feat: add HiMarket Product supports Recommendation #69 #227

Workflow file for this run

name: PR Validation
# 触发条件:PR 打开、编辑、同步、重新打开时
on:
pull_request:
types: [opened, edited, synchronize, reopened]
# 避免重复运行:同一PR的新提交会取消之前的运行
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# 限制工作流权限(安全最佳实践)
permissions:
contents: read
pull-requests: write
issues: write
jobs:
# Job 1: PR Title Check
pr-title-check:
name: PR Title Check
runs-on: ubuntu-latest
steps:
- name: Check PR Title Format
uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Allowed types
types: |
feat
fix
docs
style
refactor
perf
test
build
ci
chore
revert
# Scope is optional
requireScope: false
# Subject must start with lowercase letter or non-ASCII characters (e.g., Chinese)
subjectPattern: ^(?![A-Z]).+$
subjectPatternError: |
❌ PR title format is incorrect!
**Wrong examples:**
- `feat: Add new feature` (first letter should be lowercase)
- `add new feature` (missing type prefix)
- `featadd new feature` (missing colon and space)
**Correct format:**
- `type: brief description`
- `type(scope): brief description`
**Description rules:**
- Start with lowercase letter: `feat: add product feature`
- Or use Chinese: `feat: 添加产品特性配置`
- Do NOT start with uppercase letter: ~~`feat: Add feature`~~
**Allowed types:**
- `feat`: new feature / 新功能
- `fix`: bug fix / Bug 修复
- `docs`: documentation / 文档更新
- `style`: code formatting / 代码格式
- `refactor`: code refactoring / 代码重构
- `perf`: performance / 性能优化
- `test`: testing / 测试
- `build`: build system / 构建系统
- `ci`: CI/CD changes / CI/CD 变更
- `chore`: other changes / 其他变更
- `revert`: revert commit / 回滚提交
**Correct examples:**
✅ `feat: add product feature configuration`
✅ `fix: fix product list pagination issue`
✅ `feat(product): add feature configuration panel`
✅ `docs: update README deployment guide`
✅ `feat: 添加产品特性配置` (Chinese is OK)
✅ `fix: 修复产品列表分页问题` (Chinese is OK)
# Job 2: PR Content Check
pr-content-check:
name: PR Content Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check PR Description and Required Fields
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const body = pr.body || '';
console.log('=== PR Body ===');
console.log(body);
console.log('===============');
// Required checks
const checks = {
hasDescription: {
test: () => {
// Find Description section (with optional emoji)
// Matches until next ## heading (not ###) or end of string
const descSection = body.match(/##\s*📝?\s*Description\s*([\s\S]*?)(?=\n##[^#]|$)/iu);
if (!descSection) return false;
// Remove HTML comments and whitespace
const content = descSection[1]
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/\s+/g, ' ')
.trim();
return content.length >= 10;
},
message: '❌ Missing description or description too short (at least 10 characters required)',
suggestion: 'Please provide a detailed description of your changes in the "Description" section.'
},
hasTypeOfChange: {
test: () => {
// Find Type of Change section (with optional emoji) and check if at least one is selected
// Matches until next ## heading (not ###) or end of string
const typeSection = body.match(/##\s*✅?\s*Type of Change\s*([\s\S]*?)(?=\n##[^#]|$)/iu);
if (!typeSection) return false;
return /- \[x\]/i.test(typeSection[1]);
},
message: '❌ No change type selected',
suggestion: 'Please select at least one change type in the "Type of Change" section (Bug fix, New feature, etc.).'
}
};
// Optional checks (warnings only)
const optionalChecks = {
hasRelatedIssues: {
test: () => {
// Look for issue links: Fix #123, Close #456, Resolve #789
return /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#\d+/i.test(body);
},
message: '💡 Consider linking related issues (e.g., `Fix #123` or `Close #456`)'
},
hasTestingInfo: {
test: () => {
// Check if Testing section is filled (with optional emoji)
// Matches until next ## heading (not ###) or end of string
const testSection = body.match(/##\s*🧪?\s*Testing\s*([\s\S]*?)(?=\n##[^#]|$)/iu);
if (!testSection) return false;
const content = testSection[1]
.replace(/<!--[\s\S]*?-->/g, '') // Remove HTML comments
.replace(/^- \[ \].*$/gm, '') // Remove unchecked checkboxes
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
// Check if there's actual content (checked boxes or descriptions)
return content.length > 10 && /[a-zA-Z0-9]/.test(content);
},
message: '💡 Consider describing how you tested these changes'
}
};
// Execute checks
const errors = [];
const suggestions = [];
const warnings = [];
for (const [key, check] of Object.entries(checks)) {
if (!check.test()) {
errors.push({ message: check.message, suggestion: check.suggestion });
}
}
for (const [key, check] of Object.entries(optionalChecks)) {
if (!check.test()) {
warnings.push(check.message);
}
}
// Check PR size
const additions = pr.additions || 0;
const deletions = pr.deletions || 0;
const totalChanges = additions + deletions;
let sizeWarning = '';
if (totalChanges > 1000) {
sizeWarning = `⚠️ **Large PR** (${totalChanges} lines): Consider splitting into smaller PRs for easier review.`;
} else if (totalChanges > 500) {
sizeWarning = `⚠️ **Medium PR** (${totalChanges} lines): Please ensure the PR is focused on a single feature or fix.`;
}
// Generate report
let comment = '## 📋 PR Content Check Report\n\n';
if (errors.length === 0) {
comment += '### ✅ All required checks passed\n\n';
} else {
comment += '### ❌ Required items need attention\n\n';
errors.forEach(error => {
comment += `${error.message}\n`;
if (error.suggestion) {
comment += ` 👉 ${error.suggestion}\n`;
}
comment += '\n';
});
}
if (warnings.length > 0) {
comment += '### 💡 Suggestions (Optional)\n\n';
warnings.forEach(warning => {
comment += `${warning}\n`;
});
comment += '\n';
}
if (sizeWarning) {
comment += '### 📊 PR Size Check\n\n';
comment += `${sizeWarning}\n\n`;
}
if (errors.length === 0 && warnings.length === 0 && !sizeWarning) {
comment += '---\n\n';
comment += '🎉 **Perfect!** Your PR is well-formatted and complete, ready for review.\n';
} else {
comment += '---\n\n';
comment += '### 📝 PR Content Requirements\n\n';
comment += '**Required:**\n';
comment += '- **Description**: Clear explanation of your changes (at least 10 characters)\n';
comment += '- **Type of Change**: Check at least one change type\n\n';
comment += '**Optional but recommended:**\n';
comment += '- **Related Issues**: Link issues using `Fix #123`, `Close #456`, etc.\n';
comment += '- **Testing**: Describe how you tested these changes\n';
comment += '- **Checklist**: Check other relevant items (code formatting, self-review, tests, etc.)\n\n';
comment += '**Example format:**\n';
comment += '```markdown\n';
comment += '## 📝 Description\n\n';
comment += '- Refactored client initialization method\n';
comment += '- Optimized parameter handling logic\n\n';
comment += '## 🔗 Related Issues\n\n';
comment += 'Fix #123\n\n';
comment += '## ✅ Type of Change\n\n';
comment += '- [x] Bug fix (non-breaking change)\n';
comment += '- [ ] New feature (non-breaking change)\n\n';
comment += '## 🧪 Testing\n\n';
comment += '- [x] Manual testing completed\n';
comment += '- [x] All tests pass locally\n';
comment += '```\n';
}
// Output to Job Summary (visible in PR checks)
await core.summary
.addRaw(comment)
.write();
// Output to console
console.log('\n' + comment);
// Create annotations for errors and warnings
errors.forEach(error => {
core.error(error.message.replace(/❌\s*/, ''), {
title: 'PR Content Check Failed',
file: '.github/PULL_REQUEST_TEMPLATE.md'
});
});
warnings.forEach(warning => {
core.warning(warning.replace(/💡\s*/, ''), {
title: 'PR Content Suggestion',
file: '.github/PULL_REQUEST_TEMPLATE.md'
});
});
// Fail if there are required items incomplete
if (errors.length > 0) {
core.setFailed(`PR content check failed: ${errors.length} required item(s) incomplete`);
}
# Job 3: PR Size Check
pr-size-check:
name: PR Size Check
runs-on: ubuntu-latest
steps:
- name: Check PR Change Size
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const additions = pr.additions || 0;
const deletions = pr.deletions || 0;
const totalChanges = additions + deletions;
const changedFiles = pr.changed_files || 0;
console.log(`PR Change Statistics:`);
console.log(` Lines added: ${additions}`);
console.log(` Lines deleted: ${deletions}`);
console.log(` Total lines changed: ${totalChanges}`);
console.log(` Files changed: ${changedFiles}`);
let summary = '## 📊 PR Size Statistics\n\n';
summary += `| Metric | Value |\n`;
summary += `|--------|-------|\n`;
summary += `| ➕ Lines Added | ${additions} |\n`;
summary += `| ➖ Lines Deleted | ${deletions} |\n`;
summary += `| 📝 Total Changes | ${totalChanges} |\n`;
summary += `| 📁 Files Changed | ${changedFiles} |\n\n`;
let sizeLabel = '';
let sizeEmoji = '';
let recommendation = '';
if (totalChanges < 100) {
sizeLabel = '🟢 XS (Extra Small)';
sizeEmoji = '🟢';
recommendation = '✅ **Excellent!** Small PRs are easy to review and merge quickly.';
} else if (totalChanges < 300) {
sizeLabel = '🟢 S (Small)';
sizeEmoji = '🟢';
recommendation = '✅ **Great!** Good PR size, easy to review.';
} else if (totalChanges < 600) {
sizeLabel = '🟡 M (Medium)';
sizeEmoji = '🟡';
recommendation = '⚠️ **Medium PR**. Please ensure the PR is focused on a single feature or fix.';
} else if (totalChanges < 1000) {
sizeLabel = '🟠 L (Large)';
sizeEmoji = '🟠';
recommendation = '⚠️ **Large PR**. Consider splitting into multiple smaller PRs for easier review.';
} else {
sizeLabel = '🔴 XL (Extra Large)';
sizeEmoji = '🔴';
recommendation = '❌ **Very Large PR!** Strongly recommend splitting into multiple smaller PRs:\n' +
' - Each PR focused on a single feature or fix\n' +
' - Easier code review and issue tracking\n' +
' - Reduces risk of introducing bugs';
}
summary += `### ${sizeEmoji} PR Size Assessment: ${sizeLabel}\n\n`;
summary += `${recommendation}\n\n`;
if (changedFiles > 20) {
summary += `⚠️ **Note**: ${changedFiles} files changed. Please ensure:\n`;
summary += `- All changes are related to this PR's goal\n`;
summary += `- No unrelated files are accidentally included\n\n`;
}
summary += '---\n\n';
summary += '### 💡 PR Size Best Practices\n\n';
summary += '- **Ideal size**: < 300 lines changed\n';
summary += '- **Acceptable size**: 300-600 lines\n';
summary += '- **Should split**: > 1000 lines\n';
summary += '- **Single responsibility**: Each PR should do one thing\n';
summary += '- **Iterative approach**: Implement large features through multiple small PRs\n';
await core.summary
.addRaw(summary)
.write();
console.log('\n' + summary);
// Warning for very large PRs (won't fail, just a reminder)
if (totalChanges > 1000) {
core.warning(`PR is very large (${totalChanges} lines), consider splitting into smaller PRs`, {
title: 'PR Size Warning',
file: '.github/PULL_REQUEST_TEMPLATE.md'
});
}
# Summary of all validation results
pr-validation-summary:
name: PR Validation Summary
runs-on: ubuntu-latest
needs: [pr-title-check, pr-content-check, pr-size-check]
if: always()
steps:
- name: Check All Validation Status
uses: actions/github-script@v7
with:
script: |
const jobs = [
{ name: 'PR Title Check', status: '${{ needs.pr-title-check.result }}' },
{ name: 'PR Content Check', status: '${{ needs.pr-content-check.result }}' },
{ name: 'PR Size Check', status: '${{ needs.pr-size-check.result }}' }
];
let summary = '## ✅ PR Validation Summary\n\n';
let allPassed = true;
let hasSkipped = false;
jobs.forEach(job => {
let icon = '✅';
let statusText = job.status;
if (job.status === 'success') {
icon = '✅';
statusText = 'Passed';
} else if (job.status === 'failure') {
icon = '❌';
statusText = 'Failed';
allPassed = false;
} else if (job.status === 'cancelled') {
icon = '🚫';
statusText = 'Cancelled';
allPassed = false;
} else if (job.status === 'skipped') {
icon = '⏭️';
statusText = 'Skipped';
hasSkipped = true; // Only track, don't fail
} else {
icon = '⚠️';
allPassed = false;
}
summary += `${icon} **${job.name}**: ${statusText}\n`;
});
summary += '\n---\n\n';
if (allPassed) {
summary += '🎉 **All validations passed!**\n\n';
summary += 'Your PR meets all format and content requirements and is ready for review.\n';
if (hasSkipped) {
summary += '\n📝 _Note: Some checks were skipped (this is normal)._\n';
}
} else {
summary += '⚠️ **Some validations failed**\n\n';
summary += 'Please review the failed checks above and fix the issues. The checks will automatically re-run after you fix them.\n\n';
summary += '### 📚 Resources\n\n';
summary += '- [Contributing Guide](CONTRIBUTING.md)\n';
summary += '- [PR Guide](.github/PR_GUIDE.md)\n';
}
await core.summary
.addRaw(summary)
.write();
console.log(summary);
// Fail the workflow if any check failed, regardless of skipped jobs
if (!allPassed) {
core.setFailed('Some PR validations failed');
}