-
Notifications
You must be signed in to change notification settings - Fork 52
Feat/CVE report visibility #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
9e225d6
feat(cve-report): add canonical CVE-keyed aggregation
bluvulture bcfad7f
test(cve-report): cover status-key regression in cve-aggregate.sh
bluvulture 99a59bd
feat(cve-report): add summary renderer
bluvulture c2818a6
test(cve-report): pin computed values in summary sections, not just h…
bluvulture adb7e85
feat(cve-report): add table renderer and render-limit guard
bluvulture 9b2df10
test(cve-report): pin CVE-2025-6020 row and the 90% size-guard boundary
bluvulture 93a0155
feat(cve-report): render severity-split report, fix re-run artifact d…
bluvulture 9a71661
fix(cve-report): reconcile the Images table with the tabulated counts
bluvulture 07df1fb
feat(cve-report): add docs index and pin report branch expectation
bluvulture 6aa0990
fix(cve-report): key rows on every rendered attribute, correct report…
bluvulture File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| #!/usr/bin/env bash | ||
| # Aggregate the release workflow's per-image Trivy scans into one canonical, | ||
| # CVE-keyed JSON document -- the single interface every renderer reads. | ||
| # | ||
| # Usage: cve-aggregate.sh <data-dir> <timestamp> | ||
| # <data-dir>/<key>.meta.json {image,variant,arch,plain_digest,hardened_digest} | ||
| # <data-dir>/<key>.plain.json full Trivy JSON of the plain image | ||
| # <data-dir>/<key>.hardened.json full Trivy JSON of the hardened image (may be absent) | ||
| # | ||
| # Emits on stdout: | ||
| # {generated, images:[{image,variant,arch,plain_digest,hardened_digest, | ||
| # hardened_state,counts{},fixable_count}], | ||
| # cves:[{id,severity,pkg,status,fix,affects:[image-index,...]}]} | ||
| # | ||
| # Rows are keyed on EVERY rendered attribute -- (id, pkg, status, severity, fix) -- | ||
| # NOT on id alone. A CVE affecting two packages, fixed on one and residual on the | ||
| # other, must stay two entries; and a CVE fixed in one image but residual in another | ||
| # must stay two entries. Keying on id alone silently merges these and loses rows (see | ||
| # the multipkg regression test). | ||
| # | ||
| # severity and fix are in the key for the same reason, one step further: the group is | ||
| # collapsed into a single row that renders .[0] of each attribute, so any attribute | ||
| # NOT in the key gets one member of the group attributed to all of them. This report | ||
| # aggregates several Debian bases (bullseye/bookworm/trixie) in one run, and the same | ||
| # (id, pkg, status) legitimately carries a different `fix` across them, because the | ||
| # `fixed` branch renders InstalledVersion + " -> " + FixedVersion and Debian package | ||
| # versions differ per release. Keying on (id, pkg, status) alone therefore attributes | ||
| # ONE base version pair to every affected image. That is invisible while no CVE has a | ||
| # FixedVersion (every row renders "no fix"), and silently wrong the day one does. | ||
| # Rows that genuinely differ stay separate rows with accurate `affects` lists instead. | ||
| # | ||
| # Deliberately streams per-file rather than slurping: the real data set is ~1 GB of | ||
| # Trivy JSON across 50 scans, which `jq -s` cannot hold. | ||
| set -euo pipefail | ||
|
|
||
| data_dir="${1:?usage: cve-aggregate.sh <data-dir> <timestamp>}" | ||
| timestamp="${2:?usage: cve-aggregate.sh <data-dir> <timestamp>}" | ||
|
|
||
| work="$(mktemp -d)" | ||
| trap 'rm -rf "$work"' EXIT | ||
|
|
||
| metas="$(find "$data_dir" -maxdepth 1 -name '*.meta.json' | LC_ALL=C sort)" | ||
| if [ -z "$metas" ]; then | ||
| jq -n --arg ts "$timestamp" '{generated:$ts, images:[], cves:[]}' | ||
| exit 0 | ||
| fi | ||
|
|
||
| : > "${work}/images.jsonl" | ||
| : > "${work}/rows.tsv" | ||
|
|
||
| idx=0 | ||
| while IFS= read -r meta; do | ||
| [ -n "$meta" ] || continue | ||
| key="$(basename "$meta" .meta.json)" | ||
| plain="${data_dir}/${key}.plain.json" | ||
| hardened="${data_dir}/${key}.hardened.json" | ||
|
|
||
| # A meta without a plain scan means the scan step dropped this variant; skip it | ||
| # rather than emitting an image with no data. | ||
| [ -f "$plain" ] || continue | ||
|
|
||
| pd="$(jq -r '.plain_digest' "$meta")" | ||
| hd="$(jq -r '.hardened_digest' "$meta")" | ||
|
|
||
| if [ ! -f "$hardened" ] || [ "$hd" = "unpublished" ]; then | ||
| state="not-produced" | ||
| elif [ "$pd" = "$hd" ]; then | ||
| state="identical" | ||
| else | ||
| state="patched" | ||
| fi | ||
|
Comment on lines
+65
to
+71
|
||
|
|
||
| # Row emission. Command substitution (not process substitution) so a jq | ||
| # failure aborts under set -e instead of silently yielding zero rows. | ||
| if [ "$state" = "not-produced" ]; then | ||
| rows="$(jq -r --argjson i "$idx" ' | ||
| [.Results[]?.Vulnerabilities[]?] | ||
| | unique_by([.VulnerabilityID,.PkgName]) | ||
| | .[] | ||
| | [$i, "unpatched", .VulnerabilityID, .PkgName, .Severity, | ||
| (if (.FixedVersion//"") != "" then "fix " + .FixedVersion + " available" else "no fix" end)] | ||
| | @tsv' "$plain")" | ||
| else | ||
| residual="$(jq -r --argjson i "$idx" ' | ||
| [.Results[]?.Vulnerabilities[]?] | ||
| | unique_by([.VulnerabilityID,.PkgName]) | ||
| | .[] | ||
| | [$i, "residual", .VulnerabilityID, .PkgName, .Severity, | ||
| (if (.FixedVersion//"") != "" then "fix " + .FixedVersion + " available" else "no fix" end)] | ||
| | @tsv' "$hardened")" | ||
| # fixed = present in plain, absent from hardened, keyed on id+pkg with EXACT | ||
| # array membership (index), never `inside`/`contains` -- those do recursive | ||
| # substring matching on string arrays and would match CVE-2024-1 inside | ||
| # CVE-2024-12345. | ||
| fixed="$(jq -r --argjson i "$idx" --slurpfile h "$hardened" ' | ||
| ([$h[0].Results[]?.Vulnerabilities[]? | .VulnerabilityID + " " + .PkgName] | unique) as $hkeys | ||
| | [.Results[]?.Vulnerabilities[]?] | ||
| | map(select( (.VulnerabilityID + " " + .PkgName) as $k | ($hkeys | index($k)) == null )) | ||
| | unique_by([.VulnerabilityID,.PkgName]) | ||
| | .[] | ||
| | [$i, "fixed", .VulnerabilityID, .PkgName, .Severity, | ||
| .InstalledVersion + " → " + (.FixedVersion//"?")] | ||
| | @tsv' "$plain")" | ||
| rows="$(printf '%s\n%s' "$residual" "$fixed")" | ||
| fi | ||
| printf '%s\n' "$rows" | grep -v '^$' >> "${work}/rows.tsv" || true | ||
|
|
||
| # fixable_count is measured on the PLAIN scan: how many distinct (CVE,pkg) had an | ||
| # upstream fix available at scan time. It is the honest answer to "could Copa have | ||
| # done anything here", independent of whether it ran. | ||
| fixable="$(jq -r '[.Results[]?.Vulnerabilities[]? | select((.FixedVersion//"") != "")] | ||
| | unique_by([.VulnerabilityID,.PkgName]) | length' "$plain")" | ||
|
|
||
| jq -n -c \ | ||
| --slurpfile m "$meta" \ | ||
| --arg state "$state" \ | ||
| --argjson fixable "$fixable" \ | ||
| '$m[0] | {image, variant, arch, plain_digest, hardened_digest, | ||
| hardened_state: $state, fixable_count: $fixable}' \ | ||
| >> "${work}/images.jsonl" | ||
|
|
||
| idx=$((idx + 1)) | ||
| done <<< "$metas" | ||
|
|
||
| # Assemble. Per-image severity counts are derived from the emitted rows, but from ALL of | ||
| # them -- including the linux-libc-dev (kernel-header) rows that every renderer excludes. | ||
| # They are therefore RAW scan totals describing the image as published, and are deliberately | ||
| # HIGHER than the tabulated severity totals and the detail tables. They are not a cross-check | ||
| # on those tables and must not be presented as one: cve-render-summary.sh prints a footnote | ||
| # under the Images table saying exactly this. | ||
| jq -n -r \ | ||
| --arg ts "$timestamp" \ | ||
| --slurpfile images "${work}/images.jsonl" \ | ||
| --rawfile rows "${work}/rows.tsv" \ | ||
| ' | ||
| ($rows | rtrimstr("\n") | if . == "" then [] else split("\n") end | ||
| | map(split("\t") | {i: (.[0]|tonumber), status: .[1], id: .[2], pkg: .[3], severity: .[4], fix: .[5]}) | ||
| ) as $r | ||
| | ($r | group_by([.id, .pkg, .status, .severity, .fix]) | ||
| | map({ id: .[0].id, severity: .[0].severity, pkg: .[0].pkg, | ||
| status: .[0].status, fix: .[0].fix, | ||
| affects: (map(.i) | unique) }) | ||
| | sort_by([.severity, .id, .pkg]) | ||
| ) as $cves | ||
| | ($r | group_by(.i) | map({key: (.[0].i|tostring), | ||
| value: (group_by(.severity) | map({key: .[0].severity, value: length}) | from_entries)}) | ||
| | from_entries) as $counts | ||
| | { generated: $ts, | ||
| images: ($images | to_entries | map(.value + {counts: ($counts[(.key|tostring)] // {})})), | ||
| cves: $cves } | ||
| ' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| #!/usr/bin/env bash | ||
| # Render the summary section of the known-CVE report from the canonical CVE data. | ||
| # Usage: cve-render-summary.sh <cve-data.json> | ||
| # | ||
| # This is the part a reader actually reads. It must answer, in the first screenful: | ||
| # did hardening achieve anything, how bad is each image, and what is not tabulated. | ||
| set -euo pipefail | ||
|
|
||
| data="${1:?usage: cve-render-summary.sh <cve-data.json>}" | ||
|
|
||
| jq -r ' | ||
| def short: if startswith("sha256:") then .[7:19] else . end; | ||
|
|
||
| .images as $imgs | ||
| | .cves as $cves | ||
| | ($cves | map(select(.pkg == "linux-libc-dev"))) as $kern | ||
| | ($kern | length) as $kn | ||
| | ($kern | map(.id) | unique | length) as $kd | ||
| | ($cves | map(select(.pkg != "linux-libc-dev"))) as $tab | ||
| | ($imgs | map(.hardened_state) | unique) as $states | ||
| # NOT a CVE count: fixable_count is a per-image figure, so this sum counts a CVE | ||
| # once per image it appears in. It is also measured on the full plain scan, which | ||
| # covers OS AND library packages, while the scan that feeds Copa is restricted to OS | ||
| # packages (scan-patch-gate.sh: trivy image --pkg-types os --ignore-unfixed) -- so it | ||
| # can be non-zero with zero `fixed` rows. Severity is NOT part of that difference: | ||
| # the Copa input scan carries no --severity filter, and the CRITICAL,HIGH threshold | ||
| # (GATE_SEVERITY) applies only to the separate post-patch gate scan that decides | ||
| # whether the -hardened tag is published. The rendered sentence below must therefore | ||
| # describe it as a summed finding count, never as N CVEs, must not promise `fixed` | ||
| # rows that may not exist, and must not blame severity for the gap. | ||
| | ($imgs | map(.fixable_count) | add // 0) as $fixable | ||
| | ( | ||
| "# Known CVEs & hardening report" | ||
| , "" | ||
| , "_Generated \(.generated)._" | ||
| , "" | ||
| , "Per published **stable release image**: known CVEs from a full Trivy scan (all" | ||
| , "severities, OS + library packages, unfixable CVEs included). **Development / rolling" | ||
| , "tags (`*-dev`) are not covered** -- they are plain-only and never Copa-patched." | ||
| , "" | ||
| , "## Hardening outcome" | ||
| , "" | ||
| , ( if $fixable == 0 then | ||
| "**No fixable CVE was available upstream for any image in this run.** Debian ships no" | ||
| + " fix for any of the \($tab | map(.id) | unique | length) distinct CVEs found, so Copa" | ||
| + " had nothing to patch." | ||
| else | ||
| "**\($fixable) fix-available findings** were seen across all images -- the `Fixable`" | ||
| + " column of the Images table below, summed, so a CVE present in several images is" | ||
| + " counted once per image. It is measured on the full scan, which covers OS **and" | ||
| + " library** packages, while the scan that feeds Copa is restricted to OS packages" | ||
| + " -- so this is an upper bound on what could have been patched, not a count of" | ||
| + " `fixed` rows." | ||
| end ) | ||
| , ( if ($states | length) == 1 and $states[0] == "identical" then | ||
| " Every `-hardened` tag in this run is the **identical image** to its plain" | ||
| + " counterpart -- same digest, same contents." | ||
| elif ($states | index("not-produced")) then | ||
| " Some images have no `-hardened` tag this run (the severity gate failed or hardening" | ||
| + " was disabled); their rows are marked `unpatched`." | ||
| else "" end ) | ||
| , "" | ||
| , "**Status legend:** `fixed` = Copa patched it (old → new version) · `residual` = still" | ||
| , "present in the hardened image · `unpatched` = no hardened image was produced." | ||
| , "" | ||
| , "## Severity totals" | ||
| , "" | ||
| , "| Severity | Distinct CVEs | Tabulated rows |" | ||
| , "|----------|---------------|----------------|" | ||
| , ( ["CRITICAL","HIGH","MEDIUM","LOW","UNKNOWN"] | ||
| | map( . as $s | ||
| | ($tab | map(select(.severity == $s))) as $rows | ||
| | "| \($s) | \($rows | map(.id) | unique | length) | \($rows | length) |" ) | ||
| | join("\n") ) | ||
| , "" | ||
| , "## Not tabulated" | ||
| , "" | ||
| , ( if $kn == 0 then "_Nothing excluded._" | ||
| else | ||
| "**\($kn) `linux-libc-dev` row\(if $kn == 1 then "" else "s" end) (\($kd) distinct" | ||
| + " CVE\(if $kd == 1 then "" else "s" end))" | ||
| + " \(if $kn == 1 then "is" else "are" end)" | ||
| + " excluded from the tables.** These are Linux kernel *header* CVEs. A container" | ||
| + " runs on the host kernel, so they are not reachable inside these images. They remain in" | ||
| + " `cve-data.json`, uploaded as the `cve-report-json` artifact on the release run." | ||
| end ) | ||
| , "" | ||
| , "## CVEs by variant" | ||
| , "" | ||
| , "Variants differ enormously. This table is the fastest way to see which image flavour" | ||
| , "carries the CVE surface, and therefore which one to pick if you do not need its extras." | ||
| , "" | ||
| , "| Variant | Distinct CVEs | Image builds |" | ||
| , "|---------|---------------|--------------|" | ||
| , ( ($imgs | to_entries | map({v: .value.variant, i: .key}) | ||
| | group_by(.v) | ||
| | map({ variant: .[0].v, idxs: map(.i) })) as $byvar | ||
| | $byvar | ||
| | map( . as $g | ||
| | ($tab | map(select(.affects | any(. as $a | $g.idxs | index($a))))) as $rows | ||
| | { variant: $g.variant, | ||
| cves: ($rows | map(.id) | unique | length), | ||
| imgs: ($g.idxs | length) } ) | ||
| | sort_by(-.cves) | ||
| | map("| `\(.variant)` | \(.cves) | \(.imgs) |") | ||
| | join("\n") ) | ||
| , "" | ||
| , "## Most-affected packages" | ||
| , "" | ||
| # "Image builds affected", not "Images affected": this counts images[] indices, which | ||
| # are per-arch (amd64 and arm64 are two entries), whereas the `Affects` column of the | ||
| # detail tables collapses arch and counts logical images. Same data, two scales -- the | ||
| # column name and the footnote below keep the difference explicit instead of letting | ||
| # one word carry two answers in the same document. | ||
| , "| Package | Distinct CVEs | Image builds affected |" | ||
| , "|---------|---------------|-----------------------|" | ||
| , ( $tab | ||
| | group_by(.pkg) | ||
| | map({ pkg: .[0].pkg, | ||
| cves: (map(.id) | unique | length), | ||
| imgs: ([.[].affects[]] | unique | length) }) | ||
| | sort_by(-.cves) | ||
| | .[0:10] | ||
| | map("| `\(.pkg)` | \(.cves) | \(.imgs) |") | ||
| | join("\n") ) | ||
| , "" | ||
| , "_\"Image builds\" counts each architecture separately (amd64 and arm64 of one tag are" | ||
| , "two builds), so these figures are larger than the arch-collapsed `Affects` column in" | ||
| , "the detail tables. Both describe the same rows._" | ||
| , "" | ||
| , "## Images" | ||
| , "" | ||
| , "| Image | Arch | CRIT | HIGH | MED | LOW | UNK | Fixable | Hardening | Plain digest |" | ||
| , "|-------|------|------|------|-----|-----|-----|---------|-----------|--------------|" | ||
| , ( $imgs | ||
| | map( "| `\(.image)` | \(.arch) " | ||
| + "| \(.counts.CRITICAL // 0) | \(.counts.HIGH // 0) | \(.counts.MEDIUM // 0) " | ||
| + "| \(.counts.LOW // 0) | \(.counts.UNKNOWN // 0) | \(.fixable_count) " | ||
| + "| \(.hardened_state) | `\(.plain_digest | short)` |" ) | ||
| | join("\n") ) | ||
| , "" | ||
| # I1: these counts come straight from the Trivy scan of the image, so unlike every | ||
| # other figure in this report they still include the un-tabulated linux-libc-dev rows. | ||
| # That is deliberate -- they describe the image as published -- but it must be said out | ||
| # loud, or a reader chasing a CRIT here finds no matching table row and concludes the | ||
| # report dropped findings. | ||
| , ( if $kn == 0 then | ||
| "_`CRIT`-`UNK` and `Fixable` are raw Trivy totals for the image as published._" | ||
| else | ||
| "_`CRIT`-`UNK` and `Fixable` above are **raw Trivy totals for the image as" | ||
| + " published**: unlike the tabulated counts earlier in this report they still" | ||
| + " include the \($kn) un-tabulated `linux-libc-dev`" | ||
| + " row\(if $kn == 1 then "" else "s" end), so they intentionally exceed every" | ||
| + " tabulated count above. A `CRIT`/`HIGH` here with no matching row in any detail" | ||
| + " table is a kernel-header CVE -- see **Not tabulated** above. `Fixable` is also" | ||
| + " measured across both OS and library packages, wider than the OS-packages-only" | ||
| + " scope of the scan that feeds Copa -- severity plays no part in that difference._" | ||
| end ) | ||
| , "" | ||
| ) | ||
| ' "$data" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| #!/usr/bin/env bash | ||
| # Render one severity slice of the canonical CVE data as a markdown table. | ||
| # Usage: cve-render-table.sh <cve-data.json> <severity-csv> [title] | ||
| # e.g. cve-render-table.sh cve-data.json CRITICAL,HIGH "Critical & high severity" | ||
| # | ||
| # Always excludes linux-libc-dev: those are kernel-header CVEs, not reachable in a | ||
| # container (the container uses the host kernel). They are ~50% of all rows and the | ||
| # summary discloses their count, so nothing is hidden -- only un-tabulated. | ||
| set -euo pipefail | ||
|
|
||
| data="${1:?usage: cve-render-table.sh <cve-data.json> <severity-csv> [title]}" | ||
| sevs="${2:?usage: cve-render-table.sh <cve-data.json> <severity-csv> [title]}" | ||
| title="${3:-}" | ||
|
|
||
| [ -n "$title" ] && printf '## %s\n\n' "$title" | ||
|
|
||
| jq -r --arg sevs "$sevs" ' | ||
| # Only CVE-* identifiers are linked to NVD. Trivy also reports Debian | ||
| # security-tracker placeholders (TEMP-0000000-F7A20F and friends) for issues with no | ||
| # CVE assigned yet; NVD has no page for those, so linking them yields a guaranteed | ||
| # dead link. They render as inline code instead -- readable, and honest about not | ||
| # being resolvable anywhere. | ||
| def idcell: if startswith("CVE-") then "[\(.)](https://nvd.nist.gov/vuln/detail/\(.))" else "`\(.)`" end; | ||
| ($sevs | split(",")) as $want | ||
| # Arch is collapsed (amd64/arm64 findings are near-identical, so listing both | ||
| # doubles the cell for no information), and the cell names the affected RELEASES | ||
| # rather than every image variant. "20 images · v3.8, v4.2" answers the question a | ||
| # reader actually has; spelling out 20 variant names does not, and it pushed the | ||
| # LOW table to within 10 percent of the 512 KB GitHub render limit. | ||
| | (.images | map(.image)) as $labels | ||
| | (.images | map(.image | capture("(?<v>v[0-9]+\\.[0-9]+)$").v // "other")) as $rels | ||
| | [ .cves[] | select(.severity as $s | $want | index($s)) | select(.pkg != "linux-libc-dev") ] | ||
| | if length == 0 then | ||
| "_No CVEs in this severity range._" | ||
| else | ||
| (["| CVE | Severity | Package | Status | Affects |", | ||
| "|-----|----------|---------|--------|---------|"] | ||
| + ( sort_by([.severity, .pkg, .id]) | ||
| | map( | ||
| ([.affects[] | $labels[.]] | unique | length) as $n | ||
| | ([.affects[] | $rels[.]] | unique | sort) as $r | ||
| | "| \(.id | idcell) " | ||
| + "| \(.severity) | `\(.pkg)` | \(.status) · \(.fix) " | ||
| + "| \($n) image\(if $n == 1 then "" else "s" end) · \($r | join(", ")) |" | ||
| ) | ||
| ) | ||
| ) | join("\n") | ||
| end | ||
| ' "$data" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| #!/usr/bin/env bash | ||
| # Warn when a rendered report file approaches the GitHub markdown render limit. | ||
| # Usage: cve-size-guard.sh <file> [limit-bytes] | ||
| # | ||
| # GitHub refuses to render markdown blobs at or above 512 KB, showing "we can't show | ||
| # files that are this big" instead. That is the exact failure this report redesign | ||
| # fixed, and CVE counts only grow -- so a silent slide back past the limit must be | ||
| # detected. Warns at 90% of the limit to leave room to react. | ||
| # | ||
| # Always exits 0: the report is transparency, not a gate, and must never fail a | ||
| # release whose images already published. | ||
| set -euo pipefail | ||
|
|
||
| file="${1:?usage: cve-size-guard.sh <file> [limit-bytes]}" | ||
| limit="${2:-524288}" | ||
|
|
||
| [ -f "$file" ] || exit 0 | ||
|
|
||
| size="$(wc -c < "$file" | tr -d ' ')" | ||
| threshold=$(( limit * 90 / 100 )) | ||
|
|
||
| if [ "$size" -ge "$threshold" ]; then | ||
| printf '::warning::%s is %s bytes, at or above 90%% of the %s-byte GitHub markdown render limit. Split this table further before it stops rendering.\n' \ | ||
| "$file" "$size" "$limit" | ||
| fi |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.