Recipe Evidence: Comment #807
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # | |
| # Licensed 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. | |
| # Workflow 2 of 2 for the warning-only recipe-evidence gate. | |
| # | |
| # Triggered by workflow_run after "Recipe Evidence: Verify" completes. | |
| # Downloads the report.md artifact and posts/updates a sticky PR | |
| # comment. This workflow never checks out the PR branch, keeping any | |
| # future secrets isolated from untrusted PR code. Mirrors the pattern | |
| # established by fern-docs-preview-build + fern-docs-preview-comment. | |
| # | |
| # The PR association is re-derived here from the trusted | |
| # workflow_run.head_sha (via commits/<sha>/pulls) rather than read | |
| # from the artifact, since the artifact is produced by the build | |
| # workflow on the PR branch (fork-attacker-controllable contents). | |
| # report.md itself is also fork-influenced (its row contents come from | |
| # PR-supplied overlay filenames and the verify output), but we only | |
| # post it as Markdown under the bot identity — no parsing. | |
| name: "Recipe Evidence: Comment" | |
| on: | |
| workflow_run: | |
| workflows: ["Recipe Evidence: Verify"] | |
| types: [completed] | |
| permissions: | |
| pull-requests: write | |
| actions: read | |
| concurrency: | |
| # Include head repository identity so fork branches with the same name | |
| # don't share a group key and cancel unrelated runs. | |
| group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name || github.repository }}-${{ github.event.workflow_run.head_branch || github.event.workflow_run.id }} | |
| cancel-in-progress: true | |
| jobs: | |
| comment: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| # Run regardless of build conclusion: workflow 1 uploads partial | |
| # reports with `if: always()`. Filter to PR-triggered runs only so | |
| # unrelated workflow_run firings (e.g., manual reruns from other | |
| # contexts) don't try to post on a non-existent PR. | |
| if: ${{ github.event.workflow_run.event == 'pull_request' }} | |
| steps: | |
| - name: Download evidence report | |
| # The verify workflow only produces this artifact when it got far | |
| # enough to write a report; a build failure before the script runs | |
| # leaves none. Don't fail this warning-only gate on a missing | |
| # artifact — the comment step below no-ops when report.md is absent. | |
| continue-on-error: true | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: recipe-evidence-report | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Resolve PR number from trusted workflow_run metadata | |
| id: metadata | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| HEAD_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }} | |
| BASE_REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| # Resolve the PR from the trusted workflow_run.head_sha by | |
| # listing open PRs and matching on head SHA + head-repo owner. | |
| # | |
| # We deliberately do NOT use `commits/${HEAD_SHA}/pulls`: that | |
| # endpoint does not reliably return fork-originated PRs (the head | |
| # commit lives in the contributor's fork, not the base repo), so | |
| # every external-contributor PR resolved to zero matches and the | |
| # comment was silently skipped. Listing open PRs and comparing | |
| # `.head.sha` works uniformly for same-repo and fork PRs. | |
| # | |
| # This is still the trusted source — both the SHA and the owner | |
| # come from the workflow_run event, never an artifact. Matching | |
| # the head-repo owner keeps two PRs that share a head SHA across | |
| # forks from being confused. Require exactly one match — refuse | |
| # to guess if a single owner has multiple open PRs at this SHA. | |
| # | |
| # --paginate streams matching PR numbers across all pages | |
| # (gh applies --jq per page); `jq -s` then slurps that stream | |
| # into a JSON array. HEAD_SHA/HEAD_OWNER are read via jq's | |
| # `env.` rather than interpolated into the filter, so a hostile | |
| # value can't break out of the jq expression. (gh's --jq does | |
| # not accept jq's --arg, hence env.) | |
| matches=$(gh api --paginate \ | |
| "repos/${BASE_REPO}/pulls?state=open&per_page=100" \ | |
| --jq '.[] | select(.head.sha == env.HEAD_SHA and .head.repo.owner.login == env.HEAD_OWNER) | .number' \ | |
| | jq -s '.') | |
| match_count=$(echo "$matches" | jq 'length') | |
| if [[ "$match_count" -ne 1 ]]; then | |
| echo "::warning::expected exactly 1 open PR for head ${HEAD_SHA} from ${HEAD_OWNER}; got ${match_count}; skipping comment" | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| pr_number=$(echo "$matches" | jq -r '.[0]') | |
| if ! [[ "$pr_number" =~ ^[0-9]+$ ]]; then | |
| echo "::warning::resolved PR number ${pr_number} is not a positive integer; skipping comment" | |
| echo "skip=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "pr_number=${pr_number}" >> "$GITHUB_OUTPUT" | |
| echo "skip=false" >> "$GITHUB_OUTPUT" | |
| - name: Post or update sticky PR comment | |
| if: ${{ steps.metadata.outputs.skip != 'true' }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_NUMBER: ${{ steps.metadata.outputs.pr_number }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| MARKER="<!-- recipe-evidence-gate -->" | |
| if [[ ! -s report.md ]]; then | |
| echo "No report.md produced; skipping comment." | |
| exit 0 | |
| fi | |
| # Build the body without the marker first, then append the | |
| # marker exactly once after the size check — avoids the | |
| # double-marker edge case where the marker fits inside the | |
| # initial body but the truncation slice cuts before it. | |
| REPORT_BODY=$(cat report.md) | |
| # GitHub caps issue-comment bodies at ~65536 characters. The | |
| # build script already truncates the per-row table; this is | |
| # belt-and-braces for the trailer + any future report growth. | |
| # Reserve headroom for the trailer + marker we append below. | |
| max=64800 | |
| if (( ${#REPORT_BODY} > max )); then | |
| REPORT_BODY=$(printf '%s\n\n_(comment truncated to fit GitHub 65KB cap)_' \ | |
| "${REPORT_BODY:0:max}") | |
| fi | |
| BODY=$(printf '%s\n\n%s\n' "$REPORT_BODY" "$MARKER") | |
| # --paginate so the marker is found on PRs with >30 comments. | |
| COMMENT_ID=$(gh api --paginate "repos/${REPO}/issues/${PR_NUMBER}/comments" \ | |
| --jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" \ | |
| | tr -d '\r' | head -1) | |
| if [[ -n "$COMMENT_ID" ]]; then | |
| gh api "repos/${REPO}/issues/comments/${COMMENT_ID}" \ | |
| -X PATCH -f body="$BODY" | |
| else | |
| gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ | |
| -f body="$BODY" | |
| fi |