From 11bb7685b82a50c78f06d77832f1ad5eca839587 Mon Sep 17 00:00:00 2001 From: Metin Cakircali Date: Mon, 27 Jul 2026 10:52:34 +0200 Subject: [PATCH 1/2] feat(ci): add triage --- .github/workflows/ci.yml | 19 +++++ .github/workflows/downstream-triage.yml | 94 +++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 .github/workflows/downstream-triage.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ef9b327d..7a2922730 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,25 @@ 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: + 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..0f0016d78 --- /dev/null +++ b/.github/workflows/downstream-triage.yml @@ -0,0 +1,94 @@ +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: + 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.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 checks = await github.paginate(github.rest.checks.listForRef, { + owner, + repo, + ref, + per_page: 100, + }); + + const failedConclusions = new Set(['failure', 'timed_out', 'cancelled', 'startup_failure']); + // exclude helper checks + const excludedNames = new Set(['downstream-triage', 'notify']); + const failed = checks.filter((c) => { + if (!failedConclusions.has(c.conclusion)) { + return false; + } + if (excludedNames.has(c.name)) { + return false; + } + return true; + }); + + const header = '### Failed Downstream Runs'; + if (failed.length === 0) { + await core.summary + .addRaw(`${header}\n\nNo failed runs found.\n\n`) + .write(); + return; + } + + // write up to 30 failed runs to the summary + const lines = failed.slice(0, 30).map((c) => { + const url = c.html_url || `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; + return `- [${c.name}](${url}) (${c.conclusion})`; + }); + + await core.summary + .addRaw(`${header}\n\n${lines.join('\n')}\n\n`) + .write(); From 9376e8c23c014c1da1325b5923514ced8fd4846f Mon Sep 17 00:00:00 2001 From: Metin Cakircali Date: Mon, 27 Jul 2026 13:38:59 +0200 Subject: [PATCH 2/2] feat(ci): check fails triage --- .github/workflows/ci.yml | 1 + .github/workflows/downstream-triage.yml | 59 ++++++++++++++++++++----- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a2922730..0601623ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,7 @@ jobs: 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: diff --git a/.github/workflows/downstream-triage.yml b/.github/workflows/downstream-triage.yml index 0f0016d78..3550180d2 100644 --- a/.github/workflows/downstream-triage.yml +++ b/.github/workflows/downstream-triage.yml @@ -24,6 +24,7 @@ jobs: name: downstream-triage runs-on: ubuntu-latest permissions: + actions: read checks: read contents: read steps: @@ -47,13 +48,39 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" - name: List failed runs - if: ${{ inputs.downstream_ci_result == 'failure' || inputs.downstream_ci_hpc_result == 'failure' }} + 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, @@ -62,33 +89,41 @@ jobs: per_page: 100, }); - const failedConclusions = new Set(['failure', 'timed_out', 'cancelled', 'startup_failure']); - // exclude helper checks - const excludedNames = new Set(['downstream-triage', 'notify']); - const failed = checks.filter((c) => { + // 2) Secondary source: failed check-runs for this ref. + const failedChecks = checks.filter((c) => { if (!failedConclusions.has(c.conclusion)) { return false; } - if (excludedNames.has(c.name)) { + if (isExcluded(c.name)) { return false; } return true; }); const header = '### Failed Downstream Runs'; - if (failed.length === 0) { + if (failedJobs.length === 0 && failedChecks.length === 0) { await core.summary - .addRaw(`${header}\n\nNo failed runs found.\n\n`) + .addRaw(`${header}\n\nNo failed runs found in jobs or check-runs.\n\n`) .write(); return; } - // write up to 30 failed runs to the summary - const lines = failed.slice(0, 30).map((c) => { - const url = c.html_url || `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; + 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(`${header}\n\n${lines.join('\n')}\n\n`) + .addRaw(out) .write();