|
7 | 7 |
|
8 | 8 | const MAX_COLLAPSED_COMMITS = 4; |
9 | 9 |
|
| 10 | + const SECURITY_AUDIT_WORKFLOW = 'security-audit.yml'; |
| 11 | +
|
| 12 | + function getSecurityAuditRunsUrl(project: Project): string { |
| 13 | + const branch = encodeURIComponent(project.releaseLine.branch); |
| 14 | + return `https://github.com/${project.repository.owner}/${project.repository.repository}/actions/workflows/${SECURITY_AUDIT_WORKFLOW}?query=branch%3A${branch}`; |
| 15 | + } |
| 16 | +
|
| 17 | + /** |
| 18 | + * Map a shields.io badge's right-side text to a status bucket. Anything we don't recognize |
| 19 | + * (including "no status", "repo or workflow not found", missing repo) falls through to 'unknown'. |
| 20 | + */ |
| 21 | + function classifyShieldsMessage(svgText: string): Project['ciStatus'] { |
| 22 | + if (svgText.includes('>passing<')) return 'success'; |
| 23 | + if (svgText.includes('>failing<')) return 'failure'; |
| 24 | + if (svgText.includes('>in progress<') || svgText.includes('>queued<') || svgText.includes('>starting<') || svgText.includes('>waiting<')) return 'pending'; |
| 25 | + return 'unknown'; |
| 26 | + } |
| 27 | +
|
| 28 | + /** |
| 29 | + * Resolve the CI status for the project's security-audit workflow on its release-line branch |
| 30 | + * by reading the shields.io badge SVG. Same network cost as rendering the badge image directly, |
| 31 | + * but lets us drop the status text and treat 404-style responses as 'unknown' instead of red. |
| 32 | + */ |
| 33 | + async function fetchCiStatus(project: Project): Promise<Project['ciStatus']> { |
| 34 | + const branch = encodeURIComponent(project.releaseLine.branch); |
| 35 | + const url = `https://img.shields.io/github/actions/workflow/status/${project.repository.owner}/${project.repository.repository}/${SECURITY_AUDIT_WORKFLOW}?branch=${branch}`; |
| 36 | + try { |
| 37 | + const response = await fetch(url); |
| 38 | + if (!response.ok) return 'unknown'; |
| 39 | + return classifyShieldsMessage(await response.text()); |
| 40 | + } catch (e) { |
| 41 | + console.error(`${project.name}: failed to fetch CI status`, e); |
| 42 | + return 'unknown'; |
| 43 | + } |
| 44 | + } |
| 45 | +
|
10 | 46 | let projects = $state(getAllProjects().filter((x) => x.hide !== true)); |
11 | 47 |
|
12 | 48 | /** |
|
372 | 408 | const tagPackageLockJson = JSON.parse(tagResponse); |
373 | 409 |
|
374 | 410 | project.unreleasedCommits = await fetchUnreleasedCommits(project); |
| 411 | + project.ciStatus = await fetchCiStatus(project); |
375 | 412 |
|
376 | 413 | //now update the dependencies |
377 | 414 | for (const dependency of project.dependencies) { |
|
605 | 642 | </svg> |
606 | 643 | </a> |
607 | 644 | {/if} |
| 645 | + <a |
| 646 | + class="status-badge status-badge-{project.ciStatus ?? 'unknown'}" |
| 647 | + target="_blank" |
| 648 | + href={getSecurityAuditRunsUrl(project)} |
| 649 | + title={`Security audit on ${project.releaseLine.branch} (${project.ciStatus ?? 'unknown'})`} |
| 650 | + aria-label={`Security audit on ${project.releaseLine.branch}: ${project.ciStatus ?? 'unknown'}`} |
| 651 | + > |
| 652 | + <svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"> |
| 653 | + <rect x="3" y="8" width="10" height="7" rx="1" fill="white" /> |
| 654 | + <path d="M5 8V5a3 3 0 0 1 6 0v3" fill="none" stroke="white" stroke-width="2" /> |
| 655 | + </svg> |
| 656 | + </a> |
608 | 657 | </span> |
609 | 658 | <a |
610 | 659 | class="button release-status-button" |
|
1076 | 1125 | display: flex; |
1077 | 1126 | align-items: center; |
1078 | 1127 | gap: 0.4rem; |
| 1128 | + flex-wrap: wrap; |
| 1129 | + } |
| 1130 | +
|
| 1131 | + .status-badge { |
| 1132 | + display: inline-flex; |
| 1133 | + align-items: center; |
| 1134 | + justify-content: center; |
| 1135 | + width: 22px; |
| 1136 | + height: 18px; |
| 1137 | + border-radius: 3px; |
| 1138 | + background-color: #9f9f9f; |
| 1139 | + line-height: 1; |
| 1140 | + text-decoration: none; |
1079 | 1141 | } |
1080 | 1142 |
|
| 1143 | + .status-badge:hover { |
| 1144 | + text-decoration: none; |
| 1145 | + filter: brightness(1.1); |
| 1146 | + } |
| 1147 | +
|
| 1148 | + .status-badge-success { background-color: #4c1; } |
| 1149 | + .status-badge-failure { background-color: #e05d44; } |
| 1150 | + .status-badge-pending { background-color: #dfb317; } |
| 1151 | + .status-badge-unknown { background-color: #9f9f9f; } |
| 1152 | +
|
1081 | 1153 | .version-links .npm-link { |
1082 | 1154 | display: inline-flex; |
1083 | 1155 | align-items: center; |
|
0 commit comments