diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ef9b327d..0601623ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,26 @@ jobs: event_type: downstream-ci-hpc payload: '{"fdb": "ecmwf/fdb@${{ github.event.pull_request.head.sha || github.sha }}"}' + downstream-triage: + name: downstream-triage + needs: + - downstream-ci + - private-downstream-ci + - downstream-ci-hpc + - private-downstream-ci-hpc + if: ${{ !github.event.pull_request.draft && always() && !github.event.pull_request.head.repo.fork && (github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci') }} + uses: ./.github/workflows/downstream-triage.yml + permissions: + actions: read + checks: read + contents: read + with: + downstream_ci_result: ${{ needs.downstream-ci.result }} + private_downstream_ci_result: ${{ needs.private-downstream-ci.result }} + downstream_ci_hpc_result: ${{ needs.downstream-ci-hpc.result }} + private_downstream_ci_hpc_result: ${{ needs.private-downstream-ci-hpc.result }} + run_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + notify: runs-on: ubuntu-latest needs: diff --git a/.github/workflows/downstream-triage.yml b/.github/workflows/downstream-triage.yml new file mode 100644 index 000000000..3550180d2 --- /dev/null +++ b/.github/workflows/downstream-triage.yml @@ -0,0 +1,129 @@ +name: downstream-triage + +on: + workflow_call: + inputs: + downstream_ci_result: + required: true + type: string + private_downstream_ci_result: + required: true + type: string + downstream_ci_hpc_result: + required: true + type: string + private_downstream_ci_hpc_result: + required: true + type: string + run_url: + required: true + type: string + +jobs: + downstream-triage: + name: downstream-triage + runs-on: ubuntu-latest + permissions: + actions: read + checks: read + contents: read + steps: + - name: CI Triage + env: + DOWNSTREAM_CI_RESULT: ${{ inputs.downstream_ci_result }} + PRIVATE_DOWNSTREAM_CI_RESULT: ${{ inputs.private_downstream_ci_result }} + DOWNSTREAM_CI_HPC_RESULT: ${{ inputs.downstream_ci_hpc_result }} + PRIVATE_DOWNSTREAM_CI_HPC_RESULT: ${{ inputs.private_downstream_ci_hpc_result }} + RUN_URL: ${{ inputs.run_url }} + run: | + { + echo "## Downstream CI Triage" + echo + echo "- downstream-ci: ${DOWNSTREAM_CI_RESULT}" + echo "- private-downstream-ci: ${PRIVATE_DOWNSTREAM_CI_RESULT}" + echo "- downstream-ci-hpc: ${DOWNSTREAM_CI_HPC_RESULT}" + echo "- private-downstream-ci-hpc: ${PRIVATE_DOWNSTREAM_CI_HPC_RESULT}" + echo + echo "Workflow run: ${RUN_URL}" + } >> "$GITHUB_STEP_SUMMARY" + + - name: List failed runs + if: ${{ inputs.downstream_ci_result == 'failure' || inputs.private_downstream_ci_result == 'failure' || inputs.downstream_ci_hpc_result == 'failure' || inputs.private_downstream_ci_hpc_result == 'failure' }} + uses: actions/github-script@v8 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const ref = context.sha; + const runId = context.runId; + + const failedConclusions = new Set(['failure', 'timed_out', 'cancelled', 'startup_failure']); + const excludedPatterns = ['downstream-triage', 'notify']; + const isExcluded = (name) => { + const n = (name || '').toLowerCase(); + return excludedPatterns.some((p) => n.includes(p)); + }; + + // 1) Primary source: failed jobs in this workflow run. + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { + owner, + repo, + run_id: runId, + per_page: 100, + }); + + const failedJobs = jobs.filter((j) => { + if (!failedConclusions.has(j.conclusion)) { + return false; + } + if (isExcluded(j.name)) { + return false; + } + return true; + }); + + const checks = await github.paginate(github.rest.checks.listForRef, { + owner, + repo, + ref, + per_page: 100, + }); + + // 2) Secondary source: failed check-runs for this ref. + const failedChecks = checks.filter((c) => { + if (!failedConclusions.has(c.conclusion)) { + return false; + } + if (isExcluded(c.name)) { + return false; + } + return true; + }); + + const header = '### Failed Downstream Runs'; + if (failedJobs.length === 0 && failedChecks.length === 0) { + await core.summary + .addRaw(`${header}\n\nNo failed runs found in jobs or check-runs.\n\n`) + .write(); + return; + } + + const jobLines = failedJobs.slice(0, 30).map((j) => { + const url = j.html_url || `${context.serverUrl}/${owner}/${repo}/actions/runs/${runId}`; + return `- [${j.name}](${url}) (${j.conclusion})`; + }); + + const checkLines = failedChecks.slice(0, 30).map((c) => { + const url = c.html_url || `${context.serverUrl}/${owner}/${repo}/actions/runs/${runId}`; + return `- [${c.name}](${url}) (${c.conclusion})`; + }); + + let out = `${header}\n\n`; + out += '#### Failed jobs in this workflow run\n\n'; + out += jobLines.length ? `${jobLines.join('\n')}\n\n` : 'None\n\n'; + out += '#### Failed check-runs on this ref\n\n'; + out += checkLines.length ? `${checkLines.join('\n')}\n\n` : 'None\n\n'; + + await core.summary + .addRaw(out) + .write();