Skip to content

Commit d0a3fa5

Browse files
Add security-audit status badge to project cards (#5)
2 parents 3a6b1fe + eface6c commit d0a3fa5

2 files changed

Lines changed: 79 additions & 0 deletions

File tree

src/App.svelte

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,42 @@
77
88
const MAX_COLLAPSED_COMMITS = 4;
99
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+
1046
let projects = $state(getAllProjects().filter((x) => x.hide !== true));
1147
1248
/**
@@ -372,6 +408,7 @@
372408
const tagPackageLockJson = JSON.parse(tagResponse);
373409
374410
project.unreleasedCommits = await fetchUnreleasedCommits(project);
411+
project.ciStatus = await fetchCiStatus(project);
375412
376413
//now update the dependencies
377414
for (const dependency of project.dependencies) {
@@ -605,6 +642,18 @@
605642
</svg>
606643
</a>
607644
{/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>
608657
</span>
609658
<a
610659
class="button release-status-button"
@@ -1076,8 +1125,31 @@
10761125
display: flex;
10771126
align-items: center;
10781127
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;
10791141
}
10801142
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+
10811153
.version-links .npm-link {
10821154
display: inline-flex;
10831155
align-items: center;

src/projects.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ export interface Project {
8585
* Temporarily ignored for this session (treated as up-to-date for tier readiness)
8686
*/
8787
ignored?: boolean;
88+
89+
/**
90+
* Conclusion of the latest `security-audit.yml` workflow run on this card's release-line branch.
91+
* Parsed out of the shields.io badge response (no GitHub API calls).
92+
* `unknown` covers "no status", missing workflow, missing repo, and parse failures.
93+
*/
94+
ciStatus?: 'success' | 'failure' | 'pending' | 'unknown';
8895
}
8996

9097
export function getAllProjects(): Project[] {

0 commit comments

Comments
 (0)