d42041c5b5
* ci: implement enterprise-grade PR quality gates and security scanning * ci: implement enterprise-grade PR quality gates and security scanning * fix:pr comments and improve code * fix: improve commit linting and code quality * Removed the dependency-review job (i added it) * fix: address CodeRabbit review comments - Expand scope pattern to allow uppercase, underscores, slashes, dots - Add concurrency control to cancel duplicate security scan runs - Add explanatory comment for Bandit CLI flags - Remove dependency-review job (requires repo settings) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: update commit lint examples with expanded scope patterns Show slashes and dots in scope examples to demonstrate the newly allowed characters (api/users, package.json) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: remove feature request issue template Feature requests are directed to GitHub Discussions via the issue template config.yml 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address security vulnerabilities in service orchestrator - Fix port parsing crash on malformed docker-compose entries - Fix shell injection risk by using shlex.split() with shell=False Prevents crashes when docker-compose.yml contains environment variables in port mappings (e.g., '${PORT}:8080') and eliminates shell injection vulnerabilities in subprocess execution. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
228 lines
9.8 KiB
YAML
228 lines
9.8 KiB
YAML
name: PR Auto Label
|
|
|
|
on:
|
|
pull_request:
|
|
types: [opened, synchronize, reopened]
|
|
|
|
# Cancel in-progress runs for the same PR
|
|
concurrency:
|
|
group: pr-auto-label-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
|
|
jobs:
|
|
label:
|
|
name: Auto Label PR
|
|
runs-on: ubuntu-latest
|
|
# Don't run on fork PRs (they can't write labels)
|
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: Auto-label PR
|
|
uses: actions/github-script@v7
|
|
with:
|
|
retries: 3
|
|
retry-exempt-status-codes: 400,401,403,404,422
|
|
script: |
|
|
const { owner, repo } = context.repo;
|
|
const pr = context.payload.pull_request;
|
|
const prNumber = pr.number;
|
|
const title = pr.title;
|
|
|
|
console.log(`::group::PR #${prNumber} - Auto-labeling`);
|
|
console.log(`Title: ${title}`);
|
|
|
|
const labelsToAdd = new Set();
|
|
const labelsToRemove = new Set();
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// TYPE LABELS (from PR title - Conventional Commits)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
const typeMap = {
|
|
'feat': 'feature',
|
|
'fix': 'bug',
|
|
'docs': 'documentation',
|
|
'refactor': 'refactor',
|
|
'test': 'test',
|
|
'ci': 'ci',
|
|
'chore': 'chore',
|
|
'perf': 'performance',
|
|
'style': 'style',
|
|
'build': 'build'
|
|
};
|
|
|
|
const typeMatch = title.match(/^(\w+)(\(.+?\))?(!)?:/);
|
|
if (typeMatch) {
|
|
const type = typeMatch[1].toLowerCase();
|
|
const isBreaking = typeMatch[3] === '!';
|
|
|
|
if (typeMap[type]) {
|
|
labelsToAdd.add(typeMap[type]);
|
|
console.log(` 📝 Type: ${type} → ${typeMap[type]}`);
|
|
}
|
|
|
|
if (isBreaking) {
|
|
labelsToAdd.add('breaking-change');
|
|
console.log(` ⚠️ Breaking change detected`);
|
|
}
|
|
} else {
|
|
console.log(` ⚠️ No conventional commit prefix found in title`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// AREA LABELS (from changed files)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
let files = [];
|
|
try {
|
|
const { data } = await github.rest.pulls.listFiles({
|
|
owner,
|
|
repo,
|
|
pull_number: prNumber,
|
|
per_page: 100
|
|
});
|
|
files = data;
|
|
} catch (e) {
|
|
console.log(` ⚠️ Could not fetch files: ${e.message}`);
|
|
}
|
|
|
|
const areas = {
|
|
frontend: false,
|
|
backend: false,
|
|
ci: false,
|
|
docs: false,
|
|
tests: false
|
|
};
|
|
|
|
for (const file of files) {
|
|
const path = file.filename;
|
|
if (path.startsWith('apps/frontend/')) areas.frontend = true;
|
|
if (path.startsWith('apps/backend/')) areas.backend = true;
|
|
if (path.startsWith('.github/')) areas.ci = true;
|
|
if (path.endsWith('.md') || path.startsWith('docs/')) areas.docs = true;
|
|
if (path.startsWith('tests/') || path.includes('.test.') || path.includes('.spec.')) areas.tests = true;
|
|
}
|
|
|
|
// Determine area label (mutually exclusive)
|
|
const areaLabels = ['area/frontend', 'area/backend', 'area/fullstack', 'area/ci'];
|
|
|
|
if (areas.frontend && areas.backend) {
|
|
labelsToAdd.add('area/fullstack');
|
|
areaLabels.filter(l => l !== 'area/fullstack').forEach(l => labelsToRemove.add(l));
|
|
console.log(` 📁 Area: fullstack (${files.length} files)`);
|
|
} else if (areas.frontend) {
|
|
labelsToAdd.add('area/frontend');
|
|
areaLabels.filter(l => l !== 'area/frontend').forEach(l => labelsToRemove.add(l));
|
|
console.log(` 📁 Area: frontend (${files.length} files)`);
|
|
} else if (areas.backend) {
|
|
labelsToAdd.add('area/backend');
|
|
areaLabels.filter(l => l !== 'area/backend').forEach(l => labelsToRemove.add(l));
|
|
console.log(` 📁 Area: backend (${files.length} files)`);
|
|
} else if (areas.ci) {
|
|
labelsToAdd.add('area/ci');
|
|
areaLabels.filter(l => l !== 'area/ci').forEach(l => labelsToRemove.add(l));
|
|
console.log(` 📁 Area: ci (${files.length} files)`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// SIZE LABELS (from lines changed)
|
|
// ═══════════════════════════════════════════════════════════════
|
|
const additions = pr.additions || 0;
|
|
const deletions = pr.deletions || 0;
|
|
const totalLines = additions + deletions;
|
|
|
|
const sizeLabels = ['size/XS', 'size/S', 'size/M', 'size/L', 'size/XL'];
|
|
let sizeLabel;
|
|
|
|
if (totalLines < 10) sizeLabel = 'size/XS';
|
|
else if (totalLines < 100) sizeLabel = 'size/S';
|
|
else if (totalLines < 500) sizeLabel = 'size/M';
|
|
else if (totalLines < 1000) sizeLabel = 'size/L';
|
|
else sizeLabel = 'size/XL';
|
|
|
|
labelsToAdd.add(sizeLabel);
|
|
sizeLabels.filter(l => l !== sizeLabel).forEach(l => labelsToRemove.add(l));
|
|
console.log(` 📏 Size: ${sizeLabel} (+${additions}/-${deletions} = ${totalLines} lines)`);
|
|
|
|
console.log('::endgroup::');
|
|
|
|
// ═══════════════════════════════════════════════════════════════
|
|
// APPLY LABELS
|
|
// ═══════════════════════════════════════════════════════════════
|
|
console.log(`::group::Applying labels`);
|
|
|
|
// Remove old labels (in parallel)
|
|
const removeArray = [...labelsToRemove].filter(l => !labelsToAdd.has(l));
|
|
if (removeArray.length > 0) {
|
|
const removePromises = removeArray.map(async (label) => {
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
owner,
|
|
repo,
|
|
issue_number: prNumber,
|
|
name: label
|
|
});
|
|
console.log(` ✓ Removed: ${label}`);
|
|
} catch (e) {
|
|
if (e.status !== 404) {
|
|
console.log(` ⚠ Could not remove ${label}: ${e.message}`);
|
|
}
|
|
}
|
|
});
|
|
await Promise.all(removePromises);
|
|
}
|
|
|
|
// Add new labels
|
|
const addArray = [...labelsToAdd];
|
|
if (addArray.length > 0) {
|
|
try {
|
|
await github.rest.issues.addLabels({
|
|
owner,
|
|
repo,
|
|
issue_number: prNumber,
|
|
labels: addArray
|
|
});
|
|
console.log(` ✓ Added: ${addArray.join(', ')}`);
|
|
} catch (e) {
|
|
// Some labels might not exist
|
|
if (e.status === 404) {
|
|
core.warning(`Some labels do not exist. Please create them in repository settings.`);
|
|
// Try adding one by one
|
|
for (const label of addArray) {
|
|
try {
|
|
await github.rest.issues.addLabels({
|
|
owner,
|
|
repo,
|
|
issue_number: prNumber,
|
|
labels: [label]
|
|
});
|
|
} catch (e2) {
|
|
console.log(` ⚠ Label '${label}' does not exist`);
|
|
}
|
|
}
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('::endgroup::');
|
|
|
|
// Summary
|
|
console.log(`✅ PR #${prNumber} labeled: ${addArray.join(', ')}`);
|
|
|
|
// Write job summary
|
|
core.summary
|
|
.addHeading(`PR #${prNumber} Auto-Labels`, 3)
|
|
.addTable([
|
|
[{data: 'Category', header: true}, {data: 'Label', header: true}],
|
|
['Type', typeMatch ? typeMap[typeMatch[1].toLowerCase()] || 'none' : 'none'],
|
|
['Area', areas.frontend && areas.backend ? 'fullstack' : areas.frontend ? 'frontend' : areas.backend ? 'backend' : 'other'],
|
|
['Size', sizeLabel]
|
|
])
|
|
.addRaw(`\n**Files changed:** ${files.length}\n`)
|
|
.addRaw(`**Lines:** +${additions} / -${deletions}\n`);
|
|
await core.summary.write();
|