diff --git a/scripts/tooling/cli/misc/assets/report_template.html b/scripts/tooling/cli/misc/assets/report_template.html index 60e36f723c3..5894b75e5c5 100644 --- a/scripts/tooling/cli/misc/assets/report_template.html +++ b/scripts/tooling/cli/misc/assets/report_template.html @@ -256,25 +256,33 @@ border-top: 1px solid var(--border); padding-top: 1rem; } - /* ── Tabs (new) ─────────────────────────────────────────────── */ - .tabs { + /* ── Dashboard tabs ───────────────────────────────────────── */ + .dashboard-tabs { max-width: 1400px; - margin: 0 auto 1.2rem; + margin: 0 auto 1.5rem; display: flex; gap: 0.5rem; + flex-wrap: wrap; border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; } - .tab-btn { - font-size: 0.85rem; - padding: 0.4rem 0.9rem; - border-radius: 6px 6px 0 0; + .dashboard-tab { + border: 1px solid var(--border); border-bottom: none; + border-radius: 6px 6px 0 0; + background: var(--surface); + color: var(--muted); + padding: 0.4rem 0.9rem; + display: inline-flex; + align-items: center; + gap: 0.42rem; + font-size: 0.8rem; } - .tab-btn.active { + .dashboard-tab:hover, + .dashboard-tab.active { + border-color: var(--accent); background: var(--bg); color: #e6edf3; - border-color: var(--accent); } .tab-panel { display: none; } .tab-panel.active { display: block; } @@ -327,6 +335,86 @@ font-size: 0.82rem; } .trace-links a:hover { text-decoration: underline; } + + /* Dependency integration */ + .dependency-section { + max-width: 1400px; + margin: 0 auto 1.5rem; + } + .dependency-section h2 { + font-size: 1.1rem; + margin-bottom: 0.35rem; + } + .dependency-section > p { + color: var(--muted); + font-size: 0.82rem; + margin-bottom: 1rem; + } + .dependency-filters { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 1rem; + } + .dependency-panel { display: none; } + .dependency-panel.active { display: block; } + .dependency-legend { + color: var(--muted); + font-size: 0.8rem; + margin: 0 0 1rem; + padding-left: 1.2rem; + } + .dependency-legend li { margin-bottom: 0.35rem; } + .dependency-legend strong { color: var(--text); } + .dependency-table-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 1rem; + overflow-x: auto; + } + .dependency-table-card h3 { + font-size: 0.95rem; + padding: 0.8rem 1rem; + border-bottom: 1px solid var(--border); + } + .dependency-table { + width: 100%; + border-collapse: collapse; + font-size: 0.82rem; + } + .dependency-table th, + .dependency-table td { + padding: 0.55rem 0.7rem; + text-align: left; + vertical-align: top; + border-bottom: 1px solid var(--border); + white-space: nowrap; + } + .dependency-table tr:last-child td { border-bottom: none; } + .dependency-table th { color: #e6edf3; font-weight: 600; } + .dependency-table td { color: var(--text); } + .dependency-table .version-cell { font-family: var(--mono); } + .integration-badges { display: flex; gap: 0.35rem; flex-wrap: wrap; } + .integration-badge { + display: inline-block; + border: 1px solid; + border-radius: 20px; + padding: 0.18rem 0.5rem; + font-size: 0.72rem; + font-weight: 500; + } + .integration-aligned { color: var(--green); border-color: #3fb95055; background: #3fb95011; } + .integration-mismatch { color: var(--red); border-color: #f8514955; background: #f8514911; } + .integration-update { color: var(--yellow); border-color: #d2992255; background: #d2992211; } + .dependency-loading, + .dependency-empty, + .dependency-error { + color: var(--muted); + font-size: 0.82rem; + padding: 1rem; + } + .dependency-error { color: var(--red); } @@ -334,12 +422,6 @@

Integration Dashboard

Snapshot: {{ timestamp }}

-
- - - -
-
@@ -349,16 +431,31 @@

Integration Dashboard

+
+ 🔒 Add a GitHub personal access token (PAT) for live status — exact commit counts fetched directly from GitHub. + Enter it in the token field above, or + create one here. + Without a token, up-to-date / behind status is still shown from data embedded at report generation time. + Your PAT is not sent anywhere — it is only kept in the local cache of this page. +
+ + +
-
- 🔒 Add a GitHub personal access token (PAT) for live status — exact commit counts fetched directly from GitHub. - Enter it in the token field above, or - create one here. - Without a token, up-to-date / behind status is still shown from data embedded at report generation time. - Your PAT is not sent anywhere — it is only kept in the local cache of this page. -
-
@@ -367,6 +464,30 @@

Integration Dashboard

+ +
+
+

Dependency Integration

+

Dependencies between known-good modules, their declared versions, and the latest available repository tags.

+
+ + +
+
+
    +
  • Version Used: component version used by the Module.
  • +
  • Known Good Version: component version which is pinned in known_good.json.
  • +
  • Latest Available Version: latest release found in the component repository.
  • +
  • Integration: Aligned when the Module is using the same version as the one pinned in known_good.json; Mismatch when the Module is using a different version than the one pinned in known_good.json; Update Available when a newer component release is available to be integrated into known_good.json.
  • +
+
Loading dependency information…
+
+
+
Loading dependency information…
+
+
+
+
@@ -467,7 +588,7 @@

SBOM & Dependency Tools

// ── Tab switching ─────────────────────────────────────────────── (function() { - const tabs = document.querySelectorAll('#main-tabs .tab-btn'); + const tabs = document.querySelectorAll('#main-tabs .dashboard-tab'); const panels = document.querySelectorAll('.tab-panel'); tabs.forEach(btn => { btn.addEventListener('click', () => { @@ -475,13 +596,33 @@

SBOM & Dependency Tools

t.classList.remove('active')); panels.forEach(p => p.classList.remove('active')); btn.classList.add('active'); + tabs.forEach(t => t.setAttribute('aria-selected', t === btn ? 'true' : 'false')); document.getElementById('tab-' + target).classList.add('active'); }); }); })(); + (function() { + const tabs = document.querySelectorAll('[data-dependency-tab]'); + const panels = document.querySelectorAll('.dependency-panel'); + tabs.forEach(btn => { + btn.addEventListener('click', () => { + const target = btn.dataset.dependencyTab; + tabs.forEach(tab => { + const active = tab === btn; + tab.classList.toggle('active', active); + tab.setAttribute('aria-selected', active ? 'true' : 'false'); + }); + panels.forEach(panel => panel.classList.toggle( + 'active', panel.id === `dependency-panel-${target}` + )); + }); + }); + })(); + const MODULES = {{ modules_json | safe }}; const SBOM_PACKAGES = {{ sbom_packages_json | safe }}; + const DEPENDENCY_MODULES = MODULES.filter(mod => mod.owner_repo); // ── Token storage (unchanged) ─────────────────────────────────────── function getToken() { return localStorage.getItem('gh_token') || ''; } @@ -523,7 +664,7 @@

SBOM & Dependency Tools

SBOM & Dependency Tools char.charCodeAt(0)); + return new TextDecoder().decode(bytes); + } + + async function fetchModuleFile(mod) { + const refs = []; + if (mod.hash) refs.push(mod.hash); + if (mod.version) { + refs.push(mod.version); + if (!mod.version.startsWith('v')) refs.push(`v${mod.version}`); + } + + for (const ref of refs) { + const url = `https://api.github.com/repos/${mod.owner_repo}/contents/MODULE.bazel?ref=${encodeURIComponent(ref)}`; + const res = await fetch(url, { headers: authHeaders() }); + if (!res.ok) continue; + const data = await res.json(); + if (data.encoding === 'base64' && data.content) return decodeBase64(data.content); + } + throw new Error(`MODULE.bazel not found at the pin for ${mod.name}`); + } + + function parseKnownGoodDependencies(moduleText, knownGoodNames) { + const dependencies = []; + const bazelDepPattern = /bazel_dep\s*\(([^)]*)\)/g; + let match; + while ((match = bazelDepPattern.exec(moduleText)) !== null) { + const nameMatch = match[1].match(/name\s*=\s*["']([^"']+)["']/); + if (!nameMatch || !knownGoodNames.has(nameMatch[1])) continue; + const versionMatch = match[1].match(/version\s*=\s*["']([^"']+)["']/); + dependencies.push({ name: nameMatch[1], version: versionMatch ? versionMatch[1] : null }); + } + return dependencies; + } + + function parseDependencies(moduleText, dependencyNames) { + const dependencies = []; + const bazelDepPattern = /bazel_dep\s*\(([^)]*)\)/g; + let match; + while ((match = bazelDepPattern.exec(moduleText)) !== null) { + const nameMatch = match[1].match(/name\s*=\s*["']([^"']+)["']/); + if (!nameMatch || !dependencyNames.has(nameMatch[1])) continue; + const versionMatch = match[1].match(/version\s*=\s*["']([^"']+)["']/); + dependencies.push({ + name: nameMatch[1], + version: versionMatch ? versionMatch[1] : null, + }); + } + return dependencies; + } + + function versionParts(value) { + const match = String(value || '').replace(/^v/, '').match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/); + return match ? [Number(match[1]), Number(match[2] || 0), Number(match[3] || 0)] : null; + } + + function compareVersions(left, right) { + const a = versionParts(left); + const b = versionParts(right); + if (!a || !b) return null; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return a[i] - b[i]; + } + return 0; + } + + async function fetchLatestVersion(ownerRepo) { + const url = `https://api.github.com/repos/${ownerRepo}/tags?per_page=100`; + const res = await fetch(url, { headers: authHeaders() }); + if (!res.ok) return null; + const tags = await res.json(); + const versions = tags + .map(tag => tag.name) + .filter(tag => versionParts(tag)) + .sort((a, b) => compareVersions(b, a)); + return versions[0] || null; + } + + function displayKnownGoodVersion(mod) { + return mod.version || (mod.hash ? mod.hash.slice(0, 10) : '—'); + } + + function dependencyBadges(versionUsed, knownGoodVersion, latestVersion) { + const badges = []; + const usedComparison = compareVersions(versionUsed, knownGoodVersion); + const aligned = usedComparison === 0 || versionUsed === knownGoodVersion; + badges.push(`${aligned ? 'Aligned' : 'Mismatch'}`); + if (latestVersion && compareVersions(latestVersion, versionUsed) > 0) { + badges.push('Update Available'); + } + return badges.join(''); + } + + async function renderDependencyIntegration() { + const container = document.getElementById('dependency-tables'); + const knownGoodByName = new Map(MODULES.map(mod => [mod.name, mod])); + const knownGoodNames = new Set(knownGoodByName.keys()); + const dependencyTables = []; + + for (const source of DEPENDENCY_MODULES) { + try { + const moduleText = await fetchModuleFile(source); + const dependencies = parseKnownGoodDependencies(moduleText, knownGoodNames); + if (!dependencies.length) continue; + + const rows = []; + for (const dependency of dependencies) { + const knownGood = knownGoodByName.get(dependency.name); + const latestVersion = await fetchLatestVersion(knownGood.owner_repo); + const knownGoodVersion = displayKnownGoodVersion(knownGood); + rows.push(` + + ${dependency.name} + ${dependency.version || 'unversioned'} + ${knownGoodVersion} + ${latestVersion || 'unknown'} +
${dependencyBadges(dependency.version, knownGood.version, latestVersion)}
+ + `); + } + + dependencyTables.push(` +
+

${source.name}

+ + + ${rows.join('')} +
DependencyVersion UsedKnown Good VersionLatest Available VersionIntegration
+
+ `); + } catch (error) { + dependencyTables.push(`
${error.message}
`); + } + } + + container.innerHTML = dependencyTables.length + ? dependencyTables.join('') + : '
No dependencies between known-good modules were found.
'; + } + + const OTHER_DEPENDENCIES = [ + 'aspect_rules_lint', + 'aspect_rules_py', + 'rules_cc', + 'rules_python', + ]; + + async function renderOtherDependencies() { + const container = document.getElementById('other-dependency-tables'); + const dependencyNames = new Set(OTHER_DEPENDENCIES); + const foundDependencies = new Map(OTHER_DEPENDENCIES.map(name => [name, []])); + + for (const source of DEPENDENCY_MODULES) { + try { + const moduleText = await fetchModuleFile(source); + const dependencies = parseDependencies(moduleText, dependencyNames); + dependencies.forEach(dependency => { + foundDependencies.get(dependency.name).push({ + module: source.name, + version: dependency.version, + }); + }); + } catch { + } + } + + container.innerHTML = OTHER_DEPENDENCIES.map(name => { + const rows = foundDependencies.get(name); + return ` +
+

${name}

+ + + ${rows.length + ? rows.map(row => ``).join('') + : ''} +
ModuleVersion Used
${row.module}${row.version || 'unversioned'}
No version declared in known-good modules.
+
+ `; + }).join(''); + } + + async function resolveVersionRef(ownerRepo, version) { + const candidates = [version, `v${version}`]; + for (const candidate of candidates) { + const url = `https://api.github.com/repos/${ownerRepo}/git/ref/tags/${encodeURIComponent(candidate)}`; + const res = await fetch(url, { headers: authHeaders() }); + if (res.ok) { + const data = await res.json(); + return data.object?.sha || candidate; + } + } + throw new Error(`version tag ${version} not found`); + } + // ── Status helpers ───────────────────────────────────────────────────── function statusClass(behind) { if (behind === 0) return 'ok'; @@ -630,9 +967,12 @@

SBOM & Dependency Tools

@@ -645,7 +985,7 @@

SBOM & Dependency Tools

pinned - ${shortHash} + ${pinnedValue}
@@ -719,7 +1059,7 @@

SBOM & Dependency Tools

SBOM & Dependency ToolsSBOM & Dependency Tools render(false)); renderAuthBar(); render(true); // use cache on initial page load + renderDependencyIntegration(); + renderOtherDependencies(); // ── SBOM table rendering (optional) ──────────────────────────────────── const sbomData = typeof window.SBOM_DATA !== 'undefined' ? window.SBOM_DATA : null;