Compare commits

...

10 Commits

Author SHA1 Message Date
Alex d789d0ad15 Merge branch 'develop' into enhance-workflows 2026-01-06 12:10:36 +01:00
Alex Madera 324edfa7d5 fix bugbot comments 2026-01-04 09:01:00 +01:00
Alex b5f452b428 Merge branch 'develop' into enhance-workflows 2026-01-04 08:42:27 +01:00
Andy b0de22a988 Merge branch 'develop' into enhance-workflows 2026-01-03 23:55:57 +01:00
Alex 3c49173210 Merge branch 'develop' into enhance-workflows 2026-01-03 15:21:56 +01:00
Andy d3587c3c42 Merge branch 'develop' into enhance-workflows 2026-01-02 19:46:07 +01:00
Alex Madera f6c9a84acb fix(ci): update action versions for Node.js compatibility
- Update actions/first-interaction to v1.4.0 (Node 16+)
- Keep actions/setup-node@v4 with node-version: 24

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 16:42:09 +01:00
Alex 91b80c0df0 Merge branch 'develop' into enhance-workflows 2026-01-02 16:40:23 +01:00
Alex Madera 052e6a0970 ci: enterprise-grade unified CI pipeline
- Consolidate 16 workflows → 7 workflows
- Single CI workflow per PR (Stage 1→2→3→4)
- Smart path filtering (skip tests for docs-only changes)
- Auto-labeling (type, area, size, status)
- Production-ready with proper documentation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 16:36:47 +01:00
Alex Madera 90f5b59c35 draft: improve yml to be less cluster 2026-01-02 15:35:26 +01:00
13 changed files with 878 additions and 977 deletions
+693 -30
View File
@@ -1,49 +1,386 @@
# ╔═══════════════════════════════════════════════════════════════════════════════╗
# ║ ║
# ║ AUTO CLAUDE - CI PIPELINE ║
# ║ ║
# ║ A unified, enterprise-grade CI workflow for pull request validation ║
# ║ and scheduled security scanning. ║
# ║ ║
# ║ TRIGGERS: ║
# ║ - Pull requests to main/develop branches ║
# ║ - Weekly scheduled security scans (Monday 00:00 UTC) ║
# ║ - Manual workflow dispatch ║
# ║ ║
# ║ WORKFLOW STAGES: ║
# ║ ┌─────────────────────────────────────────────────────────────────────────┐ ║
# ║ │ Stage 1: PR Setup - Label PR and set "Checking" status │ ║
# ║ │ Stage 2: Change Detection - Determine which tests to run │ ║
# ║ │ Stage 3: Quality Gates - Run tests, linting, and security scans │ ║
# ║ │ Stage 4: Status Update - Update PR with final status │ ║
# ║ └─────────────────────────────────────────────────────────────────────────┘ ║
# ║ ║
# ║ SMART PATH FILTERING (PR mode): ║
# ║ - Backend changes (apps/backend/**, tests/**) → Python tests + lint ║
# ║ - Frontend changes (apps/frontend/**) → Frontend tests + build ║
# ║ - No code changes (docs, CI configs only) → Skip tests, mark ready ║
# ║ ║
# ║ SCHEDULED SECURITY SCANS: ║
# ║ - Weekly CodeQL analysis for Python and JavaScript/TypeScript ║
# ║ - Weekly Bandit security scan for Python backend ║
# ║ - Detects newly discovered CVEs in existing code ║
# ║ ║
# ║ LABELS APPLIED (PR mode only): ║
# ║ - Status: 🔄 Checking → ✅ Ready for Review / ❌ Checks Failed ║
# ║ - Type: feature, bug, docs, refactor, ci, chore (from PR title) ║
# ║ - Area: area/frontend, area/backend, area/fullstack, area/ci ║
# ║ - Size: size/XS, size/S, size/M, size/L, size/XL ║
# ║ ║
# ║ MAINTAINERS: See CONTRIBUTING.md for workflow modification guidelines. ║
# ║ ║
# ╚═══════════════════════════════════════════════════════════════════════════════╝
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
types: [opened, synchronize, reopened]
# Weekly scheduled security scans to detect newly discovered CVEs
# Runs every Monday at midnight UTC
schedule:
- cron: '0 0 * * 1'
# Allow manual triggering for security scans
workflow_dispatch:
# ─────────────────────────────────────────────────────────────────────────────────
# CONCURRENCY: Cancel redundant runs when new commits are pushed to the same PR
# ─────────────────────────────────────────────────────────────────────────────────
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
group: ci-${{ github.head_ref || github.ref }}
cancel-in-progress: true
# ─────────────────────────────────────────────────────────────────────────────────
# PERMISSIONS: Minimal permissions required for this workflow
# ─────────────────────────────────────────────────────────────────────────────────
permissions:
contents: read
actions: read
contents: read # Read repository contents
actions: read # Read workflow runs
security-events: write # Upload CodeQL results
pull-requests: write # Update PR labels
checks: read # Read check run status
# ═══════════════════════════════════════════════════════════════════════════════════
# JOBS
# ═══════════════════════════════════════════════════════════════════════════════════
jobs:
# Python tests
test-python:
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ STAGE 1: PR SETUP │
# │ │
# │ Purpose: Initialize PR with labels and "Checking" status │
# │ Runs: First, before any other job │
# │ Labels: Status (Checking), Type, Area, Size │
# └─────────────────────────────────────────────────────────────────────────────┘
stage-1-setup:
name: "Stage 1: PR Setup"
runs-on: ubuntu-latest
# Skip for fork PRs (they cannot write labels) and scheduled runs (no PR context)
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 5
steps:
- name: "1.1 Apply Labels and Set Checking Status"
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(`\n${'═'.repeat(60)}`);
console.log(` PR #${prNumber}: ${title}`);
console.log(`${'═'.repeat(60)}\n`);
const labelsToAdd = new Set();
const labelsToRemove = new Set();
// ─────────────────────────────────────────────────────────────
// STATUS LABEL: Set to "Checking" while CI runs
// ─────────────────────────────────────────────────────────────
const statusLabels = ['🔄 Checking', '✅ Ready for Review', '❌ Checks Failed'];
statusLabels.forEach(l => labelsToRemove.add(l));
labelsToAdd.add('🔄 Checking');
console.log('Status: 🔄 Checking');
// ─────────────────────────────────────────────────────────────
// TYPE LABEL: Extracted from Conventional Commit prefix
// Format: type(scope)!: description
// Examples: feat:, fix(ui):, docs!:, refactor(api):
// ─────────────────────────────────────────────────────────────
const typeMap = {
'feat': 'feature', // New feature
'fix': 'bug', // Bug fix
'docs': 'documentation',// Documentation only
'refactor': 'refactor', // Code refactoring
'test': 'test', // Adding/updating tests
'ci': 'ci', // CI/CD changes
'chore': 'chore', // Maintenance tasks
'perf': 'performance', // Performance improvements
'style': 'style', // Code style changes
'build': 'build' // Build system changes
};
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: ${typeMap[type]}`);
}
if (isBreaking) {
labelsToAdd.add('breaking-change');
console.log('⚠️ Breaking change detected');
}
} else {
console.log('Type: (no conventional commit prefix detected)');
}
// ─────────────────────────────────────────────────────────────
// AREA LABEL: Determined by which files were changed
// ─────────────────────────────────────────────────────────────
let files = [];
try {
const { data } = await github.rest.pulls.listFiles({
owner, repo, pull_number: prNumber, per_page: 100
});
files = data;
} catch (e) {
console.log(`Warning: Could not fetch changed files: ${e.message}`);
}
const areas = { frontend: false, backend: false, ci: false };
for (const file of files) {
const path = file.filename;
if (path.startsWith('apps/frontend/')) areas.frontend = true;
if (path.startsWith('apps/backend/') || path.startsWith('tests/')) areas.backend = true;
if (path.startsWith('.github/')) areas.ci = true;
}
// Area labels are mutually exclusive
const areaLabels = ['area/frontend', 'area/backend', 'area/fullstack', 'area/ci'];
let areaLabel = null;
if (areas.frontend && areas.backend) {
areaLabel = 'area/fullstack';
} else if (areas.frontend) {
areaLabel = 'area/frontend';
} else if (areas.backend) {
areaLabel = 'area/backend';
} else if (areas.ci) {
areaLabel = 'area/ci';
}
if (areaLabel) {
labelsToAdd.add(areaLabel);
areaLabels.filter(l => l !== areaLabel).forEach(l => labelsToRemove.add(l));
console.log(`Area: ${areaLabel.replace('area/', '')}`);
}
// ─────────────────────────────────────────────────────────────
// SIZE LABEL: Based on total lines changed
// XS: <10, S: <100, M: <500, L: <1000, XL: >=1000
// ─────────────────────────────────────────────────────────────
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'];
const sizeLabel = totalLines < 10 ? 'size/XS' :
totalLines < 100 ? 'size/S' :
totalLines < 500 ? 'size/M' :
totalLines < 1000 ? 'size/L' : 'size/XL';
labelsToAdd.add(sizeLabel);
sizeLabels.filter(l => l !== sizeLabel).forEach(l => labelsToRemove.add(l));
console.log(`Size: ${sizeLabel.replace('size/', '')} (+${additions}/-${deletions} = ${totalLines} lines)`);
// ─────────────────────────────────────────────────────────────
// APPLY LABELS
// ─────────────────────────────────────────────────────────────
console.log(`\n${'─'.repeat(60)}`);
console.log('Applying labels...');
// Remove old labels first
const removeArray = [...labelsToRemove].filter(l => !labelsToAdd.has(l));
for (const label of removeArray) {
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: label
});
} catch (e) {
// Ignore 404 (label not present)
}
}
// 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(`✓ Labels applied: ${addArray.join(', ')}`);
} catch (e) {
if (e.status === 404) {
core.warning('Some labels do not exist. Please create them in Settings > Labels.');
// Try adding labels 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`);
}
}
}
}
}
console.log(`${'─'.repeat(60)}\n`);
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ STAGE 2: CHANGE DETECTION │
# │ │
# │ Purpose: Analyze changed files to determine which tests to run │
# │ Runs: After Stage 1 completes │
# │ Outputs: backend, frontend, any_code, skip_tests │
# └─────────────────────────────────────────────────────────────────────────────┘
stage-2-changes:
name: "Stage 2: Detect Changes"
runs-on: ubuntu-latest
needs: stage-1-setup
# Always run even if stage-1 was skipped (fork PRs)
if: always()
timeout-minutes: 5
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
any_code: ${{ steps.filter.outputs.any_code }}
skip_tests: ${{ steps.evaluate.outputs.skip_tests }}
scheduled_scan: ${{ steps.evaluate.outputs.scheduled_scan }}
steps:
- name: "2.1 Checkout Repository"
uses: actions/checkout@v4
- name: "2.2 Analyze Changed Files"
id: filter
uses: dorny/paths-filter@v3
with:
filters: |
backend:
- 'apps/backend/**'
- 'tests/**'
- 'requirements*.txt'
frontend:
- 'apps/frontend/**'
- 'package*.json'
any_code:
- 'apps/**'
- 'tests/**'
- 'package*.json'
- 'requirements*.txt'
- name: "2.3 Evaluate Test Requirements"
id: evaluate
run: |
echo ""
echo "═══════════════════════════════════════════════════════════"
echo " CHANGE DETECTION RESULTS"
echo "═══════════════════════════════════════════════════════════"
echo ""
# For scheduled runs, always run security scans on all code
if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo " Event type: ${{ github.event_name }}"
echo " → Scheduled/manual security scan - running all checks"
echo ""
echo "skip_tests=false" >> $GITHUB_OUTPUT
echo "scheduled_scan=true" >> $GITHUB_OUTPUT
else
echo " Backend changes: ${{ steps.filter.outputs.backend }}"
echo " Frontend changes: ${{ steps.filter.outputs.frontend }}"
echo " Any code changes: ${{ steps.filter.outputs.any_code }}"
echo ""
echo "scheduled_scan=false" >> $GITHUB_OUTPUT
if [ "${{ steps.filter.outputs.any_code }}" = "false" ]; then
echo "skip_tests=true" >> $GITHUB_OUTPUT
echo " → No code changes detected"
echo " → Tests will be SKIPPED (docs/config only changes)"
else
echo "skip_tests=false" >> $GITHUB_OUTPUT
echo " → Code changes detected"
echo " → Tests will be EXECUTED"
fi
fi
echo ""
echo "═══════════════════════════════════════════════════════════"
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ STAGE 3: QUALITY GATES │
# │ │
# │ Purpose: Run tests, linting, and security scans based on detected changes │
# │ Runs: After Stage 2, jobs run in parallel where possible │
# │ Jobs: Python Tests, Frontend Tests, Python Lint, CodeQL, Bandit │
# └─────────────────────────────────────────────────────────────────────────────┘
# ─────────────────────────────────────────────────────────────────────────────
# PYTHON TESTS: Run pytest across Python version matrix
# Triggered: When backend files change
# ─────────────────────────────────────────────────────────────────────────────
stage-3-test-python:
name: "Stage 3: Python Tests (${{ matrix.python-version }})"
runs-on: ubuntu-latest
needs: stage-2-changes
if: needs.stage-2-changes.outputs.backend == 'true'
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
python-version: ['3.12', '3.13']
steps:
- name: Checkout
- name: "3.1 Checkout Repository"
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- name: "3.2 Setup Python ${{ matrix.python-version }}"
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
- name: "3.3 Setup UV Package Manager"
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Install dependencies
- name: "3.4 Install Dependencies"
working-directory: apps/backend
run: |
uv venv
uv pip install -r requirements.txt
uv pip install -r ../../tests/requirements-test.txt
- name: Run tests
- name: "3.5 Run Test Suite"
working-directory: apps/backend
env:
PYTHONPATH: ${{ github.workspace }}/apps/backend
@@ -51,16 +388,20 @@ jobs:
source .venv/bin/activate
pytest ../../tests/ -v --tb=short -x
- name: Run tests with coverage
- name: "3.6 Run Tests with Coverage"
if: matrix.python-version == '3.12'
working-directory: apps/backend
env:
PYTHONPATH: ${{ github.workspace }}/apps/backend
run: |
source .venv/bin/activate
pytest ../../tests/ -v --cov=. --cov-report=xml --cov-report=term-missing --cov-fail-under=20
pytest ../../tests/ -v \
--cov=. \
--cov-report=xml \
--cov-report=term-missing \
--cov-fail-under=20
- name: Upload coverage reports
- name: "3.7 Upload Coverage to Codecov"
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v4
with:
@@ -69,44 +410,366 @@ jobs:
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
# Frontend lint, typecheck, test, and build
test-frontend:
# ─────────────────────────────────────────────────────────────────────────────
# FRONTEND TESTS: Lint, typecheck, test, and build
# Triggered: When frontend files change
# ─────────────────────────────────────────────────────────────────────────────
stage-3-test-frontend:
name: "Stage 3: Frontend Tests"
runs-on: ubuntu-latest
needs: stage-2-changes
if: needs.stage-2-changes.outputs.frontend == 'true'
timeout-minutes: 15
steps:
- name: Checkout
- name: "3.1 Checkout Repository"
uses: actions/checkout@v4
- name: Setup Node.js
- name: "3.2 Setup Node.js"
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Get npm cache directory
id: npm-cache
run: echo "dir=$(npm config get cache)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
- name: "3.3 Cache npm Dependencies"
uses: actions/cache@v4
with:
path: ${{ steps.npm-cache.outputs.dir }}
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-npm-
- name: Install dependencies
- name: "3.4 Install Dependencies"
working-directory: apps/frontend
run: npm ci --ignore-scripts
- name: Lint
- name: "3.5 Run ESLint"
working-directory: apps/frontend
run: npm run lint
- name: Type check
- name: "3.6 Run TypeScript Type Check"
working-directory: apps/frontend
run: npm run typecheck
- name: Run tests
- name: "3.7 Run Unit Tests"
working-directory: apps/frontend
run: npm run test
- name: Build
- name: "3.8 Build Application"
working-directory: apps/frontend
run: npm run build
# ─────────────────────────────────────────────────────────────────────────────
# PYTHON LINT: Check code style and formatting with Ruff
# Triggered: When backend files change
# ─────────────────────────────────────────────────────────────────────────────
stage-3-lint-python:
name: "Stage 3: Python Lint"
runs-on: ubuntu-latest
needs: stage-2-changes
if: needs.stage-2-changes.outputs.backend == 'true'
timeout-minutes: 10
steps:
- name: "3.1 Checkout Repository"
uses: actions/checkout@v4
- name: "3.2 Setup Python"
uses: actions/setup-python@v5
with:
python-version: '3.12'
# Ruff version pinned to match .pre-commit-config.yaml
- name: "3.3 Install Ruff"
run: pip install ruff==0.14.10
- name: "3.4 Run Ruff Linter"
run: ruff check apps/backend/ --output-format=github
- name: "3.5 Check Code Formatting"
run: ruff format apps/backend/ --check --diff
# ─────────────────────────────────────────────────────────────────────────────
# CODEQL: Static analysis for security vulnerabilities
# Triggered: When any code files change OR on scheduled/manual security scans
# ─────────────────────────────────────────────────────────────────────────────
stage-3-codeql:
name: "Stage 3: CodeQL (${{ matrix.language }})"
runs-on: ubuntu-latest
needs: stage-2-changes
# Run on code changes OR scheduled/manual security scans
if: needs.stage-2-changes.outputs.any_code == 'true' || needs.stage-2-changes.outputs.scheduled_scan == 'true'
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
language: [python, javascript-typescript]
steps:
- name: "3.1 Checkout Repository"
uses: actions/checkout@v4
- name: "3.2 Initialize CodeQL"
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: +security-extended,security-and-quality
- name: "3.3 Autobuild"
uses: github/codeql-action/autobuild@v3
- name: "3.4 Run CodeQL Analysis"
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
# ─────────────────────────────────────────────────────────────────────────────
# PYTHON SECURITY: Bandit security scanner for Python code
# Triggered: When backend files change OR on scheduled/manual security scans
# ─────────────────────────────────────────────────────────────────────────────
stage-3-security-python:
name: "Stage 3: Python Security"
runs-on: ubuntu-latest
needs: stage-2-changes
# Run on backend changes OR scheduled/manual security scans
if: needs.stage-2-changes.outputs.backend == 'true' || needs.stage-2-changes.outputs.scheduled_scan == 'true'
timeout-minutes: 10
steps:
- name: "3.1 Checkout Repository"
uses: actions/checkout@v4
- name: "3.2 Setup Python"
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: "3.3 Install Bandit"
run: pip install bandit
- name: "3.4 Run Bandit Security Scan"
id: bandit
run: |
# Run Bandit and capture exit code
# Exit 0 = no issues, Exit 1 = issues found, Exit > 1 = real error
set +e
bandit -r apps/backend/ -ll -ii -f json -o bandit-report.json
BANDIT_EXIT=$?
set -e
if [ $BANDIT_EXIT -eq 0 ]; then
echo "✓ Bandit scan completed - no issues found"
echo "scan_status=clean" >> $GITHUB_OUTPUT
elif [ $BANDIT_EXIT -eq 1 ]; then
echo "⚠ Bandit scan completed - security issues found"
echo "scan_status=issues_found" >> $GITHUB_OUTPUT
else
echo "✗ Bandit scan failed with exit code $BANDIT_EXIT"
echo " This indicates a configuration error or missing directory"
exit $BANDIT_EXIT
fi
- name: "3.5 Analyze Security Results"
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const scanStatus = '${{ steps.bandit.outputs.scan_status }}';
if (!fs.existsSync('bandit-report.json')) {
core.setFailed('Bandit report not found - scan failed before producing output');
return;
}
// If scan was clean, we can skip detailed analysis
if (scanStatus === 'clean') {
console.log('\n' + '═'.repeat(60));
console.log(' BANDIT SECURITY SCAN RESULTS');
console.log('═'.repeat(60));
console.log('\n✓ No security issues found\n');
console.log('═'.repeat(60) + '\n');
return;
}
const report = JSON.parse(fs.readFileSync('bandit-report.json', 'utf8'));
const results = report.results || [];
// Categorize by severity
const high = results.filter(r => r.issue_severity === 'HIGH');
const medium = results.filter(r => r.issue_severity === 'MEDIUM');
const low = results.filter(r => r.issue_severity === 'LOW');
console.log('\n' + '═'.repeat(60));
console.log(' BANDIT SECURITY SCAN RESULTS');
console.log('═'.repeat(60));
console.log(`\n HIGH: ${high.length}`);
console.log(` MEDIUM: ${medium.length}`);
console.log(` LOW: ${low.length}\n`);
if (high.length > 0) {
console.log('─'.repeat(60));
console.log(' HIGH SEVERITY ISSUES:');
console.log('─'.repeat(60));
for (const issue of high) {
console.log(`\n 📍 ${issue.filename}:${issue.line_number}`);
console.log(` ${issue.issue_text}`);
console.log(` Test: ${issue.test_id} (${issue.test_name})`);
}
console.log('\n' + '═'.repeat(60));
core.setFailed(`Found ${high.length} high severity security issue(s)`);
} else {
console.log('✓ No high severity security issues found');
console.log('═'.repeat(60) + '\n');
}
# ┌─────────────────────────────────────────────────────────────────────────────┐
# │ STAGE 4: STATUS UPDATE │
# │ │
# │ Purpose: Aggregate results from all quality gates and update PR status │
# │ Runs: After ALL Stage 3 jobs complete (success, failure, or skipped) │
# │ Updates: PR label to "Ready for Review" or "Checks Failed" │
# └─────────────────────────────────────────────────────────────────────────────┘
stage-4-status:
name: "Stage 4: Update PR Status"
runs-on: ubuntu-latest
needs:
- stage-2-changes
- stage-3-test-python
- stage-3-test-frontend
- stage-3-lint-python
- stage-3-codeql
- stage-3-security-python
# Always run to update status, even if previous jobs failed or were skipped
# For scheduled runs, skip PR label updates (no PR context)
if: always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
timeout-minutes: 5
steps:
- name: "4.1 Evaluate CI Results and Update PR"
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const eventName = '${{ github.event_name }}';
const isScheduledRun = eventName === 'schedule' || eventName === 'workflow_dispatch';
const prNumber = context.payload.pull_request?.number;
// ─────────────────────────────────────────────────────────────
// COLLECT RESULTS FROM ALL QUALITY GATE JOBS
// ─────────────────────────────────────────────────────────────
const results = {
'Python Tests': '${{ needs.stage-3-test-python.result }}',
'Frontend Tests': '${{ needs.stage-3-test-frontend.result }}',
'Python Lint': '${{ needs.stage-3-lint-python.result }}',
'CodeQL': '${{ needs.stage-3-codeql.result }}',
'Python Security': '${{ needs.stage-3-security-python.result }}'
};
const skipTests = '${{ needs.stage-2-changes.outputs.skip_tests }}' === 'true';
console.log('\n' + '═'.repeat(60));
if (isScheduledRun) {
console.log(' SCHEDULED SECURITY SCAN RESULTS');
} else {
console.log(' CI PIPELINE RESULTS');
}
console.log('═'.repeat(60) + '\n');
if (skipTests && !isScheduledRun) {
console.log(' ️ No code changes detected - tests were skipped\n');
}
console.log(' Job Results:');
console.log(' ' + '─'.repeat(40));
for (const [job, result] of Object.entries(results)) {
const icon = result === 'success' ? '✓' :
result === 'skipped' ? '○' :
result === 'failure' ? '✗' : '?';
console.log(` ${icon} ${job}: ${result}`);
}
console.log(' ' + '─'.repeat(40) + '\n');
// ─────────────────────────────────────────────────────────────
// DETERMINE FINAL STATUS
// Success and skipped are acceptable; failure is not
// ─────────────────────────────────────────────────────────────
const acceptable = ['success', 'skipped'];
const failed = Object.entries(results)
.filter(([_, result]) => !acceptable.includes(result))
.map(([job, _]) => job);
const statusLabels = {
checking: '🔄 Checking',
passed: '✅ Ready for Review',
failed: '❌ Checks Failed'
};
// ─────────────────────────────────────────────────────────────
// UPDATE PR LABELS (skip for scheduled runs - no PR context)
// ─────────────────────────────────────────────────────────────
if (!isScheduledRun && prNumber) {
// Remove all status labels first
for (const label of Object.values(statusLabels)) {
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: label
});
} catch (e) {
// Ignore 404 (label not present)
}
}
// Add appropriate status label
const newLabel = failed.length > 0 ? statusLabels.failed : statusLabels.passed;
try {
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber, labels: [newLabel]
});
} catch (e) {
if (e.status === 404) {
core.warning(`Label '${newLabel}' does not exist. Please create it in Settings > Labels.`);
}
}
}
// ─────────────────────────────────────────────────────────────
// GENERATE SUMMARY
// ─────────────────────────────────────────────────────────────
if (failed.length > 0) {
const resultType = isScheduledRun ? 'SECURITY SCAN FAILED' : 'CI FAILED';
console.log(` ❌ RESULT: ${resultType}`);
console.log(` Failed jobs: ${failed.join(', ')}`);
console.log('\n' + '═'.repeat(60) + '\n');
core.summary.addRaw(`## ❌ ${resultType}\n\n`);
core.summary.addRaw(`The following checks failed:\n`);
for (const job of failed) {
core.summary.addRaw(`- ${job}\n`);
}
core.setFailed(`${resultType}: ${failed.join(', ')}`);
} else if (isScheduledRun) {
console.log(` ✅ RESULT: SECURITY SCAN PASSED`);
console.log(` All security checks passed`);
console.log('\n' + '═'.repeat(60) + '\n');
core.summary.addRaw(`## ✅ Weekly Security Scan Passed\n\n`);
core.summary.addRaw(`All scheduled security checks (CodeQL, Bandit) completed successfully.\n`);
} else if (skipTests) {
console.log(` ✅ RESULT: READY FOR REVIEW`);
console.log(` No code changes - tests skipped`);
console.log('\n' + '═'.repeat(60) + '\n');
core.summary.addRaw(`## ✅ Ready for Review\n\n`);
core.summary.addRaw(`No code changes detected. Documentation or configuration changes only.\n`);
} else {
console.log(` ✅ RESULT: READY FOR REVIEW`);
console.log(` All quality gates passed`);
console.log('\n' + '═'.repeat(60) + '\n');
core.summary.addRaw(`## ✅ Ready for Review\n\n`);
core.summary.addRaw(`All CI checks passed successfully.\n`);
}
await core.summary.write();
+121
View File
@@ -0,0 +1,121 @@
name: Community
# Consolidated community automation:
# - Welcome messages for first-time contributors (issues & PRs)
# - Auto-label issues based on form selection
# - Mark and close stale issues
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
schedule:
- cron: '0 0 * * 0' # Every Sunday at midnight UTC
workflow_dispatch: # Allow manual trigger for stale check
jobs:
# ═══════════════════════════════════════════════════════════════════════════
# WELCOME - First-time contributor welcome messages
# ═══════════════════════════════════════════════════════════════════════════
welcome:
name: Welcome New Contributors
runs-on: ubuntu-latest
if: github.event_name == 'pull_request_target' || github.event_name == 'issues'
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/first-interaction@v1.4.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: |
Thanks for opening your first issue!
A maintainer will triage this soon. In the meantime:
- Make sure you've provided all the requested info
- Join our [Discord](https://discord.gg/QhRnz9m5HE) for faster help
pr-message: |
Thanks for your first PR!
A maintainer will review it soon. Please make sure:
- Your branch is synced with `develop`
- CI checks pass
- You've followed our [contribution guide](https://github.com/AndyMik90/Auto-Claude/blob/develop/CONTRIBUTING.md)
Welcome to the Auto Claude community!
# ═══════════════════════════════════════════════════════════════════════════
# ISSUE LABELS - Auto-label issues based on form area selection
# ═══════════════════════════════════════════════════════════════════════════
issue-labels:
name: Label Issue by Area
runs-on: ubuntu-latest
if: github.event_name == 'issues'
permissions:
issues: write
steps:
- name: Add area label from form
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const body = issue.body || '';
console.log(`Processing issue #${issue.number}: ${issue.title}`);
// Map form selection to label
const areaMap = {
'Frontend': 'area/frontend',
'Backend': 'area/backend',
'Fullstack': 'area/fullstack'
};
const labels = [];
for (const [key, label] of Object.entries(areaMap)) {
if (body.includes(key)) {
console.log(`Found area: ${key}, adding label: ${label}`);
labels.push(label);
break;
}
}
if (labels.length > 0) {
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: labels
});
console.log(`Successfully added labels: ${labels.join(', ')}`);
} catch (error) {
core.setFailed(`Failed to add labels: ${error.message}`);
}
} else {
console.log('No matching area found in issue body');
}
# ═══════════════════════════════════════════════════════════════════════════
# STALE - Mark and close inactive issues
# ═══════════════════════════════════════════════════════════════════════════
stale:
name: Mark Stale Issues
runs-on: ubuntu-latest
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
permissions:
issues: write
steps:
- uses: actions/stale@v9
with:
stale-issue-message: |
This issue has been inactive for 60 days. It will be closed in 14 days if there's no activity.
- If this is still relevant, please comment or update the issue
- If you're working on this, add the `in-progress` label
close-issue-message: 'Closed due to inactivity. Feel free to reopen if still relevant.'
stale-issue-label: 'stale'
days-before-stale: 60
days-before-close: 14
exempt-issue-labels: 'priority/critical,priority/high,in-progress,blocked'
-53
View File
@@ -1,53 +0,0 @@
name: Issue Auto Label
on:
issues:
types: [opened]
jobs:
label-area:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add area label from form
uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const body = issue.body || '';
console.log(`Processing issue #${issue.number}: ${issue.title}`);
// Map form selection to label
const areaMap = {
'Frontend': 'area/frontend',
'Backend': 'area/backend',
'Fullstack': 'area/fullstack'
};
const labels = [];
for (const [key, label] of Object.entries(areaMap)) {
if (body.includes(key)) {
console.log(`Found area: ${key}, adding label: ${label}`);
labels.push(label);
break;
}
}
if (labels.length > 0) {
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: labels
});
console.log(`Successfully added labels: ${labels.join(', ')}`);
} catch (error) {
core.setFailed(`Failed to add labels: ${error.message}`);
}
} else {
console.log('No matching area found in issue body');
}
-34
View File
@@ -1,34 +0,0 @@
name: Lint
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
concurrency:
group: lint-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
# Python linting
python:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
# Pin ruff version to match .pre-commit-config.yaml (astral-sh/ruff-pre-commit rev)
- name: Install ruff
run: pip install ruff==0.14.10
- name: Run ruff check
run: ruff check apps/backend/ --output-format=github
- name: Run ruff format check
run: ruff format apps/backend/ --check --diff
-227
View File
@@ -1,227 +0,0 @@
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();
-72
View File
@@ -1,72 +0,0 @@
name: PR Status Check
on:
pull_request:
types: [opened, synchronize, reopened]
# Cancel in-progress runs for the same PR
concurrency:
group: pr-status-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
pull-requests: write
jobs:
mark-checking:
name: Set Checking Status
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: Update PR status label
uses: actions/github-script@v7
with:
retries: 3
retry-exempt-status-codes: 400,401,403,404,422
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;
const statusLabels = ['🔄 Checking', '✅ Ready for Review', '❌ Checks Failed'];
console.log(`::group::PR #${prNumber} - Setting status to Checking`);
// Remove old status labels (parallel for speed)
const removePromises = statusLabels.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 checking label
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: ['🔄 Checking']
});
console.log(` ✓ Added: 🔄 Checking`);
} catch (e) {
// Label might not exist - create helpful error
if (e.status === 404) {
core.warning(`Label '🔄 Checking' does not exist. Please create it in repository settings.`);
}
throw e;
}
console.log('::endgroup::');
console.log(`✅ PR #${prNumber} marked as checking`);
-191
View File
@@ -1,191 +0,0 @@
name: PR Status Gate
on:
workflow_run:
workflows: [CI, Lint, Quality Security]
types: [completed]
permissions:
pull-requests: write
checks: read
jobs:
update-status:
name: Update PR Status
runs-on: ubuntu-latest
# Only run if this workflow_run is associated with a PR
if: github.event.workflow_run.pull_requests[0] != null
timeout-minutes: 5
steps:
- name: Check all required checks and update label
uses: actions/github-script@v7
with:
retries: 3
retry-exempt-status-codes: 400,401,403,404,422
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.workflow_run.pull_requests[0].number;
const headSha = context.payload.workflow_run.head_sha;
const triggerWorkflow = context.payload.workflow_run.name;
// ═══════════════════════════════════════════════════════════════════════
// REQUIRED CHECK RUNS - Job-level checks (not workflow-level)
// ═══════════════════════════════════════════════════════════════════════
// Format: "{Workflow Name} / {Job Name}" or "{Workflow Name} / {Job Custom Name}"
//
// 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: 2026-01-02
// ═══════════════════════════════════════════════════════════════════════
const requiredChecks = [
// CI workflow (ci.yml) - 3 checks
'CI / test-frontend',
'CI / test-python (3.12)',
'CI / test-python (3.13)',
// Lint workflow (lint.yml) - 1 check
'Lint / python',
// Quality Security workflow (quality-security.yml) - 4 checks
'Quality Security / CodeQL (javascript-typescript)',
'Quality Security / CodeQL (python)',
'Quality Security / Python Security (Bandit)',
'Quality Security / Security Summary'
];
const statusLabels = {
checking: '🔄 Checking',
passed: '✅ Ready for Review',
failed: '❌ Checks Failed'
};
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 each required check
for (const checkName of requiredChecks) {
const check = allCheckRuns.find(c => c.name === checkName);
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('');
console.log('Check Status:');
console.log('─'.repeat(70));
for (const r of results) {
const shortName = r.name.length > 55 ? r.name.substring(0, 52) + '...' : r.name;
console.log(` ${r.status.padEnd(12)} ${shortName}`);
}
console.log('─'.repeat(70));
console.log('::endgroup::');
// Only update label if all required checks are complete
if (!allComplete) {
const pending = results.filter(r => !r.complete).length;
console.log(`⏳ ${pending}/${requiredChecks.length} checks still pending - keeping current label`);
return;
}
// Determine final label
const newLabel = anyFailed ? statusLabels.failed : statusLabels.passed;
console.log(`::group::Updating PR #${prNumber} label`);
// Remove old status labels
for (const label of Object.values(statusLabels)) {
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}`);
}
}
}
// Add final status label
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: prNumber,
labels: [newLabel]
});
console.log(` ✓ Added: ${newLabel}`);
} catch (e) {
if (e.status === 404) {
core.warning(`Label '${newLabel}' does not exist. Please create it in repository settings.`);
}
throw e;
}
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 ${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 {
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: 'Check', header: true}, {data: 'Status', header: true}],
...results.map(r => [r.name, r.status])
]);
await core.summary.write();
+64
View File
@@ -34,6 +34,70 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Package version: $VERSION"
# Validate all version files are in sync before proceeding
- name: Validate version sync
run: |
echo "Validating version synchronization across all files..."
ROOT_VERSION=$(node -p "require('./package.json').version")
FRONTEND_VERSION=$(node -p "require('./apps/frontend/package.json').version")
# Extract Python version - handles both formats: __version__ = "X.Y.Z" or __version__="X.Y.Z"
BACKEND_VERSION=$(grep -oP '__version__\s*=\s*["\x27]\K[^"\x27]+' apps/backend/__init__.py || echo "NOT_FOUND")
echo "=========================================="
echo "Version Sync Validation"
echo "=========================================="
echo "Root package.json: $ROOT_VERSION"
echo "Frontend package.json: $FRONTEND_VERSION"
echo "Backend __init__.py: $BACKEND_VERSION"
echo "=========================================="
ERRORS=0
if [ "$ROOT_VERSION" != "$FRONTEND_VERSION" ]; then
echo "::error::Version mismatch: root package.json ($ROOT_VERSION) != frontend package.json ($FRONTEND_VERSION)"
ERRORS=$((ERRORS + 1))
fi
if [ "$BACKEND_VERSION" = "NOT_FOUND" ]; then
echo "::error::Could not extract version from apps/backend/__init__.py"
ERRORS=$((ERRORS + 1))
elif [ "$ROOT_VERSION" != "$BACKEND_VERSION" ]; then
echo "::error::Version mismatch: root package.json ($ROOT_VERSION) != backend __init__.py ($BACKEND_VERSION)"
ERRORS=$((ERRORS + 1))
fi
if [ $ERRORS -gt 0 ]; then
echo ""
echo "::error::═══════════════════════════════════════════════════════════════════════"
echo "::error:: VERSION SYNC FAILED"
echo "::error::═══════════════════════════════════════════════════════════════════════"
echo "::error::"
echo "::error:: All version files must be in sync before releasing."
echo "::error::"
echo "::error:: To fix this, use the bump-version script:"
echo "::error:: node scripts/bump-version.js <patch|minor|major|X.Y.Z>"
echo "::error::"
echo "::error:: This will update all version files automatically."
echo "::error::═══════════════════════════════════════════════════════════════════════"
# Add to job summary
echo "## Version Sync Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| File | Version |" >> $GITHUB_STEP_SUMMARY
echo "|------|---------|" >> $GITHUB_STEP_SUMMARY
echo "| package.json | $ROOT_VERSION |" >> $GITHUB_STEP_SUMMARY
echo "| apps/frontend/package.json | $FRONTEND_VERSION |" >> $GITHUB_STEP_SUMMARY
echo "| apps/backend/__init__.py | $BACKEND_VERSION |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Use \`node scripts/bump-version.js <version>\` to sync all files." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo ""
echo "All version files are in sync: $ROOT_VERSION"
- name: Get latest tag version
id: latest_tag
run: |
-178
View File
@@ -1,178 +0,0 @@
name: Quality Security
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
schedule:
- cron: '0 0 * * 1' # Weekly on Monday at midnight UTC
# Cancel in-progress runs for the same branch/PR
concurrency:
group: security-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
security-events: write
actions: read
jobs:
codeql:
name: CodeQL (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
language: [python, javascript-typescript]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: +security-extended,security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
python-security:
name: Python Security (Bandit)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Bandit
run: pip install bandit
- name: Run Bandit security scan
id: bandit
run: |
echo "::group::Running Bandit security scan"
# Run Bandit; exit code 1 means issues found (expected), other codes are errors
# Flags: -r=recursive, -ll=severity LOW+, -ii=confidence LOW+, -f=format, -o=output
bandit -r apps/backend/ -ll -ii -f json -o bandit-report.json || BANDIT_EXIT=$?
if [ "${BANDIT_EXIT:-0}" -gt 1 ]; then
echo "::error::Bandit scan failed with exit code $BANDIT_EXIT"
exit 1
fi
echo "::endgroup::"
- name: Analyze Bandit results
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Check if report exists
if (!fs.existsSync('bandit-report.json')) {
core.setFailed('Bandit report not found - scan may have failed');
return;
}
const report = JSON.parse(fs.readFileSync('bandit-report.json', 'utf8'));
const results = report.results || [];
// Categorize by severity
const high = results.filter(r => r.issue_severity === 'HIGH');
const medium = results.filter(r => r.issue_severity === 'MEDIUM');
const low = results.filter(r => r.issue_severity === 'LOW');
console.log(`::group::Bandit Security Scan Results`);
console.log(`Found ${results.length} issues:`);
console.log(` 🔴 HIGH: ${high.length}`);
console.log(` 🟡 MEDIUM: ${medium.length}`);
console.log(` 🟢 LOW: ${low.length}`);
console.log('');
// Print high severity issues
if (high.length > 0) {
console.log('High Severity Issues:');
console.log('─'.repeat(60));
for (const issue of high) {
console.log(` ${issue.filename}:${issue.line_number}`);
console.log(` ${issue.issue_text}`);
console.log(` Test: ${issue.test_id} (${issue.test_name})`);
console.log('');
}
}
console.log('::endgroup::');
// Build summary
let summary = `## 🔒 Python Security Scan (Bandit)\n\n`;
summary += `| Severity | Count |\n`;
summary += `|----------|-------|\n`;
summary += `| 🔴 High | ${high.length} |\n`;
summary += `| 🟡 Medium | ${medium.length} |\n`;
summary += `| 🟢 Low | ${low.length} |\n\n`;
if (high.length > 0) {
summary += `### High Severity Issues\n\n`;
for (const issue of high) {
summary += `- **${issue.filename}:${issue.line_number}**\n`;
summary += ` - ${issue.issue_text}\n`;
summary += ` - Test: \`${issue.test_id}\` (${issue.test_name})\n\n`;
}
}
core.summary.addRaw(summary);
await core.summary.write();
// Fail if high severity issues found
if (high.length > 0) {
core.setFailed(`Found ${high.length} high severity security issue(s)`);
} else {
console.log('✅ No high severity security issues found');
}
# Summary job that waits for all security checks
security-summary:
name: Security Summary
runs-on: ubuntu-latest
needs: [codeql, python-security]
if: always()
timeout-minutes: 5
steps:
- name: Check security results
uses: actions/github-script@v7
with:
script: |
const codeql = '${{ needs.codeql.result }}';
const bandit = '${{ needs.python-security.result }}';
console.log('Security Check Results:');
console.log(` CodeQL: ${codeql}`);
console.log(` Bandit: ${bandit}`);
// Only 'failure' is a real failure; 'skipped' is acceptable (e.g., path filters)
const acceptable = ['success', 'skipped'];
const codeqlOk = acceptable.includes(codeql);
const banditOk = acceptable.includes(bandit);
const allPassed = codeqlOk && banditOk;
if (allPassed) {
console.log('\n✅ All security checks passed');
core.summary.addRaw('## ✅ Security Checks Passed\n\nAll security scans completed successfully.');
} else {
console.log('\n❌ Some security checks failed');
core.summary.addRaw('## ❌ Security Checks Failed\n\nOne or more security scans found issues.');
core.setFailed('Security checks failed');
}
await core.summary.write();
-25
View File
@@ -1,25 +0,0 @@
name: Stale Issues
on:
schedule:
- cron: '0 0 * * 0' # Every Sunday
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v9
with:
stale-issue-message: |
This issue has been inactive for 60 days. It will be closed in 14 days if there's no activity.
- If this is still relevant, please comment or update the issue
- If you're working on this, add the `in-progress` label
close-issue-message: 'Closed due to inactivity. Feel free to reopen if still relevant.'
stale-issue-label: 'stale'
days-before-stale: 60
days-before-close: 14
exempt-issue-labels: 'priority/critical,priority/high,in-progress,blocked'
-63
View File
@@ -1,63 +0,0 @@
name: Test on Tag
on:
push:
tags:
- 'v*'
jobs:
# Python tests
test-python:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.12', '3.13']
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Install dependencies
working-directory: apps/backend
run: |
uv venv
uv pip install -r requirements.txt
uv pip install -r ../../tests/requirements-test.txt
- name: Run tests
working-directory: apps/backend
env:
PYTHONPATH: ${{ github.workspace }}/apps/backend
run: |
source .venv/bin/activate
pytest ../../tests/ -v --tb=short
# Frontend tests
test-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Install dependencies
working-directory: apps/frontend
run: npm ci --ignore-scripts
- name: Run tests
working-directory: apps/frontend
run: npm run test
-71
View File
@@ -1,71 +0,0 @@
name: Validate Version
on:
push:
tags:
- 'v*'
jobs:
validate-version:
name: Validate package.json version matches tag
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Extract version from tag
id: tag_version
run: |
# Extract version from tag (e.g., v2.5.5 -> 2.5.5)
TAG_VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$TAG_VERSION" >> $GITHUB_OUTPUT
echo "Tag version: $TAG_VERSION"
- name: Extract version from package.json
id: package_version
run: |
# Read version from package.json
PACKAGE_VERSION=$(node -p "require('./apps/frontend/package.json').version")
echo "version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
echo "Package.json version: $PACKAGE_VERSION"
- name: Compare versions
run: |
TAG_VERSION="${{ steps.tag_version.outputs.version }}"
PACKAGE_VERSION="${{ steps.package_version.outputs.version }}"
echo "=========================================="
echo "Version Validation"
echo "=========================================="
echo "Git tag version: v$TAG_VERSION"
echo "package.json version: $PACKAGE_VERSION"
echo "=========================================="
if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then
echo ""
echo "❌ ERROR: Version mismatch detected!"
echo ""
echo "The version in package.json ($PACKAGE_VERSION) does not match"
echo "the git tag version ($TAG_VERSION)."
echo ""
echo "To fix this:"
echo " 1. Delete this tag: git tag -d v$TAG_VERSION"
echo " 2. Update package.json version to $TAG_VERSION"
echo " 3. Commit the change"
echo " 4. Recreate the tag: git tag -a v$TAG_VERSION -m 'Release v$TAG_VERSION'"
echo ""
echo "Or use the automated script:"
echo " node scripts/bump-version.js $TAG_VERSION"
echo ""
exit 1
fi
echo ""
echo "✅ SUCCESS: Versions match!"
echo ""
- name: Version validation result
if: success()
run: |
echo "::notice::Version validation passed - package.json version matches tag v${{ steps.tag_version.outputs.version }}"
-33
View File
@@ -1,33 +0,0 @@
name: Welcome
on:
pull_request_target:
types: [opened]
issues:
types: [opened]
jobs:
welcome:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: |
👋 Thanks for opening your first issue!
A maintainer will triage this soon. In the meantime:
- Make sure you've provided all the requested info
- Join our [Discord](https://discord.gg/QhRnz9m5HE) for faster help
pr-message: |
🎉 Thanks for your first PR!
A maintainer will review it soon. Please make sure:
- Your branch is synced with `develop`
- CI checks pass
- You've followed our [contribution guide](https://github.com/AndyMik90/Auto-Claude/blob/develop/CONTRIBUTING.md)
Welcome to the Auto Claude community!