Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:
sh -n scripts/render-community-packages.sh
sh -n scripts/collect-qt-licenses.sh
sh -n scripts/collect-go-licenses.sh
sh -n scripts/bundle-sboms.sh
- name: Validate AUR template rendering
run: |
aur_test_dir="$(mktemp -d)"
Expand Down Expand Up @@ -293,6 +294,12 @@ jobs:
run: goreleaser check
- name: Build release snapshot
run: goreleaser release --snapshot --clean
- name: Bundle release snapshot SBOMs
run: |
scripts/bundle-sboms.sh snapshot dist/checksums.txt dist cli=dist
test -s dist/whodis_snapshot_sboms.zip
test -z "$(find dist -type f -name '*.sbom.json' -print -quit)"
(cd dist && grep ' whodis_snapshot_sboms.zip$' checksums.txt | sha256sum --check --strict -)

platform-builds:
name: Build CLI (${{ matrix.goos }}/${{ matrix.goarch }})
Expand Down
27 changes: 22 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -368,17 +368,34 @@ jobs:
args: release --clean --skip=publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Verify staged release identities and SBOMs
- name: Validate and bundle release SBOMs
shell: bash
run: |
set -euo pipefail
test -s dist/checksums.txt
grep -q 'whodis-gui_' dist/checksums.txt
test "$(find .release-assets/gui -maxdepth 1 -type f -name '*.sbom.json' | wc -l)" -eq 5
for sbom in .release-assets/gui/*.sbom.json; do
jq --exit-status '.packages | length >= 10' "$sbom" >/dev/null
grep -qi whodis "$sbom"
done
test "$(find .release-assets/gui -maxdepth 1 -type f -name '*.sbom.json' | wc -l)" -eq 5
scripts/bundle-sboms.sh "$WHODIS_BUILD_VERSION" dist/checksums.txt dist \
cli=dist gui=.release-assets/gui
- name: Verify staged release identities and SBOMs
shell: bash
run: |
set -euo pipefail
version=${WHODIS_BUILD_VERSION#v}
sbom_bundle="dist/whodis_${version}_sboms.zip"
test -s dist/checksums.txt
grep -q 'whodis-gui_' dist/checksums.txt
test -s "$sbom_bundle"
unzip -tqq "$sbom_bundle"
test "$(unzip -Z1 "$sbom_bundle" | grep -c '^gui/.*\.sbom\.json$')" -eq 5
test "$(unzip -Z1 "$sbom_bundle" | grep -c '^cli/.*\.sbom\.json$')" -gt 0
test -z "$(find dist .release-assets/gui -type f -name '*.sbom.json' -print -quit)"
if grep -q '\.sbom\.json$' dist/checksums.txt; then
echo "checksums.txt still contains an individual SBOM" >&2
exit 1
fi
(cd dist && grep " $(basename "$sbom_bundle")$" checksums.txt | sha256sum --check --strict -)
if [ "$GITHUB_EVENT_NAME" = push ]; then
version=${GITHUB_REF_NAME#v}
cli=$(find dist -type f -path '*linux_amd64*/whodis' -print -quit)
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,8 +626,9 @@ and advisory so ordinary CI stays deterministic and offline.
Release automation has a non-publishing preflight that cross-builds the pure-Go
CLI and every native desktop bundle before a tag is created. It runs race and
vulnerability checks, generates staged-content SBOMs and SHA-256 checksums,
attests the exact release bytes, and only then publishes them. Releases remain
split into CLI and GUI assets so a server never needs to install Qt.
bundles the SBOMs into one auditor-friendly download, attests the exact release
bytes, and only then publishes them. Releases remain split into CLI and GUI
assets so a server never needs to install Qt.

## Boundaries and honest limitations

Expand Down
119 changes: 119 additions & 0 deletions scripts/bundle-sboms.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/bin/sh

set -eu

if [ "$#" -lt 4 ]; then
echo "usage: bundle-sboms.sh <version> <checksums-file> <output-directory> <label=sbom-directory>..." >&2
exit 2
fi

version=${1#v}
checksums_file=$2
output_dir=$3
shift 3

case "$version" in
''|*[!0-9A-Za-z._-]*)
echo "bundle-sboms: invalid version: $version" >&2
exit 2
;;
esac

for command in awk cp find grep jq sha256sum sort zip; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "bundle-sboms: required command not found: $command" >&2
exit 1
fi
done

[ -f "$checksums_file" ] || {
echo "bundle-sboms: checksums file not found: $checksums_file" >&2
exit 1
}
[ -d "$output_dir" ] || {
echo "bundle-sboms: output directory not found: $output_dir" >&2
exit 1
}

output_dir=$(CDPATH= cd -- "$output_dir" && pwd)
checksums_dir=$(CDPATH= cd -- "$(dirname -- "$checksums_file")" && pwd)
checksums_file="$checksums_dir/$(basename -- "$checksums_file")"
bundle="$output_dir/whodis_${version}_sboms.zip"

work_dir=$(mktemp -d)
trap 'rm -rf -- "$work_dir"' EXIT HUP INT TERM
content_dir="$work_dir/content"
list_dir="$work_dir/lists"
mkdir -p "$content_dir" "$list_dir"

total_count=0
for group in "$@"; do
label=${group%%=*}
source_dir=${group#*=}
if [ "$source_dir" = "$group" ]; then
echo "bundle-sboms: expected label=directory, got: $group" >&2
exit 2
fi
case "$label" in
''|*[!0-9A-Za-z._-]*)
echo "bundle-sboms: invalid group label: $label" >&2
exit 2
;;
esac
[ -d "$source_dir" ] || {
echo "bundle-sboms: SBOM directory not found: $source_dir" >&2
exit 1
}

group_dir="$content_dir/$label"
group_list="$list_dir/$label"
mkdir -p "$group_dir"
find "$source_dir" -type f -name '*.sbom.json' -print | sort > "$group_list"
[ -s "$group_list" ] || {
echo "bundle-sboms: no SBOM files found in $source_dir" >&2
exit 1
}

while IFS= read -r sbom; do
jq --exit-status '.packages | type == "array" and length > 0' "$sbom" >/dev/null
grep -qi whodis "$sbom" || {
echo "bundle-sboms: SBOM does not identify Whodis: $sbom" >&2
exit 1
}
destination="$group_dir/$(basename -- "$sbom")"
[ ! -e "$destination" ] || {
echo "bundle-sboms: duplicate SBOM filename in $label: $(basename -- "$sbom")" >&2
exit 1
}
cp "$sbom" "$destination"
total_count=$((total_count + 1))
done < "$group_list"
done

[ "$total_count" -gt 0 ] || {
echo "bundle-sboms: no SBOM files were collected" >&2
exit 1
}

rm -f -- "$bundle"
(
cd "$content_dir"
zip -q -r "$bundle" .
)

for group_list in "$list_dir"/*; do
while IFS= read -r sbom; do
rm -f -- "$sbom"
done < "$group_list"
done

filtered_checksums="$work_dir/checksums.filtered"
updated_checksums="$work_dir/checksums.updated"
awk '$2 !~ /\.sbom\.json$/ && $2 !~ /_sboms\.zip$/ { print }' \
"$checksums_file" > "$filtered_checksums"
bundle_hash=$(sha256sum "$bundle" | awk '{print $1}')
printf '%s %s\n' "$bundle_hash" "$(basename -- "$bundle")" >> "$filtered_checksums"
LC_ALL=C sort -k2,2 "$filtered_checksums" > "$updated_checksums"
cp "$updated_checksums" "$checksums_file"

echo "Bundled $total_count SBOM files in $bundle"
Loading