Skip to content

Fix ISO build: use :latest tag (BlueBuild doesn't tag :vX.Y.Z) #8

Fix ISO build: use :latest tag (BlueBuild doesn't tag :vX.Y.Z)

Fix ISO build: use :latest tag (BlueBuild doesn't tag :vX.Y.Z) #8

Workflow file for this run

name: Release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
dry_run:
description: "Dry run (skip publish)"
type: boolean
default: false
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
packages: write
id-token: write
attestations: write
checks: read
statuses: read
jobs:
preflight:
name: Release Preflight Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Verify CI passed on this commit
env:
GH_TOKEN: ${{ github.token }}
run: |
SHA="${{ github.sha }}"
echo "Checking CI status for commit ${SHA}..."
# Query check runs for required CI jobs
REQUIRED_CHECKS=(
"Go Build & Test"
"Python Test & Lint"
"Security Regression Tests"
"Dependency Vulnerability Audit"
"Test Count Drift Check"
"Documentation Validation"
)
PASS=0
FAIL=0
for check in "${REQUIRED_CHECKS[@]}"; do
# Use startswith to handle matrix jobs (e.g. "Go Build & Test (registry)")
RESULT=$(gh api "repos/${{ github.repository }}/commits/${SHA}/check-runs" \
--jq "[.check_runs[] | select(.name | startswith(\"${check}\")) | .conclusion] | if length == 0 then [\"not_found\"] else . end | if all(. == \"success\") then \"success\" else .[0] end" \
2>/dev/null || echo "not_found")
if [ "$RESULT" = "success" ]; then
echo "OK: ${check} = ${RESULT}"
PASS=$((PASS + 1))
else
echo "::warning::${check}: ${RESULT:-not_found}"
FAIL=$((FAIL + 1))
fi
done
if [ "$FAIL" -gt 0 ]; then
echo ""
echo "::error::${FAIL} required CI check(s) did not show success."
echo "Cannot create a release from a commit with failing or missing CI checks."
echo "Ensure CI is green before tagging a release."
exit 1
fi
echo "Preflight complete: ${PASS}/${#REQUIRED_CHECKS[@]} checks confirmed passing"
build-go:
name: Build Go Services
needs: [preflight]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
service: [airlock, registry, tool-firewall, gpu-integrity-watch, mcp-firewall, policy-engine, runtime-attestor, integrity-monitor, incident-recorder]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: "1.25"
cache-dependency-path: services/${{ matrix.service }}/go.sum
- name: Build (linux/amd64)
working-directory: services/${{ matrix.service }}
run: |
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-s -w -X main.version=${{ github.ref_name }}" \
-o ../../dist/${{ matrix.service }}-linux-amd64 .
- name: Build (linux/arm64)
working-directory: services/${{ matrix.service }}
run: |
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
go build -ldflags="-s -w -X main.version=${{ github.ref_name }}" \
-o ../../dist/${{ matrix.service }}-linux-arm64 .
- name: Generate SBOM (Syft)
uses: anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1
with:
path: services/${{ matrix.service }}
format: cyclonedx-json
output-file: dist/${{ matrix.service }}-sbom.cdx.json
upload-artifact: false
- name: Upload artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: go-${{ matrix.service }}
path: dist/
build-python:
name: Build Python Service SBOMs
needs: [preflight]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Install Syft
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- name: Generate Python SBOMs
run: |
mkdir -p dist
for svc in agent ui quarantine common; do
if [ -d "services/${svc}" ]; then
syft dir:services/${svc} -o cyclonedx-json=dist/${svc}-sbom.cdx.json
fi
done
# Diffusion worker and search mediator
for svc in diffusion-worker search-mediator; do
if [ -d "services/${svc}" ]; then
syft dir:services/${svc} -o cyclonedx-json=dist/${svc}-sbom.cdx.json
fi
done
- name: Upload artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: python-sboms
path: dist/
build-iso:
name: Build ISO
needs: [preflight]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Free disk space
run: |
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc
sudo docker image prune -af
- name: Build ISO
uses: jasonn3/build-container-installer@207e927e28c92704c4cdbe10b980643b3771ef01 # main
id: isogen
with:
arch: x86_64
image_name: secai_os
image_repo: ghcr.io/secai-hub
# Use :latest since BlueBuild tags main pushes as :latest, not :vX.Y.Z.
# The release tag and main HEAD point to the same commit.
image_tag: latest
version: 42
variant: Silverblue
iso_name: secai-os-${{ github.ref_name }}-x86_64.iso
- name: Install cosign
run: |
COSIGN_VERSION="v2.4.3"
curl -sSfL "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64" \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
- name: Sign ISO
run: |
ISO_PATH="${{ steps.isogen.outputs.iso_path }}"
mkdir -p dist
mv "$ISO_PATH" "dist/"
ISO_FILE="dist/secai-os-${{ github.ref_name }}-x86_64.iso"
cosign sign-blob --yes \
--key env://COSIGN_PRIVATE_KEY \
--output-signature "${ISO_FILE}.sig" \
"$ISO_FILE"
env:
COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}
- name: Upload ISO artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: iso-amd64
path: dist/secai-os-*.iso*
build-vm-images:
name: Build VM Images (QCOW2 + OVA)
needs: [preflight]
if: vars.HAS_KVM_RUNNER == 'true'
runs-on: [self-hosted, linux, x64, kvm]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Build QCOW2
run: |
bash scripts/vm/build-qcow2.sh --ci \
--image-ref "ghcr.io/secai-hub/secai_os:${{ github.ref_name }}"
- name: Build OVA from QCOW2
run: bash scripts/vm/build-ova.sh
- name: Install cosign
run: |
COSIGN_VERSION="v2.4.3"
curl -sSfL "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64" \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
- name: Sign VM artifacts
run: |
mkdir -p dist
cp output/secai-os.qcow2 "dist/secai-os-${{ github.ref_name }}.qcow2"
cp output/secai-os.ova "dist/secai-os-${{ github.ref_name }}.ova"
for f in dist/secai-os-${{ github.ref_name }}.qcow2 dist/secai-os-${{ github.ref_name }}.ova; do
cosign sign-blob --yes \
--key env://COSIGN_PRIVATE_KEY \
--output-signature "${f}.sig" \
"$f"
done
env:
COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}
- name: Upload VM artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: vm-images
path: dist/secai-os-*
provenance:
name: SLSA Provenance & Attestation
runs-on: ubuntu-latest
needs: [build-go, build-python, build-iso]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install cosign
run: |
COSIGN_VERSION="v2.4.3"
curl -sSfL "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64" \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
cosign version
- name: Download all artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: dist/
merge-multiple: true
- name: Record release image digest
run: |
IMAGE_REF="ghcr.io/${{ github.repository }}"
TAG="${{ github.ref_name }}"
DIGEST=$(skopeo inspect "docker://${IMAGE_REF}:${TAG}" 2>/dev/null | jq -r '.Digest' || echo "")
if [ -n "$DIGEST" ] && [ "$DIGEST" != "null" ]; then
echo "${DIGEST}" > dist/IMAGE_DIGEST
echo "${IMAGE_REF}@${DIGEST}" > dist/IMAGE_REF_PINNED
echo "## Install with digest pinning" >> "$GITHUB_STEP_SUMMARY"
echo '```bash' >> "$GITHUB_STEP_SUMMARY"
echo "sudo bash secai-bootstrap.sh --digest ${DIGEST}" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
else
echo "WARNING: Could not extract image digest for tag ${TAG}"
echo "unknown" > dist/IMAGE_DIGEST
fi
- name: Generate release manifest
run: |
cd dist
# Collect binary names + SHA256 hashes
BINARIES_JSON="[]"
for bin in *-linux-*; do
[ -f "$bin" ] || continue
HASH=$(sha256sum "$bin" | awk '{print $1}')
BINARIES_JSON=$(echo "$BINARIES_JSON" | jq \
--arg name "$bin" --arg sha256 "$HASH" \
'. + [{"name": $name, "sha256": $sha256}]')
done
# Collect SBOM filenames
SBOMS_JSON="[]"
for sbom in *-sbom.cdx.json; do
[ -f "$sbom" ] || continue
SBOMS_JSON=$(echo "$SBOMS_JSON" | jq \
--arg name "$sbom" \
'. + [$name]')
done
# Read image digest
IMAGE_DIGEST="unknown"
if [ -f IMAGE_DIGEST ]; then
IMAGE_DIGEST=$(cat IMAGE_DIGEST)
fi
IMAGE_REF_PINNED=""
if [ -f IMAGE_REF_PINNED ]; then
IMAGE_REF_PINNED=$(cat IMAGE_REF_PINNED)
fi
# Build manifest JSON
jq -n \
--arg schema_version "1" \
--arg tag "${{ github.ref_name }}" \
--arg image_ref "ghcr.io/${{ github.repository }}" \
--arg image_digest "$IMAGE_DIGEST" \
--arg image_ref_pinned "$IMAGE_REF_PINNED" \
--argjson binaries "$BINARIES_JSON" \
--argjson sboms "$SBOMS_JSON" \
--arg provenance_type "https://slsa.dev/provenance/v1" \
--arg checksum_file "SHA256SUMS" \
--arg signature_file "SHA256SUMS.sig" \
--arg commit_sha "${{ github.sha }}" \
--arg workflow_run "${{ github.run_id }}" \
--arg workflow_ref "${{ github.workflow_ref }}" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{
schema_version: $schema_version,
tag: $tag,
image: {
ref: $image_ref,
digest: $image_digest,
ref_pinned: $image_ref_pinned
},
binaries: $binaries,
sboms: $sboms,
provenance: {
type: $provenance_type,
attested: true
},
checksums: {
file: $checksum_file,
signature: $signature_file
},
build: {
commit_sha: $commit_sha,
workflow_run: $workflow_run,
workflow_ref: $workflow_ref,
timestamp: $timestamp
}
}' > RELEASE_MANIFEST.json
# Add install artifacts (conditionally present)
INSTALL_JSON="{}"
for artifact in secai-os-*.iso secai-os-*.qcow2 secai-os-*.ova; do
[ -f "$artifact" ] || continue
HASH=$(sha256sum "$artifact" | awk '{print $1}')
SIZE=$(stat -c%s "$artifact" 2>/dev/null || stat -f%z "$artifact" 2>/dev/null || echo 0)
TYPE="${artifact##*.}"
INSTALL_JSON=$(echo "$INSTALL_JSON" | jq \
--arg type "$TYPE" --arg name "$artifact" \
--arg sha256 "$HASH" --arg size "$SIZE" \
--arg sig "${artifact}.sig" \
'. + {($type): {"name": $name, "sha256": $sha256, "size_bytes": ($size | tonumber), "signature": $sig}}')
done
# Merge install_artifacts into manifest
jq --argjson install "$INSTALL_JSON" \
'. + {install_artifacts: $install}' RELEASE_MANIFEST.json > RELEASE_MANIFEST.json.tmp
mv RELEASE_MANIFEST.json.tmp RELEASE_MANIFEST.json
echo "--- RELEASE_MANIFEST.json ---"
cat RELEASE_MANIFEST.json
- name: Generate SHA256 checksums
run: |
cd dist
sha256sum * > SHA256SUMS
cat SHA256SUMS
- name: Sign checksums with cosign
run: |
cosign sign-blob --yes \
--key env://COSIGN_PRIVATE_KEY \
--output-signature dist/SHA256SUMS.sig \
dist/SHA256SUMS
env:
COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}
- name: Attest build provenance
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
with:
subject-path: "dist/*-linux-*"
- name: Attest SBOMs
run: |
for sbom in dist/*-sbom.cdx.json; do
service=$(basename "$sbom" -sbom.cdx.json)
cosign attest --yes --type cyclonedx \
--predicate "$sbom" \
--key env://COSIGN_PRIVATE_KEY \
ghcr.io/${{ github.repository }}:${{ github.ref_name }}-${service} || \
echo "WARN: cosign attest skipped for ${service} (no matching image)"
done
env:
COSIGN_PRIVATE_KEY: ${{ secrets.SIGNING_SECRET }}
- name: Create GitHub Release
if: ${{ !inputs.dry_run }}
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
with:
files: |
dist/*-linux-*
dist/*-sbom.cdx.json
dist/SHA256SUMS
dist/SHA256SUMS.sig
dist/IMAGE_DIGEST
dist/IMAGE_REF_PINNED
dist/RELEASE_MANIFEST.json
dist/secai-os-*.iso
dist/secai-os-*.iso.sig
dist/secai-os-*.qcow2
dist/secai-os-*.qcow2.sig
dist/secai-os-*.ova
dist/secai-os-*.ova.sig
generate_release_notes: true
fail_on_unmatched_files: false