diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e27b734e..afdcf923 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,12 +1,16 @@ name: CodeQL - Analyze on: + pull_request: + branches: [main, nightly] + push: + branches: [main, nightly, development] workflow_dispatch: schedule: - cron: '0 3 * * 1' # Mondays 03:00 UTC concurrency: - group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }} + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true env: @@ -23,9 +27,6 @@ jobs: analyze: name: CodeQL analysis (${{ matrix.language }}) runs-on: ubuntu-latest - # Skip forked PRs where CHARON_TOKEN lacks security-events permissions - if: >- - (github.event_name != 'workflow_run' || github.event.workflow_run.status != 'completed' || github.event.workflow_run.conclusion == 'success') permissions: contents: read security-events: write @@ -39,10 +40,10 @@ jobs: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - ref: ${{ github.event.workflow_run.head_sha || github.sha }} + ref: ${{ github.sha }} - name: Initialize CodeQL - uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4 + uses: github/codeql-action/init@015d8c7cbcbb8e7252a7dccfe81a90aa176260b2 # v4 with: languages: ${{ matrix.language }} # Use CodeQL config to exclude documented false positives @@ -58,10 +59,10 @@ jobs: cache-dependency-path: backend/go.sum - name: Autobuild - uses: github/codeql-action/autobuild@9e907b5e64f6b83e7804b09294d44122997950d6 # v4 + uses: github/codeql-action/autobuild@015d8c7cbcbb8e7252a7dccfe81a90aa176260b2 # v4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4 + uses: github/codeql-action/analyze@015d8c7cbcbb8e7252a7dccfe81a90aa176260b2 # v4 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 81a57851..a255bdf4 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -558,7 +558,7 @@ jobs: - name: Upload Trivy results if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/upload-sarif@015d8c7cbcbb8e7252a7dccfe81a90aa176260b2 # v4.32.3 with: sarif_file: 'trivy-results.sarif' token: ${{ secrets.GITHUB_TOKEN }} @@ -704,7 +704,7 @@ jobs: - name: Upload Trivy scan results if: always() - uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/upload-sarif@015d8c7cbcbb8e7252a7dccfe81a90aa176260b2 # v4.32.3 with: sarif_file: 'trivy-pr-results.sarif' category: 'docker-pr-image' diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index a7cb3c0a..03051341 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -71,6 +71,67 @@ jobs: echo "has_changes=true" >> "$GITHUB_OUTPUT" fi + trigger-nightly-validation: + name: Trigger Nightly Validation Workflows + needs: sync-development-to-nightly + if: needs.sync-development-to-nightly.outputs.has_changes == 'true' + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + steps: + - name: Dispatch Missing Nightly Validation Workflows + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + + const { data: nightlyBranch } = await github.rest.repos.getBranch({ + owner, + repo, + branch: 'nightly', + }); + const nightlyHeadSha = nightlyBranch.commit.sha; + core.info(`Current nightly HEAD: ${nightlyHeadSha}`); + + const workflows = [ + { id: 'quality-checks.yml' }, + { id: 'e2e-tests-split.yml' }, + { id: 'codecov-upload.yml', inputs: { run_backend: 'true', run_frontend: 'true' } }, + { id: 'security-pr.yml' }, + { id: 'supply-chain-pr.yml' }, + { id: 'codeql.yml' }, + ]; + + for (const workflow of workflows) { + const { data: workflowRuns } = await github.rest.actions.listWorkflowRuns({ + owner, + repo, + workflow_id: workflow.id, + branch: 'nightly', + per_page: 50, + }); + + const hasRunForHead = workflowRuns.workflow_runs.some( + (run) => run.head_sha === nightlyHeadSha, + ); + + if (hasRunForHead) { + core.info(`Skipping dispatch for ${workflow.id}; run already exists for nightly HEAD`); + continue; + } + + await github.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id: workflow.id, + ref: 'nightly', + ...(workflow.inputs ? { inputs: workflow.inputs } : {}), + }); + core.info(`Dispatched ${workflow.id} on nightly (missing run for HEAD)`); + } + build-and-push-nightly: needs: sync-development-to-nightly runs-on: ubuntu-latest @@ -285,7 +346,7 @@ jobs: output: 'trivy-nightly.sarif' - name: Upload Trivy results - uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/upload-sarif@015d8c7cbcbb8e7252a7dccfe81a90aa176260b2 # v4.32.3 with: sarif_file: 'trivy-nightly.sarif' category: 'trivy-nightly' diff --git a/.github/workflows/quality-checks.yml b/.github/workflows/quality-checks.yml index 935ca38a..a77c0a8d 100644 --- a/.github/workflows/quality-checks.yml +++ b/.github/workflows/quality-checks.yml @@ -3,6 +3,7 @@ name: Quality Checks on: pull_request: push: + workflow_dispatch: concurrency: diff --git a/.github/workflows/security-weekly-rebuild.yml b/.github/workflows/security-weekly-rebuild.yml index bfb3f825..fd01495a 100644 --- a/.github/workflows/security-weekly-rebuild.yml +++ b/.github/workflows/security-weekly-rebuild.yml @@ -106,7 +106,7 @@ jobs: severity: 'CRITICAL,HIGH,MEDIUM' - name: Upload Trivy results to GitHub Security - uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + uses: github/codeql-action/upload-sarif@015d8c7cbcbb8e7252a7dccfe81a90aa176260b2 # v4.32.3 with: sarif_file: 'trivy-weekly-results.sarif' diff --git a/.github/workflows/supply-chain-pr.yml b/.github/workflows/supply-chain-pr.yml index a61c2347..cb68221c 100644 --- a/.github/workflows/supply-chain-pr.yml +++ b/.github/workflows/supply-chain-pr.yml @@ -339,7 +339,7 @@ jobs: - name: Upload SARIF to GitHub Security if: steps.check-artifact.outputs.artifact_found == 'true' - uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4 + uses: github/codeql-action/upload-sarif@015d8c7cbcbb8e7252a7dccfe81a90aa176260b2 # v4 continue-on-error: true with: sarif_file: grype-results.sarif diff --git a/.github/workflows/weekly-nightly-promotion.yml b/.github/workflows/weekly-nightly-promotion.yml index 14b482db..a3b79afc 100644 --- a/.github/workflows/weekly-nightly-promotion.yml +++ b/.github/workflows/weekly-nightly-promotion.yml @@ -5,8 +5,9 @@ name: Weekly Nightly to Main Promotion on: schedule: - # Every Monday at 09:00 UTC (4am EST / 5am EDT) - - cron: '0 9 * * 1' + # Every Monday at 10:30 UTC (5:30am EST / 6:30am EDT) + # Offset from nightly sync (09:00 UTC) to avoid schedule race and allow validation completion. + - cron: '30 10 * * 1' workflow_dispatch: inputs: reason: @@ -61,40 +62,126 @@ jobs: core.info('Checking nightly branch workflow health...'); - // Get the latest workflow runs on the nightly branch - const { data: runs } = await github.rest.actions.listWorkflowRunsForRepo({ + // Resolve current nightly HEAD SHA and evaluate workflow health for that exact commit. + // This prevents stale failures from older nightly runs from blocking promotion. + const { data: nightlyBranch } = await github.rest.repos.getBranch({ owner: context.repo.owner, repo: context.repo.repo, branch: 'nightly', - status: 'completed', - per_page: 10, }); + const nightlyHeadSha = nightlyBranch.commit.sha; + core.info(`Current nightly HEAD: ${nightlyHeadSha}`); - if (runs.workflow_runs.length === 0) { - core.setOutput('is_healthy', 'true'); + // Check critical workflows on the current nightly HEAD only. + // Nightly build itself is scheduler-driven and not a reliable per-commit gate. + const criticalWorkflows = [ + { + workflowFile: 'quality-checks.yml', + fallbackNames: ['Quality Checks'], + }, + { + workflowFile: 'e2e-tests-split.yml', + fallbackNames: ['E2E Tests'], + }, + { + workflowFile: 'codeql.yml', + fallbackNames: ['CodeQL - Analyze'], + }, + ]; + + // Retry window to avoid race conditions where required checks are not yet materialized. + const maxAttempts = 6; + const waitMs = 20000; + + let branchRuns = []; + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + const { data: completedRuns } = await github.rest.actions.listWorkflowRunsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + branch: 'nightly', + status: 'completed', + per_page: 100, + }); + + branchRuns = completedRuns.workflow_runs; + + const allWorkflowsPresentForHead = criticalWorkflows.every((workflow) => { + const workflowPath = `.github/workflows/${workflow.workflowFile}`; + return branchRuns.some( + (r) => + r.head_sha === nightlyHeadSha && + ( + r.path === workflowPath || + (typeof r.path === 'string' && r.path.endsWith(`/${workflowPath}`)) || + workflow.fallbackNames.includes(r.name) + ), + ); + }); + + if (allWorkflowsPresentForHead) { + core.info(`Required workflow runs found for nightly HEAD on attempt ${attempt}`); + break; + } + + if (attempt < maxAttempts) { + core.info( + `Waiting for required runs to appear for nightly HEAD (attempt ${attempt}/${maxAttempts})`, + ); + await new Promise((resolve) => setTimeout(resolve, waitMs)); + } + } + + if (branchRuns.length === 0) { + core.setOutput('is_healthy', 'false'); core.setOutput('latest_run_url', 'No completed runs found'); - core.setOutput('failure_reason', ''); - core.info('No completed workflow runs found on nightly - proceeding'); + core.setOutput('failure_reason', 'No completed workflow runs found on nightly'); + core.warning('No completed workflow runs found on nightly - blocking promotion'); return; } - // Check the most recent critical workflows - const criticalWorkflows = ['Nightly Build & Package', 'Quality Checks', 'E2E Tests']; - const recentRuns = runs.workflow_runs.slice(0, 10); - let hasFailure = false; let failureReason = ''; - let latestRunUrl = recentRuns[0]?.html_url || 'N/A'; + let latestRunUrl = branchRuns[0]?.html_url || 'N/A'; - for (const workflowName of criticalWorkflows) { - const latestRun = recentRuns.find(r => r.name === workflowName); - if (latestRun && latestRun.conclusion === 'failure') { + for (const workflow of criticalWorkflows) { + const workflowPath = `.github/workflows/${workflow.workflowFile}`; + core.info( + `Evaluating required workflow ${workflow.workflowFile} (path match first, names fallback: ${workflow.fallbackNames.join(', ')})`, + ); + + const latestRunForHead = branchRuns.find( + (r) => + r.head_sha === nightlyHeadSha && + ( + r.path === workflowPath || + (typeof r.path === 'string' && r.path.endsWith(`/${workflowPath}`)) || + workflow.fallbackNames.includes(r.name) + ), + ); + + if (!latestRunForHead) { hasFailure = true; - failureReason = `${workflowName} failed (${latestRun.html_url})`; - latestRunUrl = latestRun.html_url; - core.warning(`Critical workflow "${workflowName}" has failed`); + failureReason = `${workflow.workflowFile} has no completed run for nightly HEAD ${nightlyHeadSha}`; + latestRunUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/workflows/${workflow.workflowFile}`; + core.warning( + `Required workflow ${workflow.workflowFile} has no completed run for current nightly HEAD`, + ); break; } + + if (latestRunForHead.conclusion !== 'success') { + hasFailure = true; + failureReason = `${workflow.workflowFile} ${latestRunForHead.conclusion} (${latestRunForHead.html_url})`; + latestRunUrl = latestRunForHead.html_url; + core.warning( + `Required workflow ${workflow.workflowFile} is ${latestRunForHead.conclusion} on nightly HEAD`, + ); + break; + } + + core.info( + `Required workflow ${workflow.workflowFile} passed for nightly HEAD via run ${latestRunForHead.id}`, + ); } core.setOutput('is_healthy', hasFailure ? 'false' : 'true'); @@ -328,9 +415,67 @@ jobs: core.setOutput('pr_number', prNumber); core.setOutput('pr_url', '${{ steps.existing-pr.outputs.pr_url }}'); + trigger-required-checks: + name: Trigger Missing Required Checks + needs: create-promotion-pr + if: needs.create-promotion-pr.outputs.skipped != 'true' + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + steps: + - name: Dispatch missing required workflows on nightly head + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + + const { data: nightlyBranch } = await github.rest.repos.getBranch({ + owner, + repo, + branch: 'nightly', + }); + const nightlyHeadSha = nightlyBranch.commit.sha; + core.info(`Current nightly HEAD for dispatch fallback: ${nightlyHeadSha}`); + + const requiredWorkflows = [ + { id: 'quality-checks.yml' }, + { id: 'e2e-tests-split.yml' }, + { id: 'codeql.yml' }, + { id: 'codecov-upload.yml', inputs: { run_backend: 'true', run_frontend: 'true' } }, + { id: 'security-pr.yml' }, + { id: 'supply-chain-pr.yml' }, + ]; + + for (const workflow of requiredWorkflows) { + const { data: runs } = await github.rest.actions.listWorkflowRuns({ + owner, + repo, + workflow_id: workflow.id, + branch: 'nightly', + per_page: 50, + }); + + const hasRunForHead = runs.workflow_runs.some((run) => run.head_sha === nightlyHeadSha); + if (hasRunForHead) { + core.info(`Skipping ${workflow.id}; run already exists for nightly HEAD`); + continue; + } + + await github.rest.actions.createWorkflowDispatch({ + owner, + repo, + workflow_id: workflow.id, + ref: 'nightly', + ...(workflow.inputs ? { inputs: workflow.inputs } : {}), + }); + core.info(`Dispatched ${workflow.id}; missing for nightly HEAD`); + } + notify-on-failure: name: Notify on Failure - needs: [check-nightly-health, create-promotion-pr] + needs: [check-nightly-health, create-promotion-pr, trigger-required-checks] runs-on: ubuntu-latest if: | always() && @@ -445,7 +590,7 @@ jobs: summary: name: Workflow Summary - needs: [check-nightly-health, create-promotion-pr] + needs: [check-nightly-health, create-promotion-pr, trigger-required-checks] runs-on: ubuntu-latest if: always()