diff --git a/.github/scripts/stage-codecov.sh b/.github/scripts/stage-codecov.sh new file mode 100644 index 00000000000..431993aad13 --- /dev/null +++ b/.github/scripts/stage-codecov.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Stage coverage + test-result reports for the deferred Codecov upload +# (.github/workflows/codecov-upload.yml). Fork PRs get no CODECOV_TOKEN on the +# pull_request event, so the build stages its reports as a `codecov-` +# artifact instead of uploading inline; the workflow_run job re-uploads them with +# a token (see codecov-upload.yml / #6685). +# +# Produces ./cc/coverage/ (coverage reports), ./cc/results/ (JUnit XMLs), and the +# pr/sha/branch identifier files. Directory structure is preserved so same-basename +# reports (e.g. amber's per-module jacoco.xml across ~8 modules) do not collide; +# the deferred job points `directory:` at cc/coverage and cc/results so Codecov's +# recursive search finds every file under each. +# +# Inputs (env): +# CC_COVERAGE space-separated list of coverage sources — each a literal file or +# a `find -path` glob (use * which matches across /), e.g. +# "*/target/scala-2.13/jacoco/report/jacoco.xml". May be empty for a +# test-results-only flag. +# CC_RESULTS same, for JUnit test-result XMLs. +# CC_PR PR number (empty on push builds) +# CC_SHA head commit sha +# CC_BRANCH head branch +set -uo pipefail + +mkdir -p cc/coverage cc/results + +copy_into() { + dest="$1"; shift + for src in "$@"; do + if [ -e "$src" ]; then + # literal existing path + cp --parents "$src" "$dest/" 2>/dev/null || cp "$src" "$dest/" 2>/dev/null || true + else + # treat as a find -path glob (matches across / since find's * does) + find . -path "./$src" -o -path "$src" 2>/dev/null \ + | while IFS= read -r f; do + [ -f "$f" ] && cp --parents "$f" "$dest/" 2>/dev/null || true + done + fi + done +} + +# shellcheck disable=SC2086 # intentional word-splitting into separate globs +copy_into cc/coverage ${CC_COVERAGE:-} +# shellcheck disable=SC2086 +copy_into cc/results ${CC_RESULTS:-} + +printf '%s' "${CC_PR:-}" > cc/pr-number.txt +printf '%s' "${CC_SHA:-}" > cc/commit-sha.txt +printf '%s' "${CC_BRANCH:-}" > cc/branch.txt + +echo "Staged coverage files:" +find cc/coverage -type f 2>/dev/null | sed 's/^/ /' || true +echo "Staged result files:" +find cc/results -type f 2>/dev/null | sed 's/^/ /' || true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1355a4dbbce..34eeff6ae3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -137,44 +137,36 @@ jobs: run: ./bin/licensing/check_binary_deps.py ${{ inputs.mode == 'PR' && '--ignore-transitive-version' || '' }} npm frontend/dist/3rdpartylicenses.json - name: Run frontend unit tests run: yarn --cwd frontend run test:ci - - name: Upload frontend coverage to Codecov - if: matrix.os == 'ubuntu-latest' && always() - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./frontend/coverage/**/lcov.info - flags: frontend - fail_ci_if_error: false - - name: Upload frontend unit test results to Codecov - # vitest.config.ts adds a `junit` reporter that writes to junit.xml - # in the working dir; the @angular/build:unit-test runner forwards - # vitest config through unchanged. - if: matrix.os == 'ubuntu-latest' && !cancelled() - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./frontend/junit.xml - flags: frontend - report_type: test_results - disable_search: true - fail_ci_if_error: false - name: Install Playwright Chromium run: yarn --cwd frontend playwright install ${{ matrix.os == 'ubuntu-latest' && '--with-deps' || '' }} chromium - name: Run frontend browser-mode tests run: yarn --cwd frontend ng run gui:test-browser - - name: Upload frontend browser-mode test results to Codecov - # vitest.browser.config.ts emits junit-browser.xml (distinct from - # the unit-test report). Same `frontend` flag — Codecov merges - # multi-file uploads under one flag. - if: matrix.os == 'ubuntu-latest' && !cancelled() - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + # Codecov upload is deferred to codecov-upload.yml (workflow_run) so fork PRs + # get authenticated uploads and a reliable PR comment — see that file and + # #6685. Stage this flag's reports as the `codecov-frontend` artifact instead + # of uploading inline. The `!cancelled()` guard is a status-check function, so + # it overrides the implicit success() and staging still runs when the unit or + # browser tests fail (matching the old always()/!cancelled() steps); it only + # skips a cancelled run. Skipped on backport builds (different ref + would + # collide on the artifact name across the target matrix). + - name: Stage frontend coverage for deferred Codecov upload + if: matrix.os == 'ubuntu-latest' && inputs.backport_target_branch == '' && !cancelled() + shell: bash + env: + CC_COVERAGE: frontend/coverage/gui/lcov.info + CC_RESULTS: frontend/junit.xml frontend/junit-browser.xml + CC_PR: ${{ github.event.pull_request.number }} + CC_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} + run: bash .github/scripts/stage-codecov.sh + - name: Upload frontend Codecov artifact + if: matrix.os == 'ubuntu-latest' && inputs.backport_target_branch == '' && !cancelled() + uses: actions/upload-artifact@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./frontend/junit-browser.xml - flags: frontend - report_type: test_results - disable_search: true - fail_ci_if_error: false + name: codecov-frontend + path: cc/ + retention-days: 1 + if-no-files-found: warn amber: # The amber job runs the cross-cutting Scala lints (scalafmtCheckAll, @@ -300,28 +292,29 @@ jobs: "WorkflowCore/jacoco" \ "WorkflowOperator/jacoco" \ "WorkflowExecutionService/jacoco" - - name: Upload amber and common coverage to Codecov - if: always() - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./**/target/scala-2.13/jacoco/report/jacoco.xml - flags: amber - fail_ci_if_error: false - - name: Upload amber and common test results to Codecov - # ScalaTest writes one JUnit-XML per spec under each module's - # target/test-reports/ (configured ThisBuild in build.sbt). Glob - # picks them up from every module that ran in the jacoco invocation - # above. `!cancelled()` so test failures still upload (the point - # of Test Analytics). - if: ${{ !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + - name: Stage amber and common coverage for deferred Codecov upload + # Deferred to codecov-upload.yml (workflow_run) so fork PRs get an + # authenticated upload — see #6685. Multi-module globs: the staging script + # preserves the directory tree so the ~8 same-basename jacoco.xml (and the + # per-spec test-reports) don't collide when flattened. `!cancelled()` keeps + # staging on test failure; skipped on backport builds (artifact-name clash). + if: inputs.backport_target_branch == '' && !cancelled() + shell: bash + env: + CC_COVERAGE: "*/target/scala-2.13/jacoco/report/jacoco.xml" + CC_RESULTS: "*/target/test-reports/*.xml" + CC_PR: ${{ github.event.pull_request.number }} + CC_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} + run: bash .github/scripts/stage-codecov.sh + - name: Upload amber Codecov artifact + if: inputs.backport_target_branch == '' && !cancelled() + uses: actions/upload-artifact@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./**/target/test-reports/*.xml - flags: amber - report_type: test_results - fail_ci_if_error: false + name: codecov-amber + path: cc/ + retention-days: 1 + if-no-files-found: warn amber-integration: # Runs Scala tests tagged @org.apache.texera.amber.tags.IntegrationTest — @@ -656,18 +649,30 @@ jobs: # --junit-xml feeds the Test Analytics upload below. run: | cd amber && pytest -m integration --junit-xml=junit-integration.xml -sv - - name: Upload amber integration test results to Codecov - # Two separate uploads because the ScalaTest and pytest runs each - # produce their own JUnit-XMLs and Codecov keys uploads by flag. - # `!cancelled()` so test failures still upload. - if: ${{ !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + - name: Stage amber-integration test results for deferred Codecov upload + # Deferred to codecov-upload.yml (workflow_run) so fork PRs get an + # authenticated upload — see #6685. Results-only flag (no jacoco). Staged + # on the ubuntu leg only: the integration specs run on both ubuntu and + # macOS, so staging both would clash on the `codecov-amber-integration` + # artifact name (upload-artifact@v4 rejects duplicates); the ubuntu run is + # representative for Test Analytics. Skipped on backport builds. + if: matrix.os == 'ubuntu-latest' && inputs.backport_target_branch == '' && !cancelled() + shell: bash + env: + CC_COVERAGE: "" + CC_RESULTS: "*/target/test-reports/*.xml amber/junit-integration.xml" + CC_PR: ${{ github.event.pull_request.number }} + CC_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} + run: bash .github/scripts/stage-codecov.sh + - name: Upload amber-integration Codecov artifact + if: matrix.os == 'ubuntu-latest' && inputs.backport_target_branch == '' && !cancelled() + uses: actions/upload-artifact@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./**/target/test-reports/*.xml,./amber/junit-integration.xml - flags: amber-integration - report_type: test_results - fail_ci_if_error: false + name: codecov-amber-integration + path: cc/ + retention-days: 1 + if-no-files-found: warn platform: # Per-service build, test, and license check for the non-amber Scala @@ -768,27 +773,30 @@ jobs: check_exit=1 fi exit "$check_exit" - - name: Upload ${{ matrix.service }} coverage to Codecov - # Per-service flag so each matrix entry has its own Codecov view - # rather than being merged into one umbrella `platform` flag. - if: always() - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./${{ matrix.service }}/target/scala-2.13/jacoco/report/jacoco.xml - flags: ${{ matrix.service }} - fail_ci_if_error: false - - name: Upload ${{ matrix.service }} test results to Codecov - # Per-service Test Analytics, mirroring the coverage upload flag. - if: ${{ !cancelled() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + - name: Stage ${{ matrix.service }} coverage for deferred Codecov upload + # Deferred to codecov-upload.yml (workflow_run) so fork PRs get an + # authenticated upload — see #6685. Per-service flag. Skipped on backport + # builds (artifact-name clash across the target matrix). + if: inputs.backport_target_branch == '' && !cancelled() + shell: bash + env: + CC_COVERAGE: "${{ matrix.service }}/target/scala-2.13/jacoco/report/jacoco.xml" + CC_RESULTS: "${{ matrix.service }}/target/test-reports/*.xml" + CC_PR: ${{ github.event.pull_request.number }} + CC_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} + run: bash .github/scripts/stage-codecov.sh + - name: Upload ${{ matrix.service }} Codecov artifact + # Adding a service to the matrix above also needs a matching + # { flag: , coverage: true } row in codecov-upload.yml, or this + # artifact is downloaded by nothing and the service's coverage is dropped. + if: inputs.backport_target_branch == '' && !cancelled() + uses: actions/upload-artifact@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./${{ matrix.service }}/target/test-reports/*.xml - flags: ${{ matrix.service }} - report_type: test_results - disable_search: true - fail_ci_if_error: false + name: codecov-${{ matrix.service }} + path: cc/ + retention-days: 1 + if-no-files-found: warn platform-integration: # Boot smoke test for the platform services (mirrors amber-integration: an @@ -1015,28 +1023,27 @@ jobs: # test PR comments and flaky-test detection on main. run: | cd amber && pytest -m "not integration" --cov=src/main/python --cov-report=xml --junit-xml=junit.xml -sv - - name: Upload pyamber coverage to Codecov - if: matrix.python-version == '3.12' && always() - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./amber/coverage.xml - flags: pyamber - fail_ci_if_error: false - - name: Upload pyamber test results to Codecov - # Test Analytics ingestion. Runs on the same 3.12 leg that uploads - # coverage to keep one canonical source per flag. `!cancelled()` - # rather than `always()` so we still upload on test failure (the - # whole point of Test Analytics) but skip cancelled runs. - if: matrix.python-version == '3.12' && !cancelled() - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + - name: Stage pyamber coverage for deferred Codecov upload + # Deferred to codecov-upload.yml (workflow_run) so fork PRs get an + # authenticated upload — see #6685. 3.12 leg only (the leg that produced + # coverage.xml / the pip-licenses snapshot). Skipped on backport builds. + if: matrix.python-version == '3.12' && inputs.backport_target_branch == '' && !cancelled() + shell: bash + env: + CC_COVERAGE: amber/coverage.xml + CC_RESULTS: amber/junit.xml + CC_PR: ${{ github.event.pull_request.number }} + CC_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} + run: bash .github/scripts/stage-codecov.sh + - name: Upload pyamber Codecov artifact + if: matrix.python-version == '3.12' && inputs.backport_target_branch == '' && !cancelled() + uses: actions/upload-artifact@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./amber/junit.xml - flags: pyamber - report_type: test_results - disable_search: true - fail_ci_if_error: false + name: codecov-pyamber + path: cc/ + retention-days: 1 + if-no-files-found: warn agent-service: if: ${{ inputs.run_agent_service }} @@ -1085,26 +1092,29 @@ jobs: # Test Analytics upload below can feed Codecov's failing-test PR # comments and flaky-test detection on main. run: bun test --coverage --coverage-reporter=lcov --reporter=junit --reporter-outfile=junit.xml - - name: Upload agent-service coverage to Codecov - if: matrix.os == 'ubuntu-latest' && always() - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./agent-service/coverage/lcov.info - flags: agent-service - fail_ci_if_error: false - - name: Upload agent-service test results to Codecov - # Test Analytics ingestion. Runs on the same ubuntu leg that - # uploads coverage. `!cancelled()` so test failures still upload. - if: matrix.os == 'ubuntu-latest' && !cancelled() - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + - name: Stage agent-service coverage for deferred Codecov upload + # Deferred to codecov-upload.yml (workflow_run) so fork PRs get an + # authenticated upload — see #6685. ubuntu leg only. working-directory is + # pinned to the workspace root (this job otherwise defaults to + # agent-service/) so the staged cc/ lands where the artifact upload expects. + if: matrix.os == 'ubuntu-latest' && inputs.backport_target_branch == '' && !cancelled() + shell: bash + working-directory: ${{ github.workspace }} + env: + CC_COVERAGE: agent-service/coverage/lcov.info + CC_RESULTS: agent-service/junit.xml + CC_PR: ${{ github.event.pull_request.number }} + CC_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CC_BRANCH: ${{ github.event.pull_request.head.ref || github.ref_name }} + run: bash .github/scripts/stage-codecov.sh + - name: Upload agent-service Codecov artifact + if: matrix.os == 'ubuntu-latest' && inputs.backport_target_branch == '' && !cancelled() + uses: actions/upload-artifact@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./agent-service/junit.xml - flags: agent-service - report_type: test_results - disable_search: true - fail_ci_if_error: false + name: codecov-agent-service + path: cc/ + retention-days: 1 + if-no-files-found: warn infra: # Generic project-tooling job — runs the lightweight (no docker, no diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml new file mode 100644 index 00000000000..b43e22906e9 --- /dev/null +++ b/.github/workflows/codecov-upload.yml @@ -0,0 +1,213 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Deferred, authenticated Codecov upload for coverage produced by "Required Checks". +# +# Why this exists (reported on #6685): a PR from a fork never receives +# secrets.CODECOV_TOKEN — the build runs on the `pull_request` event and GitHub +# withholds secrets from fork runs. An inline upload from the build therefore uses +# Codecov's tokenless path, which is rate-limited; under a burst of fork-PR uploads +# Codecov processes the report but silently drops the PR comment. This workflow runs +# on `workflow_run`, in the base-repo context where the token IS available, and +# re-uploads the staged coverage/test-results artifacts with authentication so the +# comment posts reliably even in bursts. +# +# Each build job stages its reports as a `codecov-` artifact with a uniform +# layout — coverage/ (coverage reports) and results/ (JUnit XMLs) plus pr-number / +# commit-sha / branch identifier files. The matrix below re-uploads each flag with +# `directory:` scoping so Codecov's search is confined to that flag's files (this is +# what lets the amber flag's ~8 same-basename module jacoco.xml reports upload without +# a fragile explicit file list). Codecov requires one upload per flag (flags in a +# single upload apply to all its files, and carryforward needs the flag at upload +# time), so the flags cannot be collapsed into fewer calls — the win here is the +# token, not fewer uploads. +# +# NOTE: `workflow_run` only fires from the copy of this file on the default branch, so +# this has NO effect until merged to main. After merge, exercise it manually via the +# workflow_dispatch entry below, passing a finished "Required Checks" run id. + +name: Codecov Upload + +on: + workflow_run: + workflows: ["Required Checks"] + types: + - completed + workflow_dispatch: + inputs: + run_id: + description: "Finished 'Required Checks' run id to pull the codecov-* artifacts from" + required: true + type: string + +permissions: + contents: read + actions: read # download-artifact needs this to read artifacts from another run + +jobs: + upload: + # Skip only a cancelled/skipped upstream; still run on failure so partial + # coverage is reported (mirrors the build's always()/!cancelled() staging). + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion != 'cancelled' && + github.event.workflow_run.conclusion != 'skipped') + runs-on: ubuntu-latest + # Job-level so the group can key on matrix.flag: one upload pass per source + # run + flag, and a re-run of the source supersedes the previous attempt. + concurrency: + group: codecov-upload-${{ github.event.workflow_run.id || inputs.run_id }}-${{ matrix.flag }} + cancel-in-progress: false + strategy: + fail-fast: false + matrix: + # flag = artifact suffix (codecov-); coverage=false for the + # test-results-only flag (amber-integration has no coverage report). + # + # KEEP IN SYNC with the coverage-producing jobs in build.yml: one row per + # codecov- artifact staged there — frontend, amber, amber-integration, + # pyamber, agent-service, and every service in build.yml's `platform` matrix. + # A flag staged in build.yml but missing here is downloaded by nothing and + # its coverage is silently dropped (the reverse — a row here with no artifact + # — is safe: the download continue-on-errors and the leg skips). + include: + - { flag: frontend, coverage: true } + - { flag: amber, coverage: true } + - { flag: amber-integration, coverage: false } + - { flag: pyamber, coverage: true } + - { flag: agent-service, coverage: true } + - { flag: config-service, coverage: true } + - { flag: access-control-service, coverage: true } + - { flag: file-service, coverage: true } + - { flag: computing-unit-managing-service, coverage: true } + - { flag: workflow-compiling-service, coverage: true } + - { flag: notebook-migration-service, coverage: true } + steps: + - name: Resolve source run id + id: src + shell: bash + run: echo "run_id=${{ github.event.workflow_run.id || inputs.run_id }}" >> "$GITHUB_OUTPUT" + - name: Download staged coverage + id: dl + continue-on-error: true # absent when this flag's job was label-gated out + # Layout: stage-codecov.sh roots the artifact at cc/ (coverage/, results/, + # and the pr-number/commit-sha/branch txt files), so downloading to path: cc + # restores cc/coverage/**, cc/results/**, and cc/pr-number.txt — the paths + # the steps below reference. Keep that rooting if the staging script changes. + uses: actions/download-artifact@v4 + with: + name: codecov-${{ matrix.flag }} + path: cc + run-id: ${{ steps.src.outputs.run_id }} + github-token: ${{ github.token }} + - name: Read coverage metadata + if: steps.dl.outcome == 'success' + id: meta + shell: bash + # Artifact content comes from a (possibly fork) PR build, so sanitize each + # value to its expected charset before handing it to the Codecov CLI. + run: | + echo "pr=$(cat cc/pr-number.txt 2>/dev/null | tr -cd '0-9')" >> "$GITHUB_OUTPUT" + echo "sha=$(cat cc/commit-sha.txt 2>/dev/null | tr -cd '0-9a-fA-F')" >> "$GITHUB_OUTPUT" + echo "branch=$(cat cc/branch.txt 2>/dev/null | tr -cd '[:alnum:]._/:-')" >> "$GITHUB_OUTPUT" + - name: Upload ${{ matrix.flag }} coverage to Codecov + # fail_ci_if_error: true so a genuine upload failure fails this leg and + # trips the notify-failure job below (Codecov posts its own comment only on + # success, so a failed coverage upload would otherwise leave the PR with no + # report and no signal). A label-gated-absent flag is not a failure — the + # download continue-on-errors and this step is skipped via the guards above. + if: steps.dl.outcome == 'success' && matrix.coverage && hashFiles('cc/coverage/**') != '' + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + directory: cc/coverage + flags: ${{ matrix.flag }} + fail_ci_if_error: true + override_commit: ${{ steps.meta.outputs.sha }} + override_branch: ${{ steps.meta.outputs.branch }} + override_pr: ${{ steps.meta.outputs.pr }} + - name: Upload ${{ matrix.flag }} test results to Codecov + # fail_ci_if_error: true here too (per review) so a test-results upload error + # also trips notify-failure instead of being silently dropped. Still guarded + # on hashFiles, so an absent / label-gated flag simply skips. + if: steps.dl.outcome == 'success' && hashFiles('cc/results/**') != '' + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + directory: cc/results + flags: ${{ matrix.flag }} + report_type: test_results + fail_ci_if_error: true + override_commit: ${{ steps.meta.outputs.sha }} + override_branch: ${{ steps.meta.outputs.branch }} + override_pr: ${{ steps.meta.outputs.pr }} + + notify-failure: + # If any upload above failed (coverage or test-results), leave a comment on the + # PR so a human can re-dispatch it. Codecov posts its own comment on SUCCESS, so + # we only cover the failure case (per #6730 review). A delayed / queued upload + # posts nothing — the absence of a Codecov comment then signals a stuck run. + needs: upload + if: ${{ failure() && github.event_name == 'workflow_run' }} + runs-on: ubuntu-latest + permissions: + contents: read + actions: read # download-artifact from the source run + pull-requests: write # comment on the PR + steps: + - name: Download coverage metadata + id: dl + continue-on-error: true + uses: actions/download-artifact@v4 + with: + pattern: codecov-* + path: cc-all + merge-multiple: true # every flag's artifact carries the same pr-number.txt + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + - name: Resolve PR number + id: meta + shell: bash + run: echo "pr=$(cat cc-all/pr-number.txt 2>/dev/null | tr -cd '0-9')" >> "$GITHUB_OUTPUT" + - name: Comment upload failure on the PR + if: steps.meta.outputs.pr != '' + uses: actions/github-script@v9 + with: + github-token: ${{ github.token }} + script: | + const MARKER = ''; + const pr = Number('${{ steps.meta.outputs.pr }}'); + const srcRunId = '${{ github.event.workflow_run.id }}'; + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const body = [ + MARKER, + '⚠️ **Codecov upload failed.**', + '', + `The deferred \`Codecov Upload\` workflow ([run](${runUrl})) failed, so the coverage report for this PR may be missing. This does **not** block merge.`, + '', + 'To recover: re-run the failed jobs in the run above, or manually dispatch the **Codecov Upload** workflow with', + `\`run_id\`: \`${srcRunId}\`.`, + ].join('\n'); + const { owner, repo } = context.repo; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, repo, issue_number: pr, per_page: 100, + }); + const prior = comments.find(c => c.body && c.body.includes(MARKER)); + if (prior) { + await github.rest.issues.updateComment({ owner, repo, comment_id: prior.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number: pr, body }); + }