From 9f65e3e9314a62a964303eb33d076e9104f6a8cc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 00:53:58 +0000 Subject: [PATCH 1/6] chore: add agent notes column to CVE remediation run summary The nightly CVE remediation run summary only showed the issues discovered before the agent ran, so when the agent decided not to open a PR there was no visible explanation. Move the issue table to a post-agent step in the remediation job and add a Notes column the agent can optionally populate by writing one file per issue into a notes directory. Issue metadata is handed to the remediation job through an artifact rather than a job output because identifiers contain the Linear team key and would be blocked by secret redaction. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VsjLedntEDA6nHFmfYYHAQ --- .../scripts/render-cve-remediation-summary.sh | 52 +++++++++++ .github/scripts/test-cve-remediation.sh | 90 ++++++++++++++++++- .github/workflows/_cve-remediation.yml | 38 +++++++- 3 files changed, 176 insertions(+), 4 deletions(-) create mode 100755 .github/scripts/render-cve-remediation-summary.sh diff --git a/.github/scripts/render-cve-remediation-summary.sh b/.github/scripts/render-cve-remediation-summary.sh new file mode 100755 index 000000000..eb4629b64 --- /dev/null +++ b/.github/scripts/render-cve-remediation-summary.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Renders the CVE remediation results table for the workflow run summary. +# Combines the issue metadata produced by the discovery job with the optional +# per-issue notes written by the remediation agent. A note is looked up by the +# issue's Linear identifier (SOU-123.md) or, as a fallback, its Linear UUID. +# +# Usage: render-cve-remediation-summary.sh + +ISSUES_FILE="${1:?issues JSON file is required}" +NOTES_DIR="${2:?notes directory is required}" + +MAX_NOTE_LENGTH=500 + +escape_table_cell() { + printf '%s' "${1//|/\\|}" +} + +echo '| Linear issue | Priority | Status | Title | Notes |' +echo '| --- | ---: | --- | --- | --- |' + +while IFS= read -r issue; do + identifier=$(jq -r '.identifier' <<<"$issue") + id=$(jq -r '.id' <<<"$issue") + url=$(jq -r '.url' <<<"$issue") + priority=$(jq -r '.priority' <<<"$issue") + status=$(jq -r '.status' <<<"$issue") + title=$(jq -r '.title' <<<"$issue") + + note="" + for note_key in "$identifier" "$id"; do + if [[ -z "$note_key" || "$note_key" == */* || "$note_key" == .* ]]; then + continue + fi + note_file="$NOTES_DIR/$note_key.md" + if [[ -f "$note_file" ]]; then + note=$(tr -s '[:space:]' ' ' < "$note_file") + note="${note# }" + note="${note% }" + break + fi + done + if (( ${#note} > MAX_NOTE_LENGTH )); then + note="${note:0:MAX_NOTE_LENGTH}…" + fi + + printf '| [%s](%s) | %s | %s | %s | %s |\n' \ + "$identifier" "$url" "$priority" "$status" \ + "$(escape_table_cell "$title")" \ + "$(escape_table_cell "$note")" +done < <(jq -c '.[]' "$ISSUES_FILE") diff --git a/.github/scripts/test-cve-remediation.sh b/.github/scripts/test-cve-remediation.sh index fcaf431e9..f75236d79 100755 --- a/.github/scripts/test-cve-remediation.sh +++ b/.github/scripts/test-cve-remediation.sh @@ -4,6 +4,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FILTER="$SCRIPT_DIR/filter-unlinked-cve-issues.jq" DISCOVERY_SCRIPT="$SCRIPT_DIR/find-unlinked-cve-issues.sh" +RENDER_SCRIPT="$SCRIPT_DIR/render-cve-remediation-summary.sh" WORKFLOW_FILE="$SCRIPT_DIR/../workflows/_cve-remediation.yml" CALLER_WORKFLOW_FILE="$SCRIPT_DIR/../workflows/cve-remediation.yml" SYSTEM_PROMPT_FILE="$SCRIPT_DIR/../prompts/cve-remediation-system.md" @@ -22,6 +23,19 @@ assert_json() { fi } +assert_equals() { + local description="$1" + local actual="$2" + local expected="$3" + + if [[ "$actual" != "$expected" ]]; then + echo "FAIL: $description" + echo "Expected: $expected" + echo "Actual: $actual" + exit 1 + fi +} + assert_workflow_contains() { local description="$1" local expected="$2" @@ -138,7 +152,9 @@ FAKE_CURL_DIR=$(mktemp -d) FAKE_CURL_COUNT=$(mktemp) FAKE_CURL_PAYLOAD_DIR=$(mktemp -d) FAKE_GH_LOG=$(mktemp) -trap 'rm -rf "$FAKE_CURL_DIR" "$FAKE_CURL_PAYLOAD_DIR"; rm -f "$FAKE_CURL_COUNT" "$FAKE_GH_LOG"' EXIT +RENDER_NOTES_DIR=$(mktemp -d) +RENDER_ISSUES_FILE=$(mktemp) +trap 'rm -rf "$FAKE_CURL_DIR" "$FAKE_CURL_PAYLOAD_DIR" "$RENDER_NOTES_DIR"; rm -f "$FAKE_CURL_COUNT" "$FAKE_GH_LOG" "$RENDER_ISSUES_FILE"' EXIT printf '0\n' > "$FAKE_CURL_COUNT" cat > "$FAKE_CURL_DIR/curl" <<'EOF' @@ -325,6 +341,62 @@ assert_json \ "$(jq -c '.variables.after' "$FAKE_CURL_PAYLOAD_DIR/3.json")" \ '"next-page"' +cat > "$RENDER_ISSUES_FILE" <<'EOF' +[ + { + "id": "issue-31", + "identifier": "SOU-31", + "title": "[sourcebot-dev/example] CVE-31: note keyed by identifier", + "url": "https://linear.app/sourcebot/issue/SOU-31/test", + "priority": 1, + "status": "Todo", + "statusType": "unstarted" + }, + { + "id": "issue-32", + "identifier": "SOU-32", + "title": "[sourcebot-dev/example] CVE-32: pipe | in title", + "url": "https://linear.app/sourcebot/issue/SOU-32/test", + "priority": 2, + "status": "Backlog", + "statusType": "backlog" + }, + { + "id": "issue-33", + "identifier": "SOU-33", + "title": "[sourcebot-dev/example] CVE-33: no note", + "url": "https://linear.app/sourcebot/issue/SOU-33/test", + "priority": 3, + "status": "Backlog", + "statusType": "backlog" + }, + { + "id": "issue-34", + "identifier": "SOU-34", + "title": "[sourcebot-dev/example] CVE-34: note keyed by UUID", + "url": "https://linear.app/sourcebot/issue/SOU-34/test", + "priority": 4, + "status": "Backlog", + "statusType": "backlog" + } +] +EOF +printf 'Opened PR #99 upgrading foo to 1.2.3.\n' > "$RENDER_NOTES_DIR/SOU-31.md" +printf 'No patched | release exists yet;\nre-check tomorrow.\n' > "$RENDER_NOTES_DIR/SOU-32.md" +printf 'Skipped: dependency already patched on main.\n' > "$RENDER_NOTES_DIR/issue-34.md" + +EXPECTED_SUMMARY='| Linear issue | Priority | Status | Title | Notes | +| --- | ---: | --- | --- | --- | +| [SOU-31](https://linear.app/sourcebot/issue/SOU-31/test) | 1 | Todo | [sourcebot-dev/example] CVE-31: note keyed by identifier | Opened PR #99 upgrading foo to 1.2.3. | +| [SOU-32](https://linear.app/sourcebot/issue/SOU-32/test) | 2 | Backlog | [sourcebot-dev/example] CVE-32: pipe \| in title | No patched \| release exists yet; re-check tomorrow. | +| [SOU-33](https://linear.app/sourcebot/issue/SOU-33/test) | 3 | Backlog | [sourcebot-dev/example] CVE-33: no note | | +| [SOU-34](https://linear.app/sourcebot/issue/SOU-34/test) | 4 | Backlog | [sourcebot-dev/example] CVE-34: note keyed by UUID | Skipped: dependency already patched on main. |' + +assert_equals \ + "renders the results table with flattened, escaped agent notes" \ + "$("$RENDER_SCRIPT" "$RENDER_ISSUES_FILE" "$RENDER_NOTES_DIR")" \ + "$EXPECTED_SUMMARY" + assert_json \ "maps Linear on-call assignees to GitHub reviewers" \ "$(jq -c . "$REVIEWER_MAP_FILE")" \ @@ -403,5 +475,21 @@ assert_workflow_not_contains \ "does not use the unavailable inputs context in a reusable workflow call" \ '${{ inputs.max_issues' \ "$CALLER_WORKFLOW_FILE" +assert_workflow_contains \ + "hands issue metadata to the remediation job through an artifact" \ + 'name: cve-remediation-issues' +assert_workflow_contains \ + "prompts Claude with the notes location for the run summary" \ + 'cve-remediation-notes/.md' +assert_workflow_contains \ + "renders the remediation results table after the agent runs" \ + '.cve-remediation-workflow/.github/scripts/render-cve-remediation-summary.sh' +assert_workflow_contains \ + "reports remediation results even when the agent step fails" \ + 'if: always()' +assert_workflow_contains \ + "includes the agent notes column in the results table" \ + '| Linear issue | Priority | Status | Title | Notes |' \ + "$RENDER_SCRIPT" echo "All CVE remediation tests passed." diff --git a/.github/workflows/_cve-remediation.yml b/.github/workflows/_cve-remediation.yml index 2751a48fa..c2d65258c 100644 --- a/.github/workflows/_cve-remediation.yml +++ b/.github/workflows/_cve-remediation.yml @@ -67,6 +67,7 @@ jobs: issues=$(jq -c --argjson max "$MAX_ISSUES" '.[0:$max]' <<<"$all_issues") issue_ids=$(jq -c 'map(.id)' <<<"$issues") issue_count=$(jq 'length' <<<"$issues") + printf '%s\n' "$issues" > "$RUNNER_TEMP/cve-remediation-issues.json" if ((issue_count > 0)); then has_issues=true @@ -93,12 +94,18 @@ jobs: echo "Claude was not started." else echo - echo '| Linear issue | Priority | Status | Title |' - echo '| --- | ---: | --- | --- |' - jq -r '.[] | "| [\(.identifier)](\(.url)) | \(.priority) | \(.status) | \(.title | gsub("\\|"; "\\\\|")) |"' <<<"$issues" + echo "The processed issues, along with any notes recorded by the agent, are listed in the remediation job summary." fi } >> "$GITHUB_STEP_SUMMARY" + - name: Upload issue metadata for the remediation summary + if: steps.discover.outputs.has_issues == 'true' + uses: actions/upload-artifact@v4 + with: + name: cve-remediation-issues + path: ${{ runner.temp }}/cve-remediation-issues.json + retention-days: 7 + remediate: name: Remediate CVEs with Claude needs: discover @@ -124,9 +131,19 @@ jobs: sparse-checkout: | .github/prompts/cve-remediation-system.md .github/cve-reviewers.json + .github/scripts path: .cve-remediation-workflow persist-credentials: false + - name: Download issue metadata + uses: actions/download-artifact@v4 + with: + name: cve-remediation-issues + path: ${{ runner.temp }} + + - name: Prepare agent notes directory + run: mkdir -p "$RUNNER_TEMP/cve-remediation-notes" + - name: Configure read-only Linear MCP env: LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} @@ -157,6 +174,8 @@ jobs: ${{ needs.discover.outputs.issue_ids }} This is an unattended run. Inspect every supplied issue through the read-only Linear MCP server, follow the system instructions, and open or update pull requests only for complete, verified remediations. + + For each issue, you may optionally record a short note (one or two sentences) describing the outcome by writing it to `${{ runner.temp }}/cve-remediation-notes/.md` (for example, `SOU-123.md`). Notes are shown next to each issue in the workflow run summary. Leave a note whenever the outcome would otherwise be unclear, especially when you decide not to open or update a pull request. claude_args: | --append-system-prompt-file "${{ github.workspace }}/.cve-remediation-workflow/.github/prompts/cve-remediation-system.md" --strict-mcp-config @@ -166,3 +185,16 @@ jobs: --tools "Bash,Read,Edit,Write,Glob,Grep" --allowedTools "Read,Edit,Write,Glob,Grep,Bash(git *),Bash(gh pr *),Bash(yarn *),Bash(npm *),Bash(npx *),Bash(pnpm *),Bash(bun *),Bash(go *),Bash(cargo *),Bash(uv *),Bash(pytest *),Bash(python -m pytest *),Bash(make *),Bash(just *),mcp__linear__get_issue,mcp__linear__list_comments" --disallowedTools "Bash(gh pr merge *),Bash(git push *--force*),Bash(npm publish *),Bash(yarn npm publish *),Bash(pnpm publish *),Bash(cargo publish *)" + + - name: Report remediation results + if: always() + env: + ISSUES_FILE: ${{ runner.temp }}/cve-remediation-issues.json + NOTES_DIR: ${{ runner.temp }}/cve-remediation-notes + run: | + set -euo pipefail + { + echo "## CVE remediation results" + echo + .cve-remediation-workflow/.github/scripts/render-cve-remediation-summary.sh "$ISSUES_FILE" "$NOTES_DIR" + } >> "$GITHUB_STEP_SUMMARY" From 330e11780c4d378a022a8d439e70b485655658df Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 00:54:36 +0000 Subject: [PATCH 2/6] docs: add changelog entry for #1620 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VsjLedntEDA6nHFmfYYHAQ --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c363645..0c4e70283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Added a notes column to the nightly CVE remediation run summary that the agent can write per-issue outcome notes into. [#1620](https://github.com/sourcebot-dev/sourcebot/pull/1620) + ## [5.1.10] - 2026-08-27 ### Fixed From 5c5d1f3b85bc13fa81acb5c28c8b4aa9137dd825 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 00:58:45 +0000 Subject: [PATCH 3/6] docs: remove changelog entry for workflow-only change Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VsjLedntEDA6nHFmfYYHAQ --- CHANGELOG.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c4e70283..46c363645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added -- Added a notes column to the nightly CVE remediation run summary that the agent can write per-issue outcome notes into. [#1620](https://github.com/sourcebot-dev/sourcebot/pull/1620) - ## [5.1.10] - 2026-08-27 ### Fixed From f62586a6dffc160ccc33f18ec9c3a22867de52ab Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 01:03:51 +0000 Subject: [PATCH 4/6] chore: harden CVE remediation summary rendering Address review findings: escape backslashes before pipes in table cells, fail loudly when the issues artifact is missing or malformed, exercise note truncation in the tests, and scope the always() assertion to the report step. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VsjLedntEDA6nHFmfYYHAQ --- .../scripts/render-cve-remediation-summary.sh | 8 +++- .github/scripts/test-cve-remediation.sh | 41 ++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/.github/scripts/render-cve-remediation-summary.sh b/.github/scripts/render-cve-remediation-summary.sh index eb4629b64..7ec626575 100755 --- a/.github/scripts/render-cve-remediation-summary.sh +++ b/.github/scripts/render-cve-remediation-summary.sh @@ -13,8 +13,14 @@ NOTES_DIR="${2:?notes directory is required}" MAX_NOTE_LENGTH=500 +if ! jq -e 'type == "array"' "$ISSUES_FILE" >/dev/null 2>&1; then + echo "issues file '$ISSUES_FILE' is missing or is not a JSON array" >&2 + exit 1 +fi + escape_table_cell() { - printf '%s' "${1//|/\\|}" + local escaped="${1//\\/\\\\}" + printf '%s' "${escaped//|/\\|}" } echo '| Linear issue | Priority | Status | Title | Notes |' diff --git a/.github/scripts/test-cve-remediation.sh b/.github/scripts/test-cve-remediation.sh index f75236d79..35c10d8fd 100755 --- a/.github/scripts/test-cve-remediation.sh +++ b/.github/scripts/test-cve-remediation.sh @@ -378,25 +378,46 @@ cat > "$RENDER_ISSUES_FILE" <<'EOF' "priority": 4, "status": "Backlog", "statusType": "backlog" + }, + { + "id": "issue-35", + "identifier": "SOU-35", + "title": "[sourcebot-dev/example] CVE-35: truncated long note", + "url": "https://linear.app/sourcebot/issue/SOU-35/test", + "priority": 5, + "status": "Backlog", + "statusType": "backlog" } ] EOF printf 'Opened PR #99 upgrading foo to 1.2.3.\n' > "$RENDER_NOTES_DIR/SOU-31.md" -printf 'No patched | release exists yet;\nre-check tomorrow.\n' > "$RENDER_NOTES_DIR/SOU-32.md" +cat > "$RENDER_NOTES_DIR/SOU-32.md" <<'EOF' +No patched | release \| exists yet; +re-check tomorrow. +EOF printf 'Skipped: dependency already patched on main.\n' > "$RENDER_NOTES_DIR/issue-34.md" +LONG_NOTE=$(printf 'a%.0s' $(seq 1 600)) +printf '%s\n' "$LONG_NOTE" > "$RENDER_NOTES_DIR/SOU-35.md" EXPECTED_SUMMARY='| Linear issue | Priority | Status | Title | Notes | | --- | ---: | --- | --- | --- | | [SOU-31](https://linear.app/sourcebot/issue/SOU-31/test) | 1 | Todo | [sourcebot-dev/example] CVE-31: note keyed by identifier | Opened PR #99 upgrading foo to 1.2.3. | -| [SOU-32](https://linear.app/sourcebot/issue/SOU-32/test) | 2 | Backlog | [sourcebot-dev/example] CVE-32: pipe \| in title | No patched \| release exists yet; re-check tomorrow. | +| [SOU-32](https://linear.app/sourcebot/issue/SOU-32/test) | 2 | Backlog | [sourcebot-dev/example] CVE-32: pipe \| in title | No patched \| release \\\| exists yet; re-check tomorrow. | | [SOU-33](https://linear.app/sourcebot/issue/SOU-33/test) | 3 | Backlog | [sourcebot-dev/example] CVE-33: no note | | | [SOU-34](https://linear.app/sourcebot/issue/SOU-34/test) | 4 | Backlog | [sourcebot-dev/example] CVE-34: note keyed by UUID | Skipped: dependency already patched on main. |' +EXPECTED_SUMMARY+=" +| [SOU-35](https://linear.app/sourcebot/issue/SOU-35/test) | 5 | Backlog | [sourcebot-dev/example] CVE-35: truncated long note | ${LONG_NOTE:0:500}… |" assert_equals \ - "renders the results table with flattened, escaped agent notes" \ + "renders the results table with flattened, escaped, truncated agent notes" \ "$("$RENDER_SCRIPT" "$RENDER_ISSUES_FILE" "$RENDER_NOTES_DIR")" \ "$EXPECTED_SUMMARY" +if "$RENDER_SCRIPT" "$RENDER_NOTES_DIR/does-not-exist.json" "$RENDER_NOTES_DIR" >/dev/null 2>&1; then + echo "FAIL: fails loudly instead of rendering an empty table when the issues file is missing" + exit 1 +fi + assert_json \ "maps Linear on-call assignees to GitHub reviewers" \ "$(jq -c . "$REVIEWER_MAP_FILE")" \ @@ -478,15 +499,23 @@ assert_workflow_not_contains \ assert_workflow_contains \ "hands issue metadata to the remediation job through an artifact" \ 'name: cve-remediation-issues' +assert_workflow_contains \ + "uploads the issue metadata from the discovery job" \ + 'name: Upload issue metadata for the remediation summary' +assert_workflow_contains \ + "downloads the issue metadata in the remediation job" \ + 'name: Download issue metadata' assert_workflow_contains \ "prompts Claude with the notes location for the run summary" \ 'cve-remediation-notes/.md' assert_workflow_contains \ "renders the remediation results table after the agent runs" \ '.cve-remediation-workflow/.github/scripts/render-cve-remediation-summary.sh' -assert_workflow_contains \ - "reports remediation results even when the agent step fails" \ - 'if: always()' +if ! grep -A1 -- '- name: Report remediation results' "$WORKFLOW_FILE" | grep -Fq 'if: always()'; then + echo "FAIL: reports remediation results even when the agent step fails" + echo "Expected the 'Report remediation results' step to run with 'if: always()'" + exit 1 +fi assert_workflow_contains \ "includes the agent notes column in the results table" \ '| Linear issue | Priority | Status | Title | Notes |' \ From 96d97209ac0ede5831779c5c591a24be2d0a0789 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 01:18:59 +0000 Subject: [PATCH 5/6] chore: execute remediation summary renderer from a pre-agent snapshot The report step ran the renderer from the untracked workflow-assets checkout inside the workspace, which the agent step can modify or delete (e.g. a git clean). Copy the script to the runner temp directory before the agent starts and execute that snapshot instead. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VsjLedntEDA6nHFmfYYHAQ --- .github/scripts/test-cve-remediation.sh | 7 +++++-- .github/workflows/_cve-remediation.yml | 9 ++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/scripts/test-cve-remediation.sh b/.github/scripts/test-cve-remediation.sh index 35c10d8fd..257b55698 100755 --- a/.github/scripts/test-cve-remediation.sh +++ b/.github/scripts/test-cve-remediation.sh @@ -509,8 +509,11 @@ assert_workflow_contains \ "prompts Claude with the notes location for the run summary" \ 'cve-remediation-notes/.md' assert_workflow_contains \ - "renders the remediation results table after the agent runs" \ - '.cve-remediation-workflow/.github/scripts/render-cve-remediation-summary.sh' + "snapshots the renderer before the agent can touch the checkout" \ + 'cp .cve-remediation-workflow/.github/scripts/render-cve-remediation-summary.sh "$RUNNER_TEMP/render-cve-remediation-summary.sh"' +assert_workflow_contains \ + "renders the remediation results table from the pre-agent snapshot" \ + '"$RUNNER_TEMP/render-cve-remediation-summary.sh" "$ISSUES_FILE" "$NOTES_DIR"' if ! grep -A1 -- '- name: Report remediation results' "$WORKFLOW_FILE" | grep -Fq 'if: always()'; then echo "FAIL: reports remediation results even when the agent step fails" echo "Expected the 'Report remediation results' step to run with 'if: always()'" diff --git a/.github/workflows/_cve-remediation.yml b/.github/workflows/_cve-remediation.yml index c2d65258c..7d71f9004 100644 --- a/.github/workflows/_cve-remediation.yml +++ b/.github/workflows/_cve-remediation.yml @@ -141,8 +141,11 @@ jobs: name: cve-remediation-issues path: ${{ runner.temp }} - - name: Prepare agent notes directory - run: mkdir -p "$RUNNER_TEMP/cve-remediation-notes" + - name: Prepare remediation summary inputs + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/cve-remediation-notes" + cp .cve-remediation-workflow/.github/scripts/render-cve-remediation-summary.sh "$RUNNER_TEMP/render-cve-remediation-summary.sh" - name: Configure read-only Linear MCP env: @@ -196,5 +199,5 @@ jobs: { echo "## CVE remediation results" echo - .cve-remediation-workflow/.github/scripts/render-cve-remediation-summary.sh "$ISSUES_FILE" "$NOTES_DIR" + "$RUNNER_TEMP/render-cve-remediation-summary.sh" "$ISSUES_FILE" "$NOTES_DIR" } >> "$GITHUB_STEP_SUMMARY" From 96897b220ec55a5dbc2a97e7de9ad9e7bbc4499c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 01:22:36 +0000 Subject: [PATCH 6/6] test: assert renderer snapshot precedes the agent step Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VsjLedntEDA6nHFmfYYHAQ --- .github/scripts/test-cve-remediation.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/scripts/test-cve-remediation.sh b/.github/scripts/test-cve-remediation.sh index 257b55698..186504a74 100755 --- a/.github/scripts/test-cve-remediation.sh +++ b/.github/scripts/test-cve-remediation.sh @@ -514,6 +514,13 @@ assert_workflow_contains \ assert_workflow_contains \ "renders the remediation results table from the pre-agent snapshot" \ '"$RUNNER_TEMP/render-cve-remediation-summary.sh" "$ISSUES_FILE" "$NOTES_DIR"' +snapshot_line=$(grep -nF -- 'cp .cve-remediation-workflow/.github/scripts/render-cve-remediation-summary.sh' "$WORKFLOW_FILE" | head -1 | cut -d: -f1) +agent_line=$(grep -nF -- 'name: Run Claude CVE remediation agent' "$WORKFLOW_FILE" | head -1 | cut -d: -f1) +if [[ -z "$snapshot_line" || -z "$agent_line" ]] || ((snapshot_line >= agent_line)); then + echo "FAIL: snapshots the renderer before the Claude agent step runs" + echo "Expected the renderer copy (line ${snapshot_line:-missing}) to precede the agent step (line ${agent_line:-missing})" + exit 1 +fi if ! grep -A1 -- '- name: Report remediation results' "$WORKFLOW_FILE" | grep -Fq 'if: always()'; then echo "FAIL: reports remediation results even when the agent step fails" echo "Expected the 'Report remediation results' step to run with 'if: always()'"