|
70 | 70 | run: | |
71 | 71 | node - << 'JSEOF' |
72 | 72 | const fs = require('fs'); |
| 73 | + const path = require('path'); |
73 | 74 | const { execFileSync } = require('child_process'); |
74 | 75 |
|
| 76 | + // The milestone/board pre-release rule, shared with dependabot-triage.yml. |
| 77 | + // GITHUB_ACTION_PATH is this action's own directory, so the relative path holds |
| 78 | + // wherever the caller checked the repository out. |
| 79 | + const { best } = require( |
| 80 | + path.join(process.env.GITHUB_ACTION_PATH, '..', '..', 'scripts', 'prerelease-rank.js')); |
| 81 | +
|
75 | 82 | const REPO = process.env.REPO; |
76 | 83 | const PROJECT = process.env.PROJECT; |
77 | 84 | const TYPE = process.env.TYPE; |
@@ -341,23 +348,33 @@ runs: |
341 | 348 | return version; |
342 | 349 | }; |
343 | 350 |
|
344 | | - // Existing milestone titles, fetched once and reused for every PR in this repo. |
| 351 | + // Open milestone titles, fetched once and reused for every PR in this repo. |
345 | 352 | let milestoneTitles = null; |
346 | | - const milestoneExists = title => { |
| 353 | + const openMilestones = () => { |
347 | 354 | if (milestoneTitles === null) { |
348 | 355 | // --paginate, with --slurp instead of --jq because gh refuses both together: |
349 | 356 | // a repository past a hundred open milestones would otherwise lose the newest |
350 | 357 | // ones, and those are the only ones this ever looks for. |
351 | 358 | const r = gh(['api', '--paginate', '--slurp', |
352 | 359 | `repos/${REPO}/milestones?state=open&per_page=100`]); |
353 | 360 | milestoneTitles = r.ok |
354 | | - ? new Set(parseJson(r.out, []).flat().map(m => m.title)) |
355 | | - : new Set(); |
| 361 | + ? parseJson(r.out, []).flat().map(m => m.title) |
| 362 | + : []; |
356 | 363 | if (!r.ok) warnings.push(`could not list milestones: ${r.err}`); |
357 | 364 | } |
358 | | - return milestoneTitles.has(title); |
| 365 | + return milestoneTitles; |
359 | 366 | }; |
360 | 367 |
|
| 368 | + // The branch version gives the train's GA number (5.1.0), but a train ships |
| 369 | + // milestones first, so the milestone that actually exists is 5.1.0-M1, then -M2, |
| 370 | + // then -RC1. Matching the GA title literally means every PR against a freshly |
| 371 | + // branched train is reported as "milestone 5.1.0 does not exist" until GA - which |
| 372 | + // is most of the train's life, and is exactly what happened when 2026.0.0 opened. |
| 373 | + // |
| 374 | + // Only open milestones are considered, so a shipped milestone is never chosen; of |
| 375 | + // what remains the furthest along wins, which is the one being worked toward. |
| 376 | + const resolveMilestone = base => best(openMilestones(), base); |
| 377 | +
|
361 | 378 | const scanned = prs.map(pr => { |
362 | 379 | const checks = (pr.statusCheckRollup || []).map(normalizeCheck); |
363 | 380 | const failing = checks.filter(c => FAILING.has(c.state)); |
@@ -398,11 +415,15 @@ runs: |
398 | 415 | milestoneState = 'unresolved'; |
399 | 416 | projectState = 'unresolved'; |
400 | 417 | } else { |
401 | | - expectedMilestone = version.replace(/-SNAPSHOT$/, ''); |
| 418 | + // Fall back to the bare GA title when nothing matches, so a 'missing' |
| 419 | + // verdict still names the version it was looking for. |
| 420 | + const milestoneBase = version.replace(/-SNAPSHOT$/, ''); |
| 421 | + const resolved = resolveMilestone(milestoneBase); |
| 422 | + expectedMilestone = resolved || milestoneBase; |
402 | 423 | const current = pr.milestone?.title || null; |
403 | 424 | if (current === expectedMilestone) milestoneState = 'set'; |
404 | 425 | else if (current) milestoneState = 'mismatch'; |
405 | | - else if (!milestoneExists(expectedMilestone)) milestoneState = 'missing'; |
| 426 | + else if (!resolved) milestoneState = 'missing'; |
406 | 427 | else milestoneState = 'unset'; |
407 | 428 |
|
408 | 429 | // Boards are OSS-only by design; commercial PRs get a milestone alone. |
|
0 commit comments