Skip to content

build(deps): bump the dependencies group across 1 directory with 6 updates #614

build(deps): bump the dependencies group across 1 directory with 6 updates

build(deps): bump the dependencies group across 1 directory with 6 updates #614

Workflow file for this run

name: PR Validation
# Simplified PR validation workflow for solo-maintained open-source project
# The service-specific workflows (backend-ci, frontend-ci, infrastructure-ci) run
# independently via their path-based triggers. This workflow provides:
# - Nx-based change detection (for PR status reporting)
# - Global security scan (full repository)
# - PR status comment with validation results
# - Dependabot PRs require manual merge (auto-merge removed to ensure CI gating)
on:
pull_request:
branches: [main]
paths-ignore:
# Documentation
- '**/*.md'
- '**/*.txt'
- '**/README*'
- 'docs/**'
- 'SECURITY.md'
- 'LICENSE'
- 'CODEOWNERS'
# Claude Code config
- '.claude/**'
- 'CLAUDE.md'
# Planning submodule
- 'plan/**'
# Editor/repo meta
- '**/.editorconfig'
- '**/.gitignore'
- '**/.gitattributes'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
checks: read
jobs:
# Detect what changed using Nx (for status reporting)
changes:
name: Detect Changes (Nx Affected)
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.affected.outputs.api }}
frontend: ${{ steps.affected.outputs.web }}
infrastructure: ${{ steps.infra.outputs.infrastructure }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Node & pnpm
uses: ./.github/actions/setup-node-pnpm
with:
working-directory: '.'
- name: Detect affected Nx projects
id: affected
run: |
AFFECTED=$(pnpm nx show projects --affected --base=origin/main --head=HEAD 2>/dev/null || echo "")
echo "Affected projects: $AFFECTED"
if echo "$AFFECTED" | grep -qxF "api"; then
echo "api=true" >> $GITHUB_OUTPUT
else
echo "api=false" >> $GITHUB_OUTPUT
fi
if echo "$AFFECTED" | grep -qxF "web"; then
echo "web=true" >> $GITHUB_OUTPUT
else
echo "web=false" >> $GITHUB_OUTPUT
fi
- name: Detect infrastructure changes
id: infra
run: |
if git diff --name-only origin/main...HEAD | grep -q "^infrastructure/"; then
echo "infrastructure=true" >> $GITHUB_OUTPUT
else
echo "infrastructure=false" >> $GITHUB_OUTPUT
fi
# Global security scan (full repository) - only when service-specific scans don't cover it
global-security-scan:
name: Global Security Scan
runs-on: ubuntu-latest
needs: [changes]
# Skip when service-specific CIs already run their own Trivy scans
if: |
needs.changes.outputs.backend != 'true' &&
needs.changes.outputs.frontend != 'true' &&
needs.changes.outputs.infrastructure != 'true'
# Job-level permissions replace the workflow-level block, which grants no
# security-events: write — so the SARIF upload below could never have worked.
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
# This scan previously wrote a SARIF file that was never uploaded, so it
# produced zero output at maximum cost. Two steps: report, then gate — with
# `format: sarif` the action reports all severities and ignores `severity`,
# so one step cannot both report everything and gate on HIGH/CRITICAL.
- name: Run Trivy vulnerability scanner (report)
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: 'fs'
format: 'sarif'
output: 'trivy-global-results.sarif'
- name: Upload SARIF results
if: always()
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: 'trivy-global-results.sarif'
category: 'security-global'
continue-on-error: true
- name: Fail on HIGH/CRITICAL vulnerabilities
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: 'fs'
format: 'table'
severity: 'HIGH,CRITICAL'
ignore-unfixed: 'true'
exit-code: '1'
# PR status comment - queries GitHub API for service CI results
pr-status-report:
name: PR Status Report
runs-on: ubuntu-latest
needs: [changes, global-security-scan]
if: always() && github.event_name == 'pull_request'
steps:
- name: Wait for service CIs to complete
uses: actions/github-script@v9
id: check-status
with:
script: |
// Shorter wait when no service changes detected (docs-only PRs)
const anyService = '${{ needs.changes.outputs.backend }}' === 'true' ||
'${{ needs.changes.outputs.frontend }}' === 'true' ||
'${{ needs.changes.outputs.infrastructure }}' === 'true';
const maxWait = anyService ? 5 * 60 * 1000 : 30 * 1000;
const pollInterval = 30 * 1000;
const start = Date.now();
const targetChecks = ['Backend CI/CD', 'Frontend CI/CD', 'Infrastructure CI/CD'];
while (Date.now() - start < maxWait) {
const { data: checks } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.payload.pull_request.head.sha,
});
const relevantChecks = checks.check_runs.filter(c =>
targetChecks.some(t => c.name.includes(t) || c.name.includes('Tests') || c.name.includes('Lint'))
);
const pendingChecks = relevantChecks.filter(c =>
c.status !== 'completed'
);
if (pendingChecks.length === 0 && relevantChecks.length > 0) {
console.log('All relevant checks completed');
break;
}
console.log(`Waiting for ${pendingChecks.length} checks to complete...`);
await new Promise(r => setTimeout(r, pollInterval));
}
// Get final check results
const { data: finalChecks } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: context.payload.pull_request.head.sha,
});
const results = {
backend: 'skipped',
frontend: 'skipped',
infrastructure: 'skipped',
security: '${{ needs.global-security-scan.result }}'
};
for (const check of finalChecks.check_runs) {
if (check.name.includes('Backend') || check.name.includes('api')) {
results.backend = check.conclusion || check.status;
}
if (check.name.includes('Frontend') || check.name.includes('web')) {
results.frontend = check.conclusion || check.status;
}
if (check.name.includes('Infrastructure') || check.name.includes('Terraform')) {
results.infrastructure = check.conclusion || check.status;
}
}
core.setOutput('backend', results.backend);
core.setOutput('frontend', results.frontend);
core.setOutput('infrastructure', results.infrastructure);
core.setOutput('security', results.security);
- name: Generate PR Status Report
uses: actions/github-script@v9
with:
script: |
const results = {
'Backend CI': '${{ steps.check-status.outputs.backend }}',
'Frontend CI': '${{ steps.check-status.outputs.frontend }}',
'Infrastructure CI': '${{ steps.check-status.outputs.infrastructure }}',
'Global Security Scan': '${{ steps.check-status.outputs.security }}'
};
const changes = {
backend: '${{ needs.changes.outputs.backend }}' === 'true',
frontend: '${{ needs.changes.outputs.frontend }}' === 'true',
infrastructure: '${{ needs.changes.outputs.infrastructure }}' === 'true'
};
let comment = '## 🚀 PR Validation Results\n\n';
// Changes summary
comment += '### 📁 Components Changed:\n';
if (changes.backend) comment += '- ✅ **Backend** (Spring Boot/Kotlin)\n';
if (changes.frontend) comment += '- ✅ **Frontend** (Next.js/React)\n';
if (changes.infrastructure) comment += '- ✅ **Infrastructure** (Terraform)\n';
if (!changes.backend && !changes.frontend && !changes.infrastructure) {
comment += '- ℹ️ No core component changes detected\n';
}
comment += '\n';
// Validation results
comment += '### 🔍 Validation Results:\n';
let allPassed = true;
let hasFailures = false;
for (const [check, result] of Object.entries(results)) {
if (result === 'skipped' || result === 'null') {
comment += `⏭️ **${check}**: Skipped (no relevant changes)\n`;
continue;
}
const icon = result === 'success' ? '✅' : '❌';
comment += `${icon} **${check}**: ${result}\n`;
if (result === 'failure') {
allPassed = false;
hasFailures = true;
} else if (result !== 'success') {
allPassed = false;
}
}
comment += '\n';
// Summary
if (allPassed) {
comment += '### 🎉 Status: READY FOR REVIEW\n';
comment += 'All validation checks have passed! This PR is ready for code review.\n';
} else if (hasFailures) {
comment += '### ⚠️ Status: CHANGES REQUIRED\n';
comment += 'Some checks failed. Please review and fix the issues.\n';
} else {
comment += '### ⏳ Status: IN PROGRESS\n';
comment += 'Some checks are still running.\n';
}
comment += '\n---\n';
comment += `*Updated: ${new Date().toISOString()} | PR: #${context.issue.number}*`;
// Find existing comment or create new
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(c =>
c.user.type === 'Bot' &&
c.body.includes('🚀 PR Validation Results')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}