feat: enhance the logs for the commit linting stage (#293)

* 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>

* fix(ci): improve PR title validation error messages with examples

Add helpful console output when PR title validation fails:
- Show expected format and valid types
- Provide examples of valid PR titles
- Display the user's current title
- Suggest fixes based on keywords in the title
- Handle verb variations (fixed, adding, updated, etc.)
- Show placeholder when description is empty after cleanup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* lower coverage

* feat: improve status gate to label correctly based on required checks

* fix(ci): address PR review findings for security and efficiency

- Add explicit permissions block to ci.yml (least privilege principle)
- Skip duplicate test run for Python 3.12 (tests with coverage only)
- Sanitize PR title in markdown output to prevent injection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix typo

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex
2025-12-26 10:31:27 +01:00
committed by GitHub
parent 217249c8a3
commit 8416f3076a
3 changed files with 134 additions and 54 deletions
+6 -1
View File
@@ -6,6 +6,10 @@ on:
pull_request:
branches: [main, develop]
permissions:
contents: read
actions: read
jobs:
# Python tests
test-python:
@@ -36,6 +40,7 @@ jobs:
uv pip install -r ../../tests/requirements-test.txt
- name: Run tests
if: matrix.python-version != '3.12'
working-directory: apps/backend
env:
PYTHONPATH: ${{ github.workspace }}/apps/backend
@@ -50,7 +55,7 @@ jobs:
PYTHONPATH: ${{ github.workspace }}/apps/backend
run: |
source .venv/bin/activate
pytest ../../tests/ -v --cov=. --cov-report=xml --cov-report=term-missing
pytest ../../tests/ -v --cov=. --cov-report=xml --cov-report=term-missing --cov-fail-under=20
- name: Upload coverage reports
if: matrix.python-version == '3.12'
+85 -51
View File
@@ -7,7 +7,7 @@ on:
permissions:
pull-requests: write
actions: read
checks: read
jobs:
update-status:
@@ -17,7 +17,7 @@ jobs:
if: github.event.workflow_run.pull_requests[0] != null
timeout-minutes: 5
steps:
- name: Check all workflows and update label
- name: Check all required checks and update label
uses: actions/github-script@v7
with:
retries: 3
@@ -28,13 +28,32 @@ jobs:
const headSha = context.payload.workflow_run.head_sha;
const triggerWorkflow = context.payload.workflow_run.name;
// Required workflows configuration
const required = [
{ name: 'CI', file: 'ci.yml' },
{ name: 'Lint', file: 'lint.yml' },
{ name: 'Quality Security', file: 'quality-security.yml' },
{ name: 'Quality DCO', file: 'quality-dco.yml' },
{ name: 'Quality Commit Lint', file: 'quality-commit-lint.yml' }
// ═══════════════════════════════════════════════════════════════════════
// REQUIRED CHECK RUNS - Job-level checks (not workflow-level)
// ═══════════════════════════════════════════════════════════════════════
// Format: "{Workflow Name} / {Job Name} (pull_request)"
//
// To find check names: Go to PR → Checks tab → copy exact name
// To update: Edit this list when workflow jobs are added/renamed/removed
//
// Last validated: 2025-12-26
// ═══════════════════════════════════════════════════════════════════════
const requiredChecks = [
// CI workflow (ci.yml) - 3 checks
'CI / test-frontend (pull_request)',
'CI / test-python (3.12) (pull_request)',
'CI / test-python (3.13) (pull_request)',
// Lint workflow (lint.yml) - 1 check
'Lint / python (pull_request)',
// Quality Security workflow (quality-security.yml) - 4 checks
'Quality Security / CodeQL (javascript-typescript) (pull_request)',
'Quality Security / CodeQL (python) (pull_request)',
'Quality Security / Python Security (Bandit) (pull_request)',
'Quality Security / Security Summary (pull_request)',
// Quality DCO workflow (quality-dco.yml) - 1 check
'Quality DCO / DCO Check (pull_request)',
// Quality Commit Lint workflow (quality-commit-lint.yml) - 1 check
'Quality Commit Lint / Conventional Commits (pull_request)'
];
const statusLabels = {
@@ -43,64 +62,71 @@ jobs:
failed: '❌ Checks Failed'
};
console.log(`::group::PR #${prNumber} - Checking workflow status`);
console.log(`::group::PR #${prNumber} - Checking required checks`);
console.log(`Triggered by: ${triggerWorkflow}`);
console.log(`Head SHA: ${headSha}`);
console.log(`Required checks: ${requiredChecks.length}`);
console.log('');
// Fetch all check runs for this commit
let allCheckRuns = [];
try {
const { data } = await github.rest.checks.listForRef({
owner,
repo,
ref: headSha,
per_page: 100
});
allCheckRuns = data.check_runs;
console.log(`Found ${allCheckRuns.length} total check runs`);
} catch (error) {
// Add warning annotation so maintainers are alerted
core.warning(`Failed to fetch check runs for PR #${prNumber}: ${error.message}. PR label may be outdated.`);
console.log(`::error::Failed to fetch check runs: ${error.message}`);
console.log('::endgroup::');
return;
}
let allComplete = true;
let anyFailed = false;
const results = [];
// Check all required workflows
for (const workflow of required) {
try {
const { data } = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: workflow.file,
head_sha: headSha,
per_page: 1
});
// Check each required check
for (const checkName of requiredChecks) {
const check = allCheckRuns.find(c => c.name === checkName);
if (!data.workflow_runs.length) {
results.push({ name: workflow.name, status: '⏳ Pending', complete: false });
allComplete = false;
continue;
}
const run = data.workflow_runs[0];
if (run.status !== 'completed') {
results.push({ name: workflow.name, status: '🔄 Running', complete: false });
allComplete = false;
} else if (run.conclusion === 'success') {
results.push({ name: workflow.name, status: '✅ Passed', complete: true });
} else if (run.conclusion === 'skipped') {
results.push({ name: workflow.name, status: '⏭️ Skipped', complete: true });
} else {
results.push({ name: workflow.name, status: '❌ Failed', complete: true, failed: true });
anyFailed = true;
}
} catch (error) {
console.log(`::warning::Error checking ${workflow.name}: ${error.message}`);
results.push({ name: workflow.name, status: '⚠️ Error', complete: false });
if (!check) {
results.push({ name: checkName, status: '⏳ Pending', complete: false });
allComplete = false;
} else if (check.status !== 'completed') {
results.push({ name: checkName, status: '🔄 Running', complete: false });
allComplete = false;
} else if (check.conclusion === 'success') {
results.push({ name: checkName, status: '✅ Passed', complete: true });
} else if (check.conclusion === 'skipped') {
// Skipped checks are treated as passed (e.g., path filters, conditional jobs)
results.push({ name: checkName, status: '⏭️ Skipped', complete: true, skipped: true });
} else {
results.push({ name: checkName, status: '❌ Failed', complete: true, failed: true });
anyFailed = true;
}
}
// Print results table
console.log('Workflow Status:');
console.log('─'.repeat(40));
console.log('');
console.log('Check Status:');
console.log('─'.repeat(70));
for (const r of results) {
console.log(` ${r.status.padEnd(12)} ${r.name}`);
const shortName = r.name.length > 55 ? r.name.substring(0, 52) + '...' : r.name;
console.log(` ${r.status.padEnd(12)} ${shortName}`);
}
console.log('─'.repeat(40));
console.log('─'.repeat(70));
console.log('::endgroup::');
// Only update label if all workflows are complete
// Only update label if all required checks are complete
if (!allComplete) {
console.log('⏳ Not all workflows complete yet - keeping current label');
const pending = results.filter(r => !r.complete).length;
console.log(`⏳ ${pending}/${requiredChecks.length} checks still pending - keeping current label`);
return;
}
@@ -145,17 +171,25 @@ jobs:
console.log('::endgroup::');
// Summary
const passedCount = results.filter(r => r.status === '✅ Passed').length;
const skippedCount = results.filter(r => r.skipped).length;
const failedCount = results.filter(r => r.failed).length;
if (anyFailed) {
console.log(`❌ PR #${prNumber} has failing checks`);
console.log(`❌ PR #${prNumber} has ${failedCount} failing check(s)`);
core.summary.addRaw(`## ❌ PR #${prNumber} - Checks Failed\n\n`);
core.summary.addRaw(`**${failedCount}** of **${requiredChecks.length}** required checks failed.\n\n`);
} else {
console.log(`✅ PR #${prNumber} is ready for review`);
const skippedNote = skippedCount > 0 ? ` (${skippedCount} skipped)` : '';
const totalSuccessful = passedCount + skippedCount;
console.log(`✅ PR #${prNumber} is ready for review (${totalSuccessful}/${requiredChecks.length} checks succeeded${skippedNote})`);
core.summary.addRaw(`## ✅ PR #${prNumber} - Ready for Review\n\n`);
core.summary.addRaw(`All **${requiredChecks.length}** required checks succeeded${skippedNote}.\n\n`);
}
// Add results to summary
core.summary.addTable([
[{data: 'Workflow', header: true}, {data: 'Status', header: true}],
[{data: 'Check', header: true}, {data: 'Status', header: true}],
...results.map(r => [r.name, r.status])
]);
await core.summary.write();
+43 -2
View File
@@ -22,6 +22,8 @@ jobs:
script: |
const pr = context.payload.pull_request;
const title = pr.title;
// Sanitize title for safe markdown interpolation (prevent injection)
const sanitizedTitle = title.replace(/`/g, "'").replace(/\[/g, '\\[').replace(/\]/g, '\\]');
console.log(`::group::PR #${pr.number} - Validating PR title`);
console.log(`Title: ${title}`);
@@ -37,9 +39,48 @@ jobs:
console.log('::endgroup::');
if (!isValid) {
// Log helpful error message to console (visible in workflow logs)
console.log('');
console.log('❌ PR title does not follow Conventional Commits format');
console.log('');
console.log('Expected format: type(scope): description');
console.log('');
console.log('Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert');
console.log('');
console.log('Examples of valid PR titles:');
console.log(' ✓ feat(auth): add OAuth2 login support');
console.log(' ✓ fix(api): handle null response correctly');
console.log(' ✓ docs: update README installation steps');
console.log(' ✓ chore: update dependencies');
console.log('');
console.log(`Your title: "${title}"`);
console.log('');
console.log('Suggested fix for your title:');
// Try to suggest a fix based on the title
const lowerTitle = title.toLowerCase();
const placeholder = '[add description here]';
if (lowerTitle.includes('fix') || lowerTitle.includes('bug')) {
const cleaned = title.replace(/^(fix(ed|es|ing)?|bug)[:\s]*/i, '').trim();
console.log(` → fix: ${cleaned || placeholder}`);
} else if (lowerTitle.includes('add') || lowerTitle.includes('new') || lowerTitle.includes('feature')) {
const cleaned = title.replace(/^(add(ed|s|ing)?|new|features?)[:\s]*/i, '').trim();
console.log(` → feat: ${cleaned || placeholder}`);
} else if (lowerTitle.includes('update') || lowerTitle.includes('change')) {
const cleaned = title.replace(/^(update[ds]?|chang(ed|es|ing)?)[:\s]*/i, '').trim();
console.log(` → chore: ${cleaned || placeholder}`);
} else if (lowerTitle.includes('doc') || lowerTitle.includes('readme')) {
const cleaned = title.replace(/^(docs?|readme)[:\s]*/i, '').trim();
console.log(` → docs: ${cleaned || placeholder}`);
} else {
console.log(` → feat: ${title}`);
console.log(` → fix: ${title}`);
console.log(` → chore: ${title}`);
}
console.log('');
let errorMsg = '## ❌ PR Title Validation Failed\n\n';
errorMsg += `Your PR title does not follow [Conventional Commits](https://www.conventionalcommits.org/) format:\n\n`;
errorMsg += `> \`${title}\`\n\n`;
errorMsg += `> \`${sanitizedTitle}\`\n\n`;
errorMsg += '### Expected Format\n\n';
errorMsg += '```\ntype(scope): description\n```\n\n';
errorMsg += '| Type | Description |\n';
@@ -69,6 +110,6 @@ jobs:
core.summary
.addHeading('✅ PR Title Valid', 3)
.addRaw(`PR title follows Conventional Commits format: \`${title}\``);
.addRaw(`PR title follows Conventional Commits format: \`${sanitizedTitle}\``);
await core.summary.write();
}