|
17 | 17 | # that governs it ever running on the PR that adds it. |
18 | 18 | - 'osv-scanner.toml' |
19 | 19 | - 'scripts/check-osv-exemptions.mjs' |
| 20 | + # The pull_request verdict of the OSV step (#22082): an edit to it must |
| 21 | + # be exercised on the PR that makes it. |
| 22 | + - 'scripts/osv-base-relative.mjs' |
20 | 23 | # Re-run when the workflow itself changes, so edits to these gates are |
21 | 24 | # exercised on the PR that introduces them. |
22 | 25 | - '.github/workflows/validate-deps.yml' |
|
32 | 35 | validate: |
33 | 36 | name: Validate Package Dependencies |
34 | 37 | runs-on: ubuntu-latest |
| 38 | + # Measured over the 100 most recent completed runs: p50 1.3 min, max |
| 39 | + # 1.9 min (#22082). 15 leaves room for a cold pnpm store and two OSV |
| 40 | + # scans on a pull request, and stops a hung step long before GitHub's |
| 41 | + # 360-minute default would. |
| 42 | + timeout-minutes: 15 |
35 | 43 | permissions: |
36 | 44 | contents: read |
37 | 45 | issues: write |
@@ -138,12 +146,111 @@ jobs: |
138 | 146 | node scripts/check-osv-exemptions.mjs --self-test |
139 | 147 | node scripts/check-osv-exemptions.mjs |
140 | 148 |
|
| 149 | + # Two verdicts, one scanner pin (#22082, ruling A). |
| 150 | + # |
| 151 | + # `schedule` and `workflow_dispatch` judge `main` ABSOLUTELY: every |
| 152 | + # advisory the lockfile matches fails the step. That daily red is the |
| 153 | + # signal that files the finding card, and this step is unchanged by the |
| 154 | + # pull_request verdict below. |
| 155 | + # |
| 156 | + # A pull request is judged BASE-RELATIVELY: it answers for the advisories |
| 157 | + # it introduces, not for the ones `main` already carries -- those are |
| 158 | + # `main`'s to fix, and a pull request that changes no dependency cannot |
| 159 | + # fix them. The steps below scan the pull request's lockfile and the merge |
| 160 | + # base's, each under its own `osv-scanner.toml`, and |
| 161 | + # scripts/osv-base-relative.mjs fails only on an advisory the merge base |
| 162 | + # does not match; the others become notices naming the advisory and its |
| 163 | + # open finding card. A scan that does not reach a verdict fails closed. |
| 164 | + # |
| 165 | + # The pin is an untagged upstream commit after the v2.5.0 tag whose |
| 166 | + # action.yml runs the ghcr.io/google/osv-scanner-action:v2.5.0 image (the |
| 167 | + # comment here used to read v2.3.8). Keep all three `uses:` lines on ONE |
| 168 | + # sha: the two verdicts must come from the same scanner. |
141 | 169 | - name: Audit dependencies for known vulnerabilities (OSV-Scanner) |
142 | | - uses: google/osv-scanner-action/osv-scanner-action@f4cfcc01edc9c8b756a9b873b7a623ca674da51e # v2.3.8 |
| 170 | + if: github.event_name != 'pull_request' |
| 171 | + uses: google/osv-scanner-action/osv-scanner-action@f4cfcc01edc9c8b756a9b873b7a623ca674da51e # untagged, v2.5.0 image |
143 | 172 | with: |
144 | 173 | scan-args: |- |
145 | 174 | --lockfile=pnpm-lock.yaml |
146 | 175 |
|
| 176 | + # The checkout is the pull request's test merge (refs/pull/N/merge) at |
| 177 | + # depth 1. Its first parent is the base-branch commit GitHub merged the |
| 178 | + # pull request into -- the merge base of the scanned tree and the base |
| 179 | + # branch -- so ONE depth-1 fetch of that sha is all the history this needs |
| 180 | + # (never fetch-depth: 0). When neither pnpm-lock.yaml nor osv-scanner.toml |
| 181 | + # differs from it, the pull request cannot introduce an advisory and no |
| 182 | + # scan runs. The ledger is part of the condition: a pull request that |
| 183 | + # deletes an exemption changes the verdict without touching the lockfile. |
| 184 | + - name: Stage the OSV comparison against the merge base (pull request) |
| 185 | + id: osv-stage |
| 186 | + if: github.event_name == 'pull_request' |
| 187 | + run: | |
| 188 | + node scripts/osv-base-relative.mjs --self-test |
| 189 | + mapfile -t parents < <(git cat-file -p HEAD | sed -n 's/^parent //p') |
| 190 | + if [ "${#parents[@]}" -ne 2 ]; then |
| 191 | + echo "::error title=OSV base-relative verdict unavailable::HEAD is not the pull request's two-parent test merge (${#parents[@]} parent(s) found), so there is no merge base to judge against" |
| 192 | + exit 1 |
| 193 | + fi |
| 194 | + base="${parents[0]}" |
| 195 | + git fetch --no-tags --depth=1 origin "$base" |
| 196 | + echo "base=$base" >> "$GITHUB_OUTPUT" |
| 197 | + if git diff --quiet "$base" HEAD -- pnpm-lock.yaml osv-scanner.toml; then |
| 198 | + echo "::notice title=OSV-Scanner verdict (base-relative)::lockfile unchanged — inherits main's verdict. pnpm-lock.yaml and osv-scanner.toml are identical to the merge base ${base:0:12}, so this pull request introduces no advisory; main's daily scheduled scan judges that lockfile." |
| 199 | + echo "compare=false" >> "$GITHUB_OUTPUT" |
| 200 | + exit 0 |
| 201 | + fi |
| 202 | + for side in base head; do |
| 203 | + if [ "$side" = base ]; then rev="$base"; else rev=HEAD; fi |
| 204 | + mkdir -p ".osv-compare/$side" |
| 205 | + git show "$rev:pnpm-lock.yaml" > ".osv-compare/$side/pnpm-lock.yaml" |
| 206 | + git show "$rev:osv-scanner.toml" > ".osv-compare/$side/osv-scanner.toml" |
| 207 | + done |
| 208 | + echo "compare=true" >> "$GITHUB_OUTPUT" |
| 209 | +
|
| 210 | + # Both scans may exit non-zero (findings), so neither stops the job; the |
| 211 | + # judge below reads each result file against its step's outcome. |
| 212 | + # `--config` is explicit because the scanner looks for osv-scanner.toml |
| 213 | + # only in the scanned lockfile's own directory (measured on 2.5.0: a |
| 214 | + # ledger one directory up is not applied). |
| 215 | + - name: OSV-Scanner on the merge base's lockfile (pull request) |
| 216 | + id: osv-scan-base |
| 217 | + if: steps.osv-stage.outputs.compare == 'true' |
| 218 | + continue-on-error: true |
| 219 | + uses: google/osv-scanner-action/osv-scanner-action@f4cfcc01edc9c8b756a9b873b7a623ca674da51e # untagged, v2.5.0 image |
| 220 | + with: |
| 221 | + scan-args: |- |
| 222 | + --config=.osv-compare/base/osv-scanner.toml |
| 223 | + --format=json |
| 224 | + --output-file=.osv-compare/base/results.json |
| 225 | + --lockfile=.osv-compare/base/pnpm-lock.yaml |
| 226 | +
|
| 227 | + - name: OSV-Scanner on the pull request's lockfile (pull request) |
| 228 | + id: osv-scan-head |
| 229 | + if: steps.osv-stage.outputs.compare == 'true' |
| 230 | + continue-on-error: true |
| 231 | + uses: google/osv-scanner-action/osv-scanner-action@f4cfcc01edc9c8b756a9b873b7a623ca674da51e # untagged, v2.5.0 image |
| 232 | + with: |
| 233 | + scan-args: |- |
| 234 | + --config=.osv-compare/head/osv-scanner.toml |
| 235 | + --format=json |
| 236 | + --output-file=.osv-compare/head/results.json |
| 237 | + --lockfile=.osv-compare/head/pnpm-lock.yaml |
| 238 | +
|
| 239 | + - name: Judge the pull request's OSV findings against the merge base's (pull request) |
| 240 | + if: steps.osv-stage.outputs.compare == 'true' |
| 241 | + env: |
| 242 | + BASE_OUTCOME: ${{ steps.osv-scan-base.outcome }} |
| 243 | + HEAD_OUTCOME: ${{ steps.osv-scan-head.outcome }} |
| 244 | + BASE_SHA: ${{ steps.osv-stage.outputs.base }} |
| 245 | + # Read-only use: the open-issue search that names an inherited |
| 246 | + # advisory's finding card. The verdict never depends on it. |
| 247 | + GITHUB_TOKEN: ${{ github.token }} |
| 248 | + run: | |
| 249 | + node scripts/osv-base-relative.mjs \ |
| 250 | + --base-results .osv-compare/base/results.json --base-outcome "$BASE_OUTCOME" \ |
| 251 | + --head-results .osv-compare/head/results.json --head-outcome "$HEAD_OUTCOME" \ |
| 252 | + --base-sha "$BASE_SHA" |
| 253 | +
|
147 | 254 | - name: List outdated packages |
148 | 255 | if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' |
149 | 256 | run: | |
|
0 commit comments