build(deps): bump the dependencies group across 1 directory with 6 updates #614
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }); | |
| } | |