| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,14 +1,15 @@ | |||
| 1 | - def has_linked_github_pr: | ||
| 1 | + def has_open_linked_github_pr: | ||
| 2 | 2 | any( | |
| 3 | - .attachments.nodes[]?.url?; | ||
| 4 | - type == "string" | ||
| 5 | - and test("^https://github\\.com/[^/]+/[^/]+/pull/[0-9]+(?:[/?#].*)?$") | ||
| 3 | + .attachments.nodes[]?; | ||
| 4 | + (.url? | type == "string" | ||
| 5 | + and test("^https://github\\.com/[^/]+/[^/]+/pull/[0-9]+(?:[/?#].*)?$")) | ||
| 6 | + and .githubPrState? == "open" | ||
| 6 | 7 | ); | |
| 7 | 8 | ||
| 8 | 9 | [ | |
| 9 | 10 | .[] | |
| 10 | 11 | | select(any(.labels.nodes[]?; .name == "CVE")) | |
| 11 | - | select(has_linked_github_pr | not) | ||
| 12 | + | select(has_open_linked_github_pr | not) | ||
| 12 | 13 | | { | |
| 13 | 14 | id, | |
| 14 | 15 | identifier, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -103,4 +103,56 @@ while true; do | |||
| 103 | 103 | fi | |
| 104 | 104 | done | |
| 105 | 105 | ||
| 106 | + github_pr_states='[]' | ||
| 107 | + while IFS= read -r pr_url; do | ||
| 108 | + if [[ ! "$pr_url" =~ ^https://github\.com/([^/]+)/([^/]+)/pull/([0-9]+)([/?#].*)?$ ]]; then | ||
| 109 | + echo "Could not parse linked GitHub pull request URL: $pr_url" >&2 | ||
| 110 | + exit 1 | ||
| 111 | + fi | ||
| 112 | + | ||
| 113 | + owner="${BASH_REMATCH[1]}" | ||
| 114 | + repository="${BASH_REMATCH[2]}" | ||
| 115 | + pull_number="${BASH_REMATCH[3]}" | ||
| 116 | + if ! pr_state=$(gh api "repos/$owner/$repository/pulls/$pull_number" --jq '.state'); then | ||
| 117 | + echo "Could not fetch linked GitHub pull request: $pr_url" >&2 | ||
| 118 | + exit 1 | ||
| 119 | + fi | ||
| 120 | + | ||
| 121 | + if [[ "$pr_state" != "open" && "$pr_state" != "closed" ]]; then | ||
| 122 | + echo "GitHub returned an unexpected state for $pr_url: $pr_state" >&2 | ||
| 123 | + exit 1 | ||
| 124 | + fi | ||
| 125 | + | ||
| 126 | + github_pr_states=$(jq -cn \ | ||
| 127 | + --argjson states "$github_pr_states" \ | ||
| 128 | + --arg url "$pr_url" \ | ||
| 129 | + --arg state "$pr_state" \ | ||
| 130 | + '$states + [{url: $url, state: $state}]') | ||
| 131 | + done < <(jq -r ' | ||
| 132 | + [ | ||
| 133 | + .[] | ||
| 134 | + | select(any(.labels.nodes[]?; .name == "CVE")) | ||
| 135 | + | .attachments.nodes[]?.url? | ||
| 136 | + | strings | ||
| 137 | + | select(test("^https://github\\.com/[^/]+/[^/]+/pull/[0-9]+(?:[/?#].*)?$")) | ||
| 138 | + ] | ||
| 139 | + | unique[] | ||
| 140 | + ' <<<"$all_issues") | ||
| 141 | + | ||
| 142 | + all_issues=$(jq -cn \ | ||
| 143 | + --argjson issues "$all_issues" \ | ||
| 144 | + --argjson states "$github_pr_states" ' | ||
| 145 | + ($states | map({key: .url, value: .state}) | from_entries) as $states_by_url | ||
| 146 | + | $issues | ||
| 147 | + | map( | ||
| 148 | + .attachments.nodes |= map( | ||
| 149 | + if $states_by_url[.url] != null then | ||
| 150 | + . + {githubPrState: $states_by_url[.url]} | ||
| 151 | + else | ||
| 152 | + . | ||
| 153 | + end | ||
| 154 | + ) | ||
| 155 | + ) | ||
| 156 | + ') | ||
| 157 | + | ||
| 106 | 158 | jq -c -f "$FILTER" <<<"$all_issues" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -60,22 +60,22 @@ ISSUES='[ | |||
| 60 | 60 | { | |
| 61 | 61 | "id": "issue-2", | |
| 62 | 62 | "identifier": "SOU-2", | |
| 63 | - "title": "[sourcebot-dev/example] CVE-2: linked in this repository", | ||
| 63 | + "title": "[sourcebot-dev/example] CVE-2: open PR linked in this repository", | ||
| 64 | 64 | "url": "https://linear.app/sourcebot/issue/SOU-2/test", | |
| 65 | 65 | "priority": 2, | |
| 66 | 66 | "state": {"name": "In Progress", "type": "started"}, | |
| 67 | 67 | "labels": {"nodes": [{"name": "CVE"}]}, | |
| 68 | - "attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/example/pull/42"}]} | ||
| 68 | + "attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/example/pull/42", "githubPrState": "open"}]} | ||
| 69 | 69 | }, | |
| 70 | 70 | { | |
| 71 | 71 | "id": "issue-3", | |
| 72 | 72 | "identifier": "SOU-3", | |
| 73 | - "title": "[sourcebot-dev/example] CVE-3: linked in a companion repository", | ||
| 73 | + "title": "[sourcebot-dev/example] CVE-3: closed PR linked in a companion repository", | ||
| 74 | 74 | "url": "https://linear.app/sourcebot/issue/SOU-3/test", | |
| 75 | 75 | "priority": 1, | |
| 76 | 76 | "state": {"name": "Todo", "type": "unstarted"}, | |
| 77 | 77 | "labels": {"nodes": [{"name": "CVE"}]}, | |
| 78 | - "attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/companion/pull/9/files"}]} | ||
| 78 | + "attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/companion/pull/9/files", "githubPrState": "closed"}]} | ||
| 79 | 79 | }, | |
| 80 | 80 | { | |
| 81 | 81 | "id": "issue-4", | |
@@ -100,6 +100,15 @@ ISSUES='[ | |||
| 100 | 100 | ]' | |
| 101 | 101 | ||
| 102 | 102 | EXPECTED='[ | |
| 103 | + { | ||
| 104 | + "id": "issue-3", | ||
| 105 | + "identifier": "SOU-3", | ||
| 106 | + "title": "[sourcebot-dev/example] CVE-3: closed PR linked in a companion repository", | ||
| 107 | + "url": "https://linear.app/sourcebot/issue/SOU-3/test", | ||
| 108 | + "priority": 1, | ||
| 109 | + "status": "Todo", | ||
| 110 | + "statusType": "unstarted" | ||
| 111 | + }, | ||
| 103 | 112 | { | |
| 104 | 113 | "id": "issue-5", | |
| 105 | 114 | "identifier": "SOU-5", | |
@@ -121,14 +130,15 @@ EXPECTED='[ | |||
| 121 | 130 | ]' | |
| 122 | 131 | ||
| 123 | 132 | assert_json \ | |
| 124 | - "keeps only CVEs without a linked GitHub pull request and sorts by priority" \ | ||
| 133 | + "keeps CVEs without an open linked GitHub pull request and sorts by priority" \ | ||
| 125 | 134 | "$(jq -c -f "$FILTER" <<<"$ISSUES")" \ | |
| 126 | 135 | "$EXPECTED" | |
| 127 | 136 | ||
| 128 | 137 | FAKE_CURL_DIR=$(mktemp -d) | |
| 129 | 138 | FAKE_CURL_COUNT=$(mktemp) | |
| 130 | 139 | FAKE_CURL_PAYLOAD_DIR=$(mktemp -d) | |
| 131 | - trap 'rm -rf "$FAKE_CURL_DIR" "$FAKE_CURL_PAYLOAD_DIR"; rm -f "$FAKE_CURL_COUNT"' EXIT | ||
| 140 | + FAKE_GH_LOG=$(mktemp) | ||
| 141 | + trap 'rm -rf "$FAKE_CURL_DIR" "$FAKE_CURL_PAYLOAD_DIR"; rm -f "$FAKE_CURL_COUNT" "$FAKE_GH_LOG"' EXIT | ||
| 132 | 142 | printf '0\n' > "$FAKE_CURL_COUNT" | |
| 133 | 143 | ||
| 134 | 144 | cat > "$FAKE_CURL_DIR/curl" <<'EOF' | |
@@ -181,12 +191,22 @@ elif ((count == 2)); then | |||
| 181 | 191 | { | |
| 182 | 192 | "id": "page-1-linked", | |
| 183 | 193 | "identifier": "SOU-21", | |
| 184 | - "title": "[sourcebot-dev/example] CVE-21: linked", | ||
| 194 | + "title": "[sourcebot-dev/example] CVE-21: open PR linked", | ||
| 185 | 195 | "url": "https://linear.app/sourcebot/issue/SOU-21/test", | |
| 186 | 196 | "priority": 1, | |
| 187 | 197 | "state": {"name": "Backlog", "type": "backlog"}, | |
| 188 | 198 | "labels": {"nodes": [{"name": "CVE"}]}, | |
| 189 | 199 | "attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/example/pull/21"}]} | |
| 200 | + }, | ||
| 201 | + { | ||
| 202 | + "id": "page-1-closed-pr", | ||
| 203 | + "identifier": "SOU-23", | ||
| 204 | + "title": "[sourcebot-dev/example] CVE-23: closed PR linked", | ||
| 205 | + "url": "https://linear.app/sourcebot/issue/SOU-23/test", | ||
| 206 | + "priority": 2, | ||
| 207 | + "state": {"name": "Backlog", "type": "backlog"}, | ||
| 208 | + "labels": {"nodes": [{"name": "CVE"}]}, | ||
| 209 | + "attachments": {"nodes": [{"url": "https://github.com/sourcebot-dev/companion/pull/23/files"}]} | ||
| 190 | 210 | } | |
| 191 | 211 | ], | |
| 192 | 212 | "pageInfo": {"hasNextPage": true, "endCursor": "next-page"} | |
@@ -220,10 +240,38 @@ printf '200' | |||
| 220 | 240 | EOF | |
| 221 | 241 | chmod +x "$FAKE_CURL_DIR/curl" | |
| 222 | 242 | ||
| 243 | + cat > "$FAKE_CURL_DIR/gh" <<'EOF' | ||
| 244 | + #!/usr/bin/env bash | ||
| 245 | + set -euo pipefail | ||
| 246 | + | ||
| 247 | + if [[ "$1" != "api" ]]; then | ||
| 248 | + echo "Unexpected gh command: $*" >&2 | ||
| 249 | + exit 1 | ||
| 250 | + fi | ||
| 251 | + | ||
| 252 | + endpoint="$2" | ||
| 253 | + printf '%s\n' "$endpoint" >> "$FAKE_GH_LOG" | ||
| 254 | + case "$endpoint" in | ||
| 255 | + repos/sourcebot-dev/example/pulls/21) | ||
| 256 | + printf 'open\n' | ||
| 257 | + ;; | ||
| 258 | + repos/sourcebot-dev/companion/pulls/23) | ||
| 259 | + printf 'closed\n' | ||
| 260 | + ;; | ||
| 261 | + *) | ||
| 262 | + echo "Unexpected GitHub API endpoint: $endpoint" >&2 | ||
| 263 | + exit 1 | ||
| 264 | + ;; | ||
| 265 | + esac | ||
| 266 | + EOF | ||
| 267 | + chmod +x "$FAKE_CURL_DIR/gh" | ||
| 268 | + | ||
| 223 | 269 | DISCOVERED=$( | |
| 224 | 270 | PATH="$FAKE_CURL_DIR:$PATH" \ | |
| 225 | 271 | FAKE_CURL_COUNT="$FAKE_CURL_COUNT" \ | |
| 226 | 272 | FAKE_CURL_PAYLOAD_DIR="$FAKE_CURL_PAYLOAD_DIR" \ | |
| 273 | + FAKE_GH_LOG="$FAKE_GH_LOG" \ | ||
| 274 | + GH_TOKEN="test-token" \ | ||
| 227 | 275 | LINEAR_API_KEY="test-key" \ | |
| 228 | 276 | LINEAR_TEAM_ID="team-key" \ | |
| 229 | 277 | LINEAR_GRAPHQL_ATTEMPTS=1 \ | |
@@ -240,6 +288,15 @@ EXPECTED_DISCOVERED='[ | |||
| 240 | 288 | "status": "Todo", | |
| 241 | 289 | "statusType": "unstarted" | |
| 242 | 290 | }, | |
| 291 | + { | ||
| 292 | + "id": "page-1-closed-pr", | ||
| 293 | + "identifier": "SOU-23", | ||
| 294 | + "title": "[sourcebot-dev/example] CVE-23: closed PR linked", | ||
| 295 | + "url": "https://linear.app/sourcebot/issue/SOU-23/test", | ||
| 296 | + "priority": 2, | ||
| 297 | + "status": "Backlog", | ||
| 298 | + "statusType": "backlog" | ||
| 299 | + }, | ||
| 243 | 300 | { | |
| 244 | 301 | "id": "page-1-unlinked", | |
| 245 | 302 | "identifier": "SOU-20", | |
@@ -251,6 +308,10 @@ EXPECTED_DISCOVERED='[ | |||
| 251 | 308 | } | |
| 252 | 309 | ]' | |
| 253 | 310 | assert_json "paginates Linear results and filters before invoking Claude" "$DISCOVERED" "$EXPECTED_DISCOVERED" | |
| 311 | + assert_json \ | ||
| 312 | + "checks every unique linked GitHub pull request state" \ | ||
| 313 | + "$(jq -Rsc 'split("\n") | map(select(length > 0))' "$FAKE_GH_LOG")" \ | ||
| 314 | + '["repos/sourcebot-dev/companion/pulls/23","repos/sourcebot-dev/example/pulls/21"]' | ||
| 254 | 315 | assert_json \ | |
| 255 | 316 | "resolves the configured Linear team identifier" \ | |
| 256 | 317 | "$(jq -c '.variables.teamId' "$FAKE_CURL_PAYLOAD_DIR/1.json")" \ | |
@@ -275,6 +336,12 @@ assert_workflow_contains \ | |||
| 275 | 336 | assert_workflow_contains \ | |
| 276 | 337 | "only invokes Claude when discovery found work" \ | |
| 277 | 338 | "if: needs.discover.outputs.has_issues == 'true'" | |
| 339 | + assert_workflow_contains \ | ||
| 340 | + "grants discovery read access to pull request state" \ | ||
| 341 | + 'pull-requests: read' | ||
| 342 | + assert_workflow_contains \ | ||
| 343 | + "authenticates GitHub API requests with the workflow token" \ | ||
| 344 | + 'GH_TOKEN: ${{ github.token }}' | ||
| 278 | 345 | assert_workflow_contains \ | |
| 279 | 346 | "passes only Linear UUIDs between jobs to avoid secret redaction" \ | |
| 280 | 347 | "issue_ids=\$(jq -c 'map(.id)'" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,7 +14,7 @@ on: | |||
| 14 | 14 | type: string | |
| 15 | 15 | default: '' | |
| 16 | 16 | max_issues: | |
| 17 | - description: Maximum number of unlinked CVEs to pass to Claude in one run. | ||
| 17 | + description: Maximum number of CVEs without an open linked PR to pass to Claude in one run. | ||
| 18 | 18 | required: false | |
| 19 | 19 | type: number | |
| 20 | 20 | default: 50 | |
@@ -28,10 +28,11 @@ on: | |||
| 28 | 28 | ||
| 29 | 29 | jobs: | |
| 30 | 30 | discover: | |
| 31 | - name: Find unlinked CVEs | ||
| 31 | + name: Find CVEs without an open PR | ||
| 32 | 32 | runs-on: ubuntu-latest | |
| 33 | 33 | permissions: | |
| 34 | 34 | contents: read | |
| 35 | + pull-requests: read | ||
| 35 | 36 | outputs: | |
| 36 | 37 | has_issues: ${{ steps.discover.outputs.has_issues }} | |
| 37 | 38 | issue_ids: ${{ steps.discover.outputs.issue_ids }} | |
@@ -49,9 +50,10 @@ jobs: | |||
| 49 | 50 | path: .cve-remediation-workflow | |
| 50 | 51 | persist-credentials: false | |
| 51 | 52 | ||
| 52 | - - name: Find open CVEs without a linked PR | ||
| 53 | + - name: Find open CVEs without an open linked PR | ||
| 53 | 54 | id: discover | |
| 54 | 55 | env: | |
| 56 | + GH_TOKEN: ${{ github.token }} | ||
| 55 | 57 | LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} | |
| 56 | 58 | LINEAR_TEAM_ID: ${{ secrets.LINEAR_TEAM_ID }} | |
| 57 | 59 | REPOSITORY: ${{ github.repository }} | |
@@ -82,7 +84,7 @@ jobs: | |||
| 82 | 84 | { | |
| 83 | 85 | echo "## CVE remediation discovery" | |
| 84 | 86 | echo | |
| 85 | - echo "Found **$total_issue_count** open CVE(s) for \`$REPOSITORY\` without a linked GitHub PR." | ||
| 87 | + echo "Found **$total_issue_count** open CVE(s) for \`$REPOSITORY\` without an open linked GitHub PR." | ||
| 86 | 88 | if ((total_issue_count > issue_count)); then | |
| 87 | 89 | echo "This run will process the first **$issue_count** by Linear priority." | |
| 88 | 90 | fi | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -7,7 +7,7 @@ on: | |||
| 7 | 7 | workflow_dispatch: | |
| 8 | 8 | inputs: | |
| 9 | 9 | max_issues: | |
| 10 | - description: Maximum number of unlinked CVEs to process. | ||
| 10 | + description: Maximum number of CVEs without an open linked PR to process. | ||
| 11 | 11 | required: false | |
| 12 | 12 | type: number | |
| 13 | 13 | default: 50 | |
| Back | FazBrowse Home | New Git URL |
0 commit comments