Fix ISO build: use :latest tag (BlueBuild doesn't tag :vX.Y.Z) #8
Workflow file for this run
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
| 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 |