CI/CD Integration
This guide shows how to integrate TrustBOM into your CI/CD pipeline using GitHub Actions.
Architecture Overview
The CI/CD workflow consists of two jobs:
┌─────────────────────────────────────┐ ┌─────────────────────────────────────┐
│ Job 1: build-and-scan │ │ Job 2: build-smt-and-store │
│ (GitHub-hosted runner) │────▶│ (Self-hosted runner in K8s) │
├─────────────────────────────────────┤ ├─────────────────────────────────────┤
│ • Build Docker image │ │ • Download sbom-converter │
│ • Push to GHCR │ │ • Build SMT from SBOM │
│ • Generate SBOM with Syft │ │ • Store SMT in merkle-proof-service│
│ • Calculate SBOM hash │ │ • Write root hash to blockchain │
│ • Upload artifacts │ │ │
└─────────────────────────────────────┘ └─────────────────────────────────────┘
Job 1 runs on standard GitHub-hosted runners and handles the build and SBOM generation.
Job 2 runs on a self-hosted runner inside your Kubernetes cluster. It uses the sbom-conversion tool to transform the CycloneDX SBOM into a Sparse Merkle Tree (SMT).
The two-job architecture with a self-hosted runner is specific to this prototype setup. Since TrustBOM services and the Hardhat blockchain run locally (not in the cloud), Job 2 needs to run inside the Kubernetes cluster to access them.
In a production deployment with cloud-hosted services, the entire workflow could run on standard GitHub-hosted runners.
Prerequisites
Self-Hosted Runner
The second job requires a self-hosted runner deployed via Actions Runner Controller (ARC). This is set up automatically by the TrustBOM installation script (./scripts/setup.sh).
Verify the runner is available:
kubectl get pods -n arc-runners
GitHub Token Setup
The workflow uses GITHUB_TOKEN for pushing images to GitHub Container Registry (ghcr.io) and downloading artifacts between jobs. This token is automatically provided by GitHub Actions - you don't need to create it manually.
However, you need to ensure your repository has the correct permissions:
- Go to your repository on GitHub
- Navigate to Settings → Actions → General
- Scroll to Workflow permissions
- Select Read and write permissions
- Check Allow GitHub Actions to create and approve pull requests (optional)
- Click Save
For the first push to GHCR, you may need to configure package visibility:
- After the first workflow run, go to your repository's Packages tab
- Click on the package → Package settings
- Under Manage Actions access, add your repository with Write role
Complete Workflow Template
Copy this workflow to .github/workflows/sbom-ci.yaml in your repository. This template needs to be customized for your project—see Customization Points below for required changes (image name, Dockerfile path, etc.).
The proving-service currently only supports Package URL (PURL) verification. License-based verification is not yet implemented in the ZK proof generation.
# TrustBOM SBOM Commitment Workflow
#
# This workflow automates the commitment phase of TrustBOM:
# 1. Builds your application container
# 2. Generates a CycloneDX SBOM
# 3. Transforms SBOM into a Sparse Merkle Tree (SMT)
# 4. Stores the SMT and commits its root hash to the blockchain
#
# Prerequisites:
# - TrustBOM services deployed in Kubernetes
# - Self-hosted GitHub Actions Runner via ARC
# - GHCR (GitHub Container Registry) access
name: SBOM System CI/CD
on:
push:
branches: [main]
workflow_dispatch: # Allow manual triggers
jobs:
# Job 1: Build, Push, and Generate SBOM
# Runs on GitHub-hosted runners
build-and-scan:
runs-on: ubuntu-latest
outputs:
image_digest: ${{ steps.build.outputs.digest }}
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Generates consistent tags for your container image
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
# CUSTOMIZE: Change 'myapp' to your image name
images: ghcr.io/${{ github.repository }}/myapp
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
# CUSTOMIZE: Update path if your Dockerfile is elsewhere
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Generate CycloneDX SBOM from the container image using Anchore Syft
- name: Create sboms directory
run: mkdir -p sboms
- name: Generate SBOM with Syft
uses: anchore/sbom-action@v0
with:
# CUSTOMIZE: Match your image name from above
image: ghcr.io/${{ github.repository }}/myapp:latest
format: cyclonedx-json
output-file: sboms/app.json
- name: Calculate SBOM hash
run: |
sha256sum sboms/app.json | awk '{print $1}' > sboms/app.sha256
echo "SBOM Hash: $(cat sboms/app.sha256)"
# Makes SBOM available to the second job
- name: Upload SBOM artifacts
uses: actions/upload-artifact@v4
with:
name: sbom-and-security-reports
path: |
sboms/app.json
sboms/app.sha256
retention-days: 30
# Job 2: Build SMT and Store on Blockchain
# Runs on self-hosted runner inside Kubernetes cluster
# (Required for prototype setup to access local services)
build-smt-and-store:
runs-on: [self-hosted]
needs: [build-and-scan]
timeout-minutes: 30
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Download SBOM artifacts
uses: actions/download-artifact@v4
with:
name: sbom-and-security-reports
path: .
- name: Verify and organize SBOM artifacts
run: |
if [ -f "app.json" ] && [ ! -f "sboms/app.json" ]; then
mkdir -p sboms
mv app.json sboms/app.json 2>/dev/null || true
mv app.sha256 sboms/app.sha256 2>/dev/null || true
fi
[ ! -f "sboms/app.json" ] && { echo "ERROR: SBOM not found"; exit 1; }
echo "SBOM file found: $(ls -lh sboms/app.json)"
- name: Install kubectl
run: |
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
- name: Wait for merkle-proof-service
run: |
kubectl wait --for=condition=available --timeout=300s \
deployment/merkle-proof-service -n sharing-sbom-system || true
MERKLE_URL="http://merkle-proof-service.sharing-sbom-system.svc.cluster.local:8090"
for i in {1..30}; do
curl -s --max-time 5 "$MERKLE_URL/health" > /dev/null 2>&1 && break
sleep 2
done
# Download sbom-converter (https://github.com/tuberlin-blockchain-prototyping/sbom-conversion) and transform CycloneDX SBOM into a Sparse Merkle Tree
- name: Build SMT from SBOM
id: build_smt
timeout-minutes: 15
run: |
SBOM_FILE="sboms/app.json"
[ ! -f "$SBOM_FILE" ] && { echo "ERROR: SBOM not found"; exit 1; }
# Download latest sbom-converter release
LATEST=$(curl -s https://api.github.com/repos/tuberlin-blockchain-prototyping/sbom-conversion/releases/latest)
TAG=$(echo "$LATEST" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
URL=$(echo "$LATEST" | grep -o '"browser_download_url": "[^"]*linux[^"]*amd64[^"]*"' | head -1 | sed 's/"browser_download_url": "\([^"]*\)"/\1/')
curl -L -o sbom-converter "$URL"
chmod +x sbom-converter
# Build SMT using dependency (PURL) extraction
# Note: Only 'dependency' type is currently supported by the proving-service
OUTPUT_FILE="sboms/app-smt.json"
OUTPUT=$(./sbom-converter build --file "$SBOM_FILE" --output "$OUTPUT_FILE" --type dependency 2>&1)
echo "$OUTPUT"
ROOT_HASH=$(echo "$OUTPUT" | tail -n1 | tr -d '[:space:]')
[[ ! "$ROOT_HASH" =~ ^[0-9a-fA-F]{64}$ ]] && { echo "ERROR: Invalid root hash"; exit 1; }
echo "root_hash=$ROOT_HASH" >> $GITHUB_OUTPUT
echo "SMT Root Hash: $ROOT_HASH"
# Store SMT data for later proof generation
- name: Store SMT
run: |
MERKLE_URL="http://merkle-proof-service.sharing-sbom-system.svc.cluster.local:8090"
SMT_FILE="sboms/app-smt.json"
ROOT_HASH="${{ steps.build_smt.outputs.root_hash }}"
REQUEST=$(mktemp)
jq -n --arg root "$ROOT_HASH" --slurpfile smt "$SMT_FILE" \
'{root_hash: $root, smt_data: $smt[0]}' > "$REQUEST"
RESPONSE=$(curl -X POST "$MERKLE_URL/store-smt" \
-H "Content-Type: application/json" \
--data @"$REQUEST" \
--max-time 60 \
--write-out "\nHTTP_CODE:%{http_code}" \
--silent)
rm -f "$REQUEST"
HTTP_CODE=$(echo "$RESPONSE" | grep "HTTP_CODE:" | cut -d: -f2)
[ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ] && {
echo "ERROR: Failed to store SMT (HTTP $HTTP_CODE)"
exit 1
}
- name: Read SBOM hash
id: sbom_hash
run: |
SBOM_HASH=$(cat sboms/app.sha256)
echo "sbom_hash=$SBOM_HASH" >> $GITHUB_OUTPUT
# Write immutable record to blockchain
- name: Write SMT root to blockchain
run: |
CONTRACT_ADDR="0x5FbDB2315678afecb367f032d93F642f64180aa3"
ROOT_HASH="${{ steps.build_smt.outputs.root_hash }}"
SBOM_HASH="${{ steps.sbom_hash.outputs.sbom_hash }}"
IMAGE_DIGEST="${{ needs.build-and-scan.outputs.image_digest }}"
[[ "$IMAGE_DIGEST" == sha256:* ]] && SOFTWARE_DIGEST="${IMAGE_DIGEST#sha256:}" || SOFTWARE_DIGEST="$IMAGE_DIGEST"
echo "Writing to blockchain:"
echo " Root Hash: $ROOT_HASH"
echo " Software Digest: sha256:$SOFTWARE_DIGEST"
echo " SBOM Hash: $SBOM_HASH"
HARDHAT_POD=$(kubectl get pods -n blockchain -l app=hardhat-node -o jsonpath='{.items[0].metadata.name}')
IDENT="$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]'):${{ github.run_id }}"
TX_OUTPUT=$(kubectl exec -n blockchain "$HARDHAT_POD" -- sh -c \
"cd /workspace && \
ADDR='$CONTRACT_ADDR' \
ROOT_HASH='$ROOT_HASH' \
SOFTWARE_DIGEST='$SOFTWARE_DIGEST' \
SBOM_HASH='$SBOM_HASH' \
IDENT='$IDENT' \
npx hardhat run store_smt_root.js --network localhost 2>&1")
echo "$TX_OUTPUT"
if echo "$TX_OUTPUT" | grep -q "SKIPPED"; then
echo "SMT root already registered"
else
TX_HASH=$(echo "$TX_OUTPUT" | grep -E "^0x[0-9a-fA-F]{64}$" | tail -n1)
echo "Transaction: $TX_HASH"
fi
Customization Points
Image Name
Update these locations to match your image name:
# In the metadata step
images: ghcr.io/${{ github.repository }}/myapp # Change 'myapp'
# In the SBOM generation step
image: ghcr.io/${{ github.repository }}/myapp:latest # Change 'myapp'
Dockerfile Path
If your Dockerfile is not at the repository root:
file: ./path/to/Dockerfile
Workflow Output
After successful execution:
- Docker image pushed to GitHub Container Registry
- SBOM artifact retained for 30 days (downloadable from Actions UI)
- SMT stored in merkle-proof-service (indexed by root hash)
- Root hash recorded on blockchain with metadata
The SMT root hash is now your cryptographic commitment. Share it with customers to enable verification requests.