From e24cb25070d7efa105bbe675b311ed3794118e70 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Tue, 21 Jul 2026 18:47:41 -0700 Subject: [PATCH 1/5] ci: defer frontend Codecov upload to workflow_run for reliable fork-PR comments Fork PRs never receive secrets.CODECOV_TOKEN on the pull_request event, so the direct codecov-action step falls back to Codecov's tokenless path. That path is rate-limited: under a burst of fork-PR uploads Codecov processes the report but silently drops the PR comment (reported on #6685). Stage the frontend lcov + JUnit reports as an artifact in build.yml and upload them with a token from a new workflow_run-triggered codecov-upload.yml, which runs in the base-repo context where the token is available. Authenticated uploads are not rate-limited, so the comment posts reliably even in bursts. Scope: frontend flag only (proof of concept). Other flags still upload directly. --- .github/workflows/build.yml | 66 +++++++------- .github/workflows/codecov-upload.yml | 128 +++++++++++++++++++++++++++ 2 files changed, 161 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/codecov-upload.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e0092fa4b6..08cbbf00c15 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -137,44 +137,44 @@ 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 the codecov-upload.yml workflow, which runs + # on workflow_run in the base-repo context. A PR from a fork never receives + # secrets.CODECOV_TOKEN on the pull_request event, so uploading directly from + # here falls back to Codecov's tokenless path; under a burst of fork-PR + # uploads Codecov still processes the report but silently drops the PR comment + # (reported on #6685). Staging the reports as an artifact lets the follow-up workflow + # re-upload them with a token so the comment posts reliably. Skipped on + # backport builds — they check out a different ref and 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 + run: | + mkdir -p frontend/codecov-report + cp frontend/coverage/gui/lcov.info frontend/codecov-report/ 2>/dev/null || true + cp frontend/junit.xml frontend/codecov-report/ 2>/dev/null || true + cp frontend/junit-browser.xml frontend/codecov-report/ 2>/dev/null || true + # Persist the identifiers the deferred upload needs to attach coverage to + # the right pull request: the workflow_run event resolves to the default + # branch and its pull_requests array is empty for forks, and the PR build + # checks out the merge commit (github.sha) while Codecov keys on the PR + # head sha — so stash the head sha explicitly. All three fall back to the + # push-commit values when there is no pull_request (push to main). + echo "${{ github.event.pull_request.number }}" > frontend/codecov-report/pr-number.txt + echo "${{ github.event.pull_request.head.sha || github.sha }}" > frontend/codecov-report/commit-sha.txt + echo "${{ github.event.pull_request.head.ref || github.ref_name }}" > frontend/codecov-report/branch.txt + - name: Upload frontend coverage 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: frontend/codecov-report/ + retention-days: 1 + if-no-files-found: warn amber: # The amber job runs the cross-cutting Scala lints (scalafmtCheckAll, diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml new file mode 100644 index 00000000000..b02e1255e09 --- /dev/null +++ b/.github/workflows/codecov-upload.yml @@ -0,0 +1,128 @@ +# 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. A direct 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 artifact with authentication so the comment posts reliably even in bursts. +# +# Proof of concept: only the `frontend` flag is routed through here for now. build.yml +# stages the frontend lcov + JUnit reports as the `codecov-frontend` artifact; every +# other flag still uploads directly from build.yml. +# +# NOTE: `workflow_run` only fires from the copy of this file on the default branch, so +# this has NO effect until it is 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-frontend artifact from" + required: true + type: string + +permissions: + contents: read + actions: read # download-artifact needs this to read artifacts from another run + +# One upload per source run; a re-run of the source supersedes rather than races. +concurrency: + group: codecov-upload-${{ github.event.workflow_run.id || inputs.run_id }} + cancel-in-progress: false + +jobs: + frontend: + # Skip only a cancelled/skipped upstream; still run on failure so partial + # coverage is reported (mirrors the old always()/!cancelled() upload steps). + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion != 'cancelled' && + github.event.workflow_run.conclusion != 'skipped') + runs-on: ubuntu-latest + 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 frontend coverage + id: dl + continue-on-error: true # absent when the frontend job was label-gated out + uses: actions/download-artifact@v4 + with: + name: codecov-frontend + path: codecov-frontend + run-id: ${{ steps.src.outputs.run_id }} + github-token: ${{ github.token }} + - name: Read coverage metadata + if: steps.dl.outcome == 'success' + id: meta + shell: bash + # The 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 codecov-frontend/pr-number.txt 2>/dev/null | tr -cd '0-9')" >> "$GITHUB_OUTPUT" + echo "sha=$(cat codecov-frontend/commit-sha.txt 2>/dev/null | tr -cd '0-9a-fA-F')" >> "$GITHUB_OUTPUT" + echo "branch=$(cat codecov-frontend/branch.txt 2>/dev/null | tr -cd '[:alnum:]._/:-')" >> "$GITHUB_OUTPUT" + - name: Upload frontend coverage to Codecov + if: steps.dl.outcome == 'success' && hashFiles('codecov-frontend/lcov.info') != '' + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov-frontend/lcov.info + flags: frontend + disable_search: true + fail_ci_if_error: false + override_commit: ${{ steps.meta.outputs.sha }} + override_branch: ${{ steps.meta.outputs.branch }} + override_pr: ${{ steps.meta.outputs.pr }} + - name: Upload frontend unit test results to Codecov + if: steps.dl.outcome == 'success' && hashFiles('codecov-frontend/junit.xml') != '' + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov-frontend/junit.xml + flags: frontend + report_type: test_results + disable_search: true + fail_ci_if_error: false + override_commit: ${{ steps.meta.outputs.sha }} + override_branch: ${{ steps.meta.outputs.branch }} + override_pr: ${{ steps.meta.outputs.pr }} + - name: Upload frontend browser-mode test results to Codecov + if: steps.dl.outcome == 'success' && hashFiles('codecov-frontend/junit-browser.xml') != '' + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov-frontend/junit-browser.xml + flags: frontend + report_type: test_results + disable_search: true + fail_ci_if_error: false + override_commit: ${{ steps.meta.outputs.sha }} + override_branch: ${{ steps.meta.outputs.branch }} + override_pr: ${{ steps.meta.outputs.pr }} From 48efa63f433a2c9025b3529021408622f6293cae Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Tue, 21 Jul 2026 19:11:33 -0700 Subject: [PATCH 2/5] ci: document !cancelled() staging intent for deferred frontend Codecov upload Clarify why the staging/upload-artifact steps use !cancelled() rather than always(): the status-check function overrides the implicit success(), so staging still runs when tests fail (matching the prior always()/!cancelled() upload steps) and only skips cancelled runs. No behavior change. --- .github/workflows/build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08cbbf00c15..d4777d1af77 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -149,7 +149,11 @@ jobs: # (reported on #6685). Staging the reports as an artifact lets the follow-up workflow # re-upload them with a token so the comment posts reliably. Skipped on # backport builds — they check out a different ref and would collide on the - # artifact name across the target matrix. + # artifact name across the target matrix. The `!cancelled()` guard is a + # status-check function, so it overrides the implicit success() and staging + # still runs when the unit/browser tests fail (matching the old + # always()/!cancelled() upload steps); it only skips a cancelled run, where + # the reports would be incomplete anyway. - name: Stage frontend coverage for deferred Codecov upload if: matrix.os == 'ubuntu-latest' && inputs.backport_target_branch == '' && !cancelled() shell: bash From 518be62ca458a0bebc0194aa0e16c3d77aff6f5f Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Wed, 22 Jul 2026 00:52:10 -0700 Subject: [PATCH 3/5] ci: route all Codecov flags through the deferred workflow_run upload Generalize the frontend proof of concept to every flag so fork PRs get an authenticated (non-rate-limited) upload and a reliable Codecov comment across the board, not just for frontend. - build.yml: each coverage job now stages its reports as a codecov- artifact via .github/scripts/stage-codecov.sh instead of uploading inline (amber, amber-integration, platform x6, pyamber, agent-service; frontend moved to the same uniform layout). The script preserves the directory tree so amber's ~8 same-basename module jacoco.xml don't collide. Staging is guarded off backport builds (artifact-name collisions across the target matrix) and, for the dual-OS/multi-leg jobs, pinned to the single leg that produces the reports. - codecov-upload.yml: a per-flag matrix downloads each artifact and re-uploads it with the token, using directory: scoping so Codecov's search finds every file per flag. Coverage uploads fail hard (fail_ci_if_error: true); a new notify-failure job then comments on the PR with a link + the run_id to manually dispatch, since Codecov only self-comments on success. Test-results stay best-effort. Codecov requires one upload per flag (flags apply to all files in an upload and carryforward needs the flag at upload time), so the calls can't be batched into fewer uploads without dropping the per-flag/carryforward model; the token, not batching, is what removes the tokenless rate-limit pressure. --- .github/scripts/stage-codecov.sh | 55 ++++++ .github/workflows/build.yml | 248 ++++++++++++++------------- .github/workflows/codecov-upload.yml | 190 ++++++++++++++------ 3 files changed, 320 insertions(+), 173 deletions(-) create mode 100644 .github/scripts/stage-codecov.sh diff --git a/.github/scripts/stage-codecov.sh b/.github/scripts/stage-codecov.sh new file mode 100644 index 00000000000..9467421c1b9 --- /dev/null +++ b/.github/scripts/stage-codecov.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# 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 d4777d1af77..059e65eb6fa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -141,42 +141,30 @@ jobs: 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 - # Codecov upload is deferred to the codecov-upload.yml workflow, which runs - # on workflow_run in the base-repo context. A PR from a fork never receives - # secrets.CODECOV_TOKEN on the pull_request event, so uploading directly from - # here falls back to Codecov's tokenless path; under a burst of fork-PR - # uploads Codecov still processes the report but silently drops the PR comment - # (reported on #6685). Staging the reports as an artifact lets the follow-up workflow - # re-upload them with a token so the comment posts reliably. Skipped on - # backport builds — they check out a different ref and would collide on the - # artifact name across the target matrix. The `!cancelled()` guard is a - # status-check function, so it overrides the implicit success() and staging - # still runs when the unit/browser tests fail (matching the old - # always()/!cancelled() upload steps); it only skips a cancelled run, where - # the reports would be incomplete anyway. + # 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 - run: | - mkdir -p frontend/codecov-report - cp frontend/coverage/gui/lcov.info frontend/codecov-report/ 2>/dev/null || true - cp frontend/junit.xml frontend/codecov-report/ 2>/dev/null || true - cp frontend/junit-browser.xml frontend/codecov-report/ 2>/dev/null || true - # Persist the identifiers the deferred upload needs to attach coverage to - # the right pull request: the workflow_run event resolves to the default - # branch and its pull_requests array is empty for forks, and the PR build - # checks out the merge commit (github.sha) while Codecov keys on the PR - # head sha — so stash the head sha explicitly. All three fall back to the - # push-commit values when there is no pull_request (push to main). - echo "${{ github.event.pull_request.number }}" > frontend/codecov-report/pr-number.txt - echo "${{ github.event.pull_request.head.sha || github.sha }}" > frontend/codecov-report/commit-sha.txt - echo "${{ github.event.pull_request.head.ref || github.ref_name }}" > frontend/codecov-report/branch.txt - - name: Upload frontend coverage artifact + 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: name: codecov-frontend - path: frontend/codecov-report/ + path: cc/ retention-days: 1 if-no-files-found: warn @@ -304,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 — @@ -660,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 @@ -772,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 @@ -1019,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 }} @@ -1089,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 index b02e1255e09..8ee8f5513df 100644 --- a/.github/workflows/codecov-upload.yml +++ b/.github/workflows/codecov-upload.yml @@ -16,21 +16,28 @@ # 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. A direct 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 artifact with authentication so the comment posts reliably even in bursts. +# 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. # -# Proof of concept: only the `frontend` flag is routed through here for now. build.yml -# stages the frontend lcov + JUnit reports as the `codecov-frontend` artifact; every -# other flag still uploads directly from build.yml. +# 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 it is merged to main. After merge, exercise it manually via -# the workflow_dispatch entry below, passing a finished "Required Checks" run id. +# 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 @@ -42,7 +49,7 @@ on: workflow_dispatch: inputs: run_id: - description: "Finished 'Required Checks' run id to pull the codecov-frontend artifact from" + description: "Finished 'Required Checks' run id to pull the codecov-* artifacts from" required: true type: string @@ -50,79 +57,158 @@ permissions: contents: read actions: read # download-artifact needs this to read artifacts from another run -# One upload per source run; a re-run of the source supersedes rather than races. -concurrency: - group: codecov-upload-${{ github.event.workflow_run.id || inputs.run_id }} - cancel-in-progress: false - jobs: - frontend: + upload: # Skip only a cancelled/skipped upstream; still run on failure so partial - # coverage is reported (mirrors the old always()/!cancelled() upload steps). + # 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 frontend coverage + - name: Download staged coverage id: dl - continue-on-error: true # absent when the frontend job was label-gated out + 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-frontend - path: codecov-frontend + 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 - # The artifact content comes from a (possibly fork) PR build, so sanitize - # each value to its expected charset before handing it to the Codecov CLI. + # 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 codecov-frontend/pr-number.txt 2>/dev/null | tr -cd '0-9')" >> "$GITHUB_OUTPUT" - echo "sha=$(cat codecov-frontend/commit-sha.txt 2>/dev/null | tr -cd '0-9a-fA-F')" >> "$GITHUB_OUTPUT" - echo "branch=$(cat codecov-frontend/branch.txt 2>/dev/null | tr -cd '[:alnum:]._/:-')" >> "$GITHUB_OUTPUT" - - name: Upload frontend coverage to Codecov - if: steps.dl.outcome == 'success' && hashFiles('codecov-frontend/lcov.info') != '' + 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 }} - files: codecov-frontend/lcov.info - flags: frontend - disable_search: true - fail_ci_if_error: false + 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 frontend unit test results to Codecov - if: steps.dl.outcome == 'success' && hashFiles('codecov-frontend/junit.xml') != '' + - name: Upload ${{ matrix.flag }} test results to Codecov + # Test Analytics is best-effort (fail_ci_if_error: false) — a JUnit-ingest + # hiccup should not raise a "coverage upload failed" alarm on the PR. + if: steps.dl.outcome == 'success' && hashFiles('cc/results/**') != '' uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} - files: codecov-frontend/junit.xml - flags: frontend + directory: cc/results + flags: ${{ matrix.flag }} report_type: test_results - disable_search: true fail_ci_if_error: false override_commit: ${{ steps.meta.outputs.sha }} override_branch: ${{ steps.meta.outputs.branch }} override_pr: ${{ steps.meta.outputs.pr }} - - name: Upload frontend browser-mode test results to Codecov - if: steps.dl.outcome == 'success' && hashFiles('codecov-frontend/junit-browser.xml') != '' - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + + notify-failure: + # If any coverage upload above failed, 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 both Codecov's comment and this one signals a stuck run to look at. + 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: - token: ${{ secrets.CODECOV_TOKEN }} - files: codecov-frontend/junit-browser.xml - flags: frontend - report_type: test_results - disable_search: true - fail_ci_if_error: false - override_commit: ${{ steps.meta.outputs.sha }} - override_branch: ${{ steps.meta.outputs.branch }} - override_pr: ${{ steps.meta.outputs.pr }} + 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 coverage 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}\`.`, + '', + "_If Codecov's own coverage comment appears, the upload succeeded and this can be ignored._", + ].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 }); + } From a01b36694967bc9a8b6b389acbdc7e83fac05e99 Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Wed, 22 Jul 2026 01:07:17 -0700 Subject: [PATCH 4/5] ci: fail hard on test-results upload too; trim the failure comment Per review on #6730: - test-results upload now also uses fail_ci_if_error: true, so a Test Analytics upload error trips notify-failure instead of being silently dropped. - Drop the "if Codecov's comment appears, ignore this" line from the failure comment: on a re-run a stale Codecov success comment from a prior run can coexist with a fresh failure, which made that note misleading. --- .github/workflows/codecov-upload.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.github/workflows/codecov-upload.yml b/.github/workflows/codecov-upload.yml index 8ee8f5513df..b43e22906e9 100644 --- a/.github/workflows/codecov-upload.yml +++ b/.github/workflows/codecov-upload.yml @@ -140,8 +140,9 @@ jobs: override_branch: ${{ steps.meta.outputs.branch }} override_pr: ${{ steps.meta.outputs.pr }} - name: Upload ${{ matrix.flag }} test results to Codecov - # Test Analytics is best-effort (fail_ci_if_error: false) — a JUnit-ingest - # hiccup should not raise a "coverage upload failed" alarm on the PR. + # 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: @@ -149,16 +150,16 @@ jobs: directory: cc/results flags: ${{ matrix.flag }} report_type: test_results - fail_ci_if_error: false + 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 coverage upload above failed, 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 both Codecov's comment and this one signals a stuck run to look at. + # 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 @@ -193,14 +194,12 @@ jobs: const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const body = [ MARKER, - '⚠️ **Codecov coverage upload failed.**', + '⚠️ **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}\`.`, - '', - "_If Codecov's own coverage comment appears, the upload succeeded and this can be ignored._", ].join('\n'); const { owner, repo } = context.repo; const comments = await github.paginate(github.rest.issues.listComments, { From 51f3d7773d8960d0c00729bd8f8321fa0b47a79f Mon Sep 17 00:00:00 2001 From: Xinyuan Lin Date: Wed, 22 Jul 2026 01:53:33 -0700 Subject: [PATCH 5/5] ci: add ASF license header to stage-codecov.sh The license-eye header check (Check License Headers) failed because the new staging script was missing the Apache license header. --- .github/scripts/stage-codecov.sh | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/scripts/stage-codecov.sh b/.github/scripts/stage-codecov.sh index 9467421c1b9..431993aad13 100644 --- a/.github/scripts/stage-codecov.sh +++ b/.github/scripts/stage-codecov.sh @@ -1,4 +1,21 @@ #!/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-`