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
17 changes: 17 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ jobs:
- name: Validate repository
run: ./tests/validate.sh

- name: Install pinned SPDX validator
run: |
python3 -m venv "$RUNNER_TEMP/spdx-tools"
"$RUNNER_TEMP/spdx-tools/bin/python" -m pip install --disable-pip-version-check 'spdx-tools==0.8.5'

- name: Validate SPDX release artifact independently
run: |
output="$RUNNER_TEMP/spdx-validation-artifact"
scripts/release-artifact build \
--version 2.2.0 \
--commit "$(git rev-parse --verify 'HEAD^{commit}')" \
--output "$output"
"$RUNNER_TEMP/spdx-tools/bin/python" tests/validate-spdx.py \
"$output/base-bash-libs-v2.2.0.spdx.json"
"$RUNNER_TEMP/spdx-tools/bin/python" tests/validate-spdx.py \
--expect-missing-sha1 tests/fixtures/spdx/missing-sha1.spdx.json

- name: Smoke supported runner Bash
run: |
bash -c '
Expand Down
12 changes: 12 additions & 0 deletions docs/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ policy](https://github.com/basefoundry/base/blob/main/docs/ecosystem-policy.md).
9. Verify the annotated `vX.Y.Z` tag and the GitHub Release for
`basefoundry/base-bash-libs`.

## SBOM Interoperability

The SPDX 2.3 SBOM format is versioned with the release: releases before v2.2.0
retain their published SHA256-only file records, while v2.2.0 and later include
both SHA256 and the SHA1 checksum required by SPDX 2.3 file records. The Linux
and macOS validation jobs independently parse and semantically validate each
candidate SBOM with `spdx-tools==0.8.5`; macOS also exercises the `shasum -a 1`
checksum path. Both jobs check a parseable negative fixture that must produce
exactly one semantic error, specifically the missing SHA1 checksum. This
validation does not rewrite or replace any already-published release asset;
corrected SBOMs are produced only for a future authorized release.

## Homebrew Handoff

The release contract requires the tap-owned formula
Expand Down
38 changes: 36 additions & 2 deletions scripts/release-artifact
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ hash_file() {
fi
}

hash_file_sha1() {
if command -v sha1sum > /dev/null 2>&1; then
sha1sum -- "$1" | awk '{print $1}'
else
shasum -a 1 -- "$1" | awk '{print $1}'
fi
}

json_escape() {
local value="${1-}"
value=${value//\\/\\\\}
Expand Down Expand Up @@ -139,8 +147,25 @@ normalize_tree_times() {
done < <(find "$root" -type d -print)
}

spdx_sbom_requires_sha1() {
local version="${1-}" major minor version_tail

major="${version%%.*}"
version_tail="${version#*.}"
minor="${version_tail%%.*}"
[[ "$major" =~ ^[0-9]+$ && "$minor" =~ ^[0-9]+$ ]] || return 2
((10#$major > 2 || (10#$major == 2 && 10#$minor >= 2)))
}

write_sbom() {
local output="$1" version="$2" commit="$3" root="$4" path relative first=1
local include_sha1=0 status file_id file_name sha1_hash sha256_hash
if spdx_sbom_requires_sha1 "$version"; then
include_sha1=1
else
status=$?
((status == 1)) || return "$status"
fi
{
printf '{\n'
printf ' "spdxVersion": "SPDX-2.3",\n'
Expand All @@ -159,8 +184,17 @@ write_sbom() {
printf ',\n'
fi
first=0
printf ' {"SPDXID": "%s", "fileName": "%s", "checksums": [{"algorithm": "SHA256", "checksumValue": "%s"}], "licenseConcluded": "Apache-2.0"}' \
"$(spdx_file_identifier "$relative")" "$(json_escape "$relative")" "$(hash_file "$path")"
file_id="$(spdx_file_identifier "$relative")" || return 1
file_name="$(json_escape "$relative")"
sha256_hash="$(hash_file "$path")" || return 1
if ((include_sha1)); then
sha1_hash="$(hash_file_sha1 "$path")" || return 1
printf ' {"SPDXID": "%s", "fileName": "%s", "checksums": [{"algorithm": "SHA1", "checksumValue": "%s"}, {"algorithm": "SHA256", "checksumValue": "%s"}], "licenseConcluded": "Apache-2.0"}' \
"$file_id" "$file_name" "$sha1_hash" "$sha256_hash"
else
printf ' {"SPDXID": "%s", "fileName": "%s", "checksums": [{"algorithm": "SHA256", "checksumValue": "%s"}], "licenseConcluded": "Apache-2.0"}' \
"$file_id" "$file_name" "$sha256_hash"
fi
done < <(find "$root" -type f -print | LC_ALL=C sort)
printf '\n ],\n'
printf ' "relationships": [{"spdxElementId": "SPDXRef-DOCUMENT", "relationshipType": "DESCRIBES", "relatedSpdxElement": "SPDXRef-Package"}]\n'
Expand Down
41 changes: 41 additions & 0 deletions tests/fixtures/spdx/missing-sha1.spdx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "base-bash-libs-semantic-negative-fixture",
"documentNamespace": "https://example.invalid/base-bash-libs/semantic-negative-fixture",
"creationInfo": {
"created": "2026-01-01T00:00:00Z",
"creators": ["Tool: base-bash-libs-test"]
},
"packages": [
{
"SPDXID": "SPDXRef-Package",
"name": "fixture",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": true,
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION"
}
],
"files": [
{
"SPDXID": "SPDXRef-File-fixture",
"fileName": "fixture.sh",
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
],
"licenseConcluded": "NOASSERTION"
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relationshipType": "DESCRIBES",
"relatedSpdxElement": "SPDXRef-Package"
}
]
}
29 changes: 25 additions & 4 deletions tests/release-artifact.bats
Original file line number Diff line number Diff line change
Expand Up @@ -94,29 +94,50 @@ EOF
@test "release artifact build creates a deterministic verified asset set" {
local first="$TEST_TMPDIR/first" second="$TEST_TMPDIR/second"

bats_run "$RELEASE_ARTIFACT" build --version 2.0.0-rc.1 --commit "$RELEASE_COMMIT" --output "$first"
bats_run "$RELEASE_ARTIFACT" build --version 2.2.0-rc.1 --commit "$RELEASE_COMMIT" --output "$first"
[ "$status" -eq 0 ]
bats_run "$RELEASE_ARTIFACT" verify "$first"
[ "$status" -eq 0 ]
[[ "$output" == *"verified"* ]]

bats_run "$RELEASE_ARTIFACT" build --version 2.0.0-rc.1 --commit "$RELEASE_COMMIT" --output "$second"
bats_run "$RELEASE_ARTIFACT" build --version 2.2.0-rc.1 --commit "$RELEASE_COMMIT" --output "$second"
[ "$status" -eq 0 ]
diff -ru "$first" "$second"
grep -F '"spdxVersion": "SPDX-2.3"' "$first"/*.spdx.json
grep -F '"algorithm": "SHA1"' "$first"/*.spdx.json
grep -F '"algorithm": "SHA256"' "$first"/*.spdx.json
awk '/"SPDXID": "SPDXRef-File-/ { count++; if ($0 !~ /"algorithm": "SHA1"/ || $0 !~ /"algorithm": "SHA256"/) missing++ } END { exit (count > 0 && missing == 0 ? 0 : 1) }' \
"$first"/*.spdx.json
grep -F '"reproducible": true' "$first"/*.provenance.json
awk '/"SPDXID": "SPDXRef-File-/ { id=$0; sub(/^.*"SPDXID": "/, "", id); sub(/".*$/, "", id); if (id !~ /^SPDXRef-File-[A-Za-z0-9.-]+$/ || seen[id]++) exit 1; count++ } END { exit (count > 0 ? 0 : 1) }' \
"$first"/*.spdx.json
}

@test "legacy 2.1 SBOM verification retains its SHA256-only file records" {
local artifact="$TEST_TMPDIR/legacy" sbom verified="$TEST_TMPDIR/verified"

release_test_build "$artifact" 2.1.0
sbom="$artifact/base-bash-libs-v2.1.0.spdx.json"
awk '/"SPDXID": "SPDXRef-File-/ { count++; if ($0 ~ /"algorithm": "SHA1"/ || $0 !~ /"algorithm": "SHA256"/) invalid++ } END { exit (count > 0 && invalid == 0 ? 0 : 1) }' \
"$sbom"

export REMOTE_VERSION=2.1.0 REMOTE_COMMIT="$RELEASE_COMMIT" REMOTE_SOURCE="$artifact"
export REMOTE_TAG_OBJECT=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb REMOTE_ASSET_MODE=full
release_test_make_remote_gh_stub
bats_run "$RELEASE_ARTIFACT" verify-remote --version "$REMOTE_VERSION" \
--commit "$REMOTE_COMMIT" --output "$verified"
[ "$status" -eq 0 ]
[[ "$output" == *"GitHub Release assets verified"* ]]
}

@test "release artifact bytes are independent of the host timezone" {
local utc="$TEST_TMPDIR/utc" local_tz="$TEST_TMPDIR/local-tz"

bats_run env TZ=UTC0 TZDIR="$TEST_TMPDIR/missing-zoneinfo" "$RELEASE_ARTIFACT" build \
--version 2.0.0-rc.1 --commit "$RELEASE_COMMIT" --output "$utc"
--version 2.2.0-rc.1 --commit "$RELEASE_COMMIT" --output "$utc"
[ "$status" -eq 0 ]
bats_run env TZ=Asia/Kolkata "$RELEASE_ARTIFACT" build \
--version 2.0.0-rc.1 --commit "$RELEASE_COMMIT" --output "$local_tz"
--version 2.2.0-rc.1 --commit "$RELEASE_COMMIT" --output "$local_tz"
[ "$status" -eq 0 ]

diff -ru "$utc" "$local_tz"
Expand Down
59 changes: 59 additions & 0 deletions tests/validate-spdx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Validate SPDX JSON with the pinned independent spdx-tools package."""

from __future__ import annotations

import argparse
import sys
from pathlib import Path

from spdx_tools.spdx.parser.parse_anything import parse_file
from spdx_tools.spdx.validation.document_validator import validate_full_spdx_document


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("document", type=Path)
parser.add_argument(
"--expect-missing-sha1",
action="store_true",
help="pass only when semantic validation rejects a file without SHA1",
)
arguments = parser.parse_args()

try:
document = parse_file(str(arguments.document))
except Exception as error: # parser errors are distinct from semantic errors
print(f"SPDX parse failed for {arguments.document}: {error}", file=sys.stderr)
return 1

messages = validate_full_spdx_document(document, spdx_version="SPDX-2.3")
if arguments.expect_missing_sha1:
sha1_errors = [
message
for message in messages
if "must contain a SHA1 algorithm checksum" in message.validation_message
]
if len(messages) == 1 and len(sha1_errors) == 1:
print("SPDX semantic-negative fixture rejected for its missing required SHA1 checksum.")
return 0
print(
"SPDX semantic-negative fixture must produce exactly one validation error, for its missing SHA1 checksum.",
file=sys.stderr,
)
for message in messages:
print(message, file=sys.stderr)
return 1

if messages:
print(f"SPDX validation failed for {arguments.document}:", file=sys.stderr)
for message in messages:
print(message, file=sys.stderr)
return 1

print(f"SPDX 2.3 document is semantically valid: {arguments.document}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading