release: v0.16.2 #83
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*'] | |
| permissions: | |
| contents: write | |
| attestations: write | |
| id-token: write | |
| jobs: | |
| preflight: | |
| runs-on: macos-14 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Select Xcode 16.2 | |
| run: sudo xcode-select -s /Applications/Xcode_16.2.app | |
| - name: Import Apple certificate (preflight) | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| run: | | |
| echo "$APPLE_CERTIFICATE" | base64 --decode > cert.p12 | |
| KEYCHAIN="preflight-$$.keychain" | |
| security create-keychain -p "" "$KEYCHAIN" | |
| security import cert.p12 -k "$KEYCHAIN" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple: -k "" "$KEYCHAIN" | |
| IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN" | grep "Developer ID" || true) | |
| if [ -z "$IDENTITY" ]; then | |
| echo "::error::No Developer ID signing identity found. Check APPLE_CERTIFICATE secret." | |
| echo "::error::If the p12 was created with OpenSSL 3.x, it uses PBES2/AES encryption." | |
| echo "::error::macOS requires legacy 3DES. Re-export with: scripts/fix_p12_legacy.sh" | |
| exit 1 | |
| fi | |
| echo "Signing identity: $IDENTITY" | |
| security delete-keychain "$KEYCHAIN" | |
| rm cert.p12 | |
| - name: Verify Tauri signing key | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| run: | | |
| if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then | |
| echo "::error::TAURI_SIGNING_PRIVATE_KEY secret is not set" | |
| exit 1 | |
| fi | |
| echo "Tauri signing key is set" | |
| - name: Verify notarization credentials | |
| env: | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} | |
| APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_PATH }} | |
| run: | | |
| if [ -z "$APPLE_API_ISSUER" ] || [ -z "$APPLE_API_KEY" ] || [ -z "$APPLE_API_KEY_CONTENT" ]; then | |
| echo "::error::Notarization secrets missing (APPLE_API_ISSUER, APPLE_API_KEY, or APPLE_API_KEY_PATH)" | |
| exit 1 | |
| fi | |
| mkdir -p "$RUNNER_TEMP/private_keys" | |
| echo "$APPLE_API_KEY_CONTENT" > "$RUNNER_TEMP/private_keys/AuthKey_${APPLE_API_KEY}.p8" | |
| xcrun notarytool history \ | |
| --key "$RUNNER_TEMP/private_keys/AuthKey_${APPLE_API_KEY}.p8" \ | |
| --key-id "$APPLE_API_KEY" \ | |
| --issuer "$APPLE_API_ISSUER" \ | |
| >/dev/null 2>&1 \ | |
| && echo "Notarization credentials verified (notarytool history succeeded)" \ | |
| || { echo "::error::notarytool history failed -- check APPLE_API_ISSUER, APPLE_API_KEY, and APPLE_API_KEY_PATH secrets"; exit 1; } | |
| rm -rf "$RUNNER_TEMP/private_keys" | |
| build-assets: | |
| needs: preflight | |
| strategy: | |
| matrix: | |
| include: | |
| - arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| rust-target: aarch64-unknown-linux-musl | |
| - arch: x86_64 | |
| runner: ubuntu-24.04 | |
| rust-target: x86_64-unknown-linux-musl | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: docker/setup-buildx-action@v4 | |
| - uses: astral-sh/setup-uv@v5 | |
| - run: uv sync | |
| - uses: extractions/setup-just@v3 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.rust-target }} | |
| - name: Build VM assets (kernel + rootfs) | |
| run: | | |
| just build-kernel ${{ matrix.arch }} | |
| just build-rootfs ${{ matrix.arch }} | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: vm-assets-${{ matrix.arch }} | |
| path: assets/${{ matrix.arch }}/ | |
| test: | |
| needs: preflight | |
| runs-on: macos-14 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| components: llvm-tools | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| key: test | |
| - uses: pnpm/action-setup@v5 | |
| with: | |
| version: 9 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| cache-dependency-path: frontend/pnpm-lock.yaml | |
| - run: cd frontend && pnpm install --frozen-lockfile | |
| - name: Dependency audit | |
| run: | | |
| cargo install cargo-audit --locked | |
| cargo audit | |
| cd frontend && pnpm audit | |
| - name: Frontend type-check, test, and build | |
| run: cd frontend && pnpm run check && pnpm run test && pnpm run build | |
| - name: Install cargo-llvm-cov | |
| run: cargo install cargo-llvm-cov --locked | |
| - name: Create stub assets for Tauri build.rs | |
| run: | | |
| mkdir -p assets/current | |
| touch assets/current/vmlinuz assets/current/initrd.img | |
| echo '{"releases":{}}' > assets/manifest.json | |
| - name: Unit tests with coverage | |
| run: | | |
| cargo llvm-cov --workspace --no-cfg-coverage --codecov --output-path codecov.json 2>&1 | tee test-output.txt | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| files: codecov.json | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| fail_ci_if_error: false | |
| - name: Test summary | |
| if: always() | |
| run: | | |
| # Parse "test result: ok. X passed; Y failed" lines from cargo output | |
| TESTS=$(grep -o '[0-9]* passed' test-output.txt 2>/dev/null | awk '{s+=$1} END {print s+0}') | |
| FAILED=$(grep -o '[0-9]* failed' test-output.txt 2>/dev/null | awk '{s+=$1} END {print s+0}') | |
| cat >> "$GITHUB_STEP_SUMMARY" << EOF | |
| ## Test Results | |
| | Metric | Result | | |
| |--------|--------| | |
| | Rust tests passed | $TESTS | | |
| | Rust tests failed | $FAILED | | |
| | Frontend tests | vitest | | |
| | Audit | cargo audit + pnpm audit | | |
| EOF | |
| build-app-macos: | |
| needs: [preflight, build-assets] | |
| runs-on: macos-14 | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Select Xcode 16.2 | |
| run: sudo xcode-select -s /Applications/Xcode_16.2.app | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: vm-assets-arm64 | |
| path: assets/arm64/ | |
| # Regenerate manifest for this arch (creates assets/current symlink). | |
| - uses: astral-sh/setup-uv@v5 | |
| - run: uv sync | |
| - name: Generate manifest | |
| run: | | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| uv run python3 -c " | |
| from pathlib import Path | |
| from capsem.builder.docker import generate_checksums | |
| generate_checksums(Path('assets'), '$VERSION') | |
| " | |
| # Replace symlink with real copy -- GitHub Actions strips symlinks | |
| # and Tauri build.rs needs assets/current/ to exist as a real dir. | |
| - name: Copy assets/current | |
| run: | | |
| rm -rf assets/current | |
| cp -r assets/arm64 assets/current | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| key: build-app-macos | |
| - uses: pnpm/action-setup@v5 | |
| with: | |
| version: 9 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| cache-dependency-path: frontend/pnpm-lock.yaml | |
| - run: cd frontend && pnpm install --frozen-lockfile | |
| - name: Build frontend | |
| run: cd frontend && pnpm build | |
| - name: Install cargo tools | |
| run: cargo install tauri-cli cargo-auditable cargo-sbom --locked | |
| - name: Import Apple certificate | |
| if: env.APPLE_CERTIFICATE != '' | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| run: | | |
| echo "$APPLE_CERTIFICATE" | base64 --decode > cert.p12 | |
| security create-keychain -p "" build.keychain | |
| security default-keychain -s build.keychain | |
| security unlock-keychain -p "" build.keychain | |
| security set-keychain-settings -t 3600 build.keychain | |
| security import cert.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple: -k "" build.keychain | |
| security list-keychains -d user -s build.keychain $(security list-keychains -d user | tr -d '"') | |
| - name: Write Apple API key file | |
| env: | |
| APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_PATH }} | |
| run: | | |
| mkdir -p "$RUNNER_TEMP/private_keys" | |
| echo "$APPLE_API_KEY_CONTENT" > "$RUNNER_TEMP/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8" | |
| - name: Verify assets layout | |
| run: | | |
| echo "=== assets/ ===" | |
| ls -la assets/ | |
| echo "=== assets/arm64/ ===" | |
| ls -la assets/arm64/ | |
| echo "=== assets/current/ ===" | |
| ls -la assets/current/ | |
| echo "=== assets/manifest.json ===" | |
| cat assets/manifest.json | head -5 | |
| - name: Validate source artifacts (pre-build gate) | |
| run: | | |
| uv run python3 -c " | |
| from capsem.builder.doctor import check_source_files | |
| from pathlib import Path | |
| result = check_source_files(Path('.')) | |
| if not result.passed: | |
| raise SystemExit(f'Source file check failed: {result.detail}') | |
| print(f'OK: {result.detail}') | |
| " | |
| - name: Build and sign app | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} | |
| APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} | |
| APPLE_API_KEY_PATH: ${{ runner.temp }}/private_keys/AuthKey_${{ secrets.APPLE_API_KEY }}.p8 | |
| run: | | |
| cd crates/capsem-app | |
| cargo tauri build --skip-stapling | |
| - name: Generate SBOM | |
| run: cargo sbom --output-format spdx_json_2_3 > capsem-sbom.spdx.json | |
| - name: Collect macOS artifacts | |
| run: | | |
| mkdir -p release-artifacts | |
| cp target/release/bundle/dmg/*.dmg release-artifacts/ | |
| cp capsem-sbom.spdx.json release-artifacts/ | |
| ls -lh release-artifacts/ | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-macos | |
| path: release-artifacts/ | |
| build-app-linux: | |
| needs: [preflight, build-assets] | |
| strategy: | |
| matrix: | |
| include: | |
| - arch: arm64 | |
| runner: ubuntu-24.04-arm | |
| bundles: deb | |
| - arch: x86_64 | |
| runner: ubuntu-24.04 | |
| bundles: deb | |
| runs-on: ${{ matrix.runner }} | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: vm-assets-${{ matrix.arch }} | |
| path: assets/${{ matrix.arch }}/ | |
| # Regenerate manifest for this arch (creates assets/current symlink). | |
| - uses: astral-sh/setup-uv@v5 | |
| - run: uv sync | |
| - name: Generate manifest | |
| run: | | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| uv run python3 -c " | |
| from pathlib import Path | |
| from capsem.builder.docker import generate_checksums | |
| generate_checksums(Path('assets'), '$VERSION') | |
| " | |
| # Replace symlink with real copy -- GitHub Actions strips symlinks | |
| # and Tauri build.rs needs assets/current/ to exist as a real dir. | |
| - name: Copy assets/current | |
| run: | | |
| rm -rf assets/current | |
| cp -r assets/${{ matrix.arch }} assets/current | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| key: build-app-linux-${{ matrix.arch }} | |
| - name: Install Tauri system deps | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libssl-dev libgtk-3-dev libwebkit2gtk-4.1-dev xdg-utils xvfb | |
| - uses: pnpm/action-setup@v5 | |
| with: | |
| version: 9 | |
| - uses: actions/setup-node@v5 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| cache-dependency-path: frontend/pnpm-lock.yaml | |
| - run: cd frontend && pnpm install --frozen-lockfile | |
| - name: Build frontend | |
| run: cd frontend && pnpm build | |
| - name: Install cargo tools | |
| run: cargo install tauri-cli cargo-auditable --locked | |
| - name: Verify assets layout | |
| run: | | |
| echo "=== assets/ ===" | |
| ls -la assets/ | |
| echo "=== assets/${{ matrix.arch }}/ ===" | |
| ls -la assets/${{ matrix.arch }}/ | |
| echo "=== assets/current/ ===" | |
| ls -la assets/current/ | |
| echo "=== assets/manifest.json ===" | |
| cat assets/manifest.json | head -5 | |
| - name: Validate source artifacts (pre-build gate) | |
| run: | | |
| uv run python3 -c " | |
| from capsem.builder.doctor import check_source_files | |
| from pathlib import Path | |
| result = check_source_files(Path('.')) | |
| if not result.passed: | |
| raise SystemExit(f'Source file check failed: {result.detail}') | |
| print(f'OK: {result.detail}') | |
| " | |
| - name: Validate rootfs contains all required binaries | |
| run: | | |
| ROOTFS="assets/${{ matrix.arch }}/rootfs.squashfs" | |
| MOUNT=$(mktemp -d) | |
| sudo mount -t squashfs -o loop,ro "$ROOTFS" "$MOUNT" | |
| MISSING="" | |
| for bin in capsem-pty-agent capsem-net-proxy capsem-mcp-server \ | |
| capsem-doctor capsem-bench snapshots; do | |
| if [ ! -f "$MOUNT/usr/local/bin/$bin" ]; then | |
| MISSING="$MISSING $bin" | |
| fi | |
| done | |
| sudo umount "$MOUNT" | |
| rmdir "$MOUNT" | |
| if [ -n "$MISSING" ]; then | |
| echo "::error::rootfs is missing required binaries:$MISSING" | |
| exit 1 | |
| fi | |
| echo "All required binaries present in rootfs" | |
| - name: Build app | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| cd crates/capsem-app | |
| cargo tauri build --bundles ${{ matrix.bundles }} | |
| - name: Validate artifacts | |
| run: | | |
| echo "=== Validate deb ===" | |
| dpkg-deb --info target/release/bundle/deb/*.deb | |
| - name: Boot test (x86_64) | |
| if: matrix.arch == 'x86_64' | |
| run: | | |
| ls -l /dev/kvm || echo "KVM MISSING (pre-udev)" | |
| echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules | |
| sudo udevadm control --reload-rules | |
| sudo udevadm trigger --name-match=kvm | |
| ls -l /dev/kvm | |
| # Probe KVM capability: nested/restricted KVM may lack CPUID support | |
| # which makes full VM boot impossible. Skip gracefully. | |
| if python3 -c " | |
| import fcntl, os, struct, array | |
| kvm = os.open('/dev/kvm', os.O_RDWR) | |
| vm = fcntl.ioctl(kvm, 0xAE01) # KVM_CREATE_VM | |
| buf = struct.pack('II', 256, 0) + b'\x00' * (256 * 40) | |
| try: | |
| fcntl.ioctl(vm, 0xC008AE05, buf, True) # KVM_GET_SUPPORTED_CPUID | |
| print('KVM supports CPUID -- boot test will run') | |
| except OSError as e: | |
| print(f'KVM lacks CPUID support ({e}) -- skipping boot test') | |
| os._exit(1) | |
| finally: | |
| os.close(vm); os.close(kvm) | |
| "; then | |
| sudo dpkg -i target/release/bundle/deb/*.deb || sudo apt-get install -f -y | |
| xvfb-run timeout 120 capsem run "capsem-doctor" | |
| else | |
| echo "::warning::Skipping x86_64 boot test -- KVM on this runner lacks full VM support" | |
| fi | |
| - name: Collect Linux artifacts | |
| run: | | |
| mkdir -p release-artifacts | |
| cp target/release/bundle/deb/*.deb release-artifacts/ | |
| ls -lh release-artifacts/ | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: release-linux-${{ matrix.arch }} | |
| path: release-artifacts/ | |
| create-release: | |
| needs: [test, build-app-macos, build-app-linux] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| # Download all platform artifacts. | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: release-macos | |
| path: release-artifacts/ | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: release-linux-arm64 | |
| path: release-artifacts/ | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: release-linux-x86_64 | |
| path: release-artifacts/ | |
| # Download per-arch VM assets for the release. | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: vm-assets-arm64 | |
| path: release-artifacts/arm64/ | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: vm-assets-x86_64 | |
| path: release-artifacts/x86_64/ | |
| # Regenerate unified manifest.json from both arch dirs. | |
| - uses: astral-sh/setup-uv@v5 | |
| - run: uv sync | |
| - name: Generate unified manifest | |
| run: | | |
| # Set up a temp assets dir with both arch subdirs for checksums. | |
| mkdir -p unified-assets/arm64 unified-assets/x86_64 | |
| cp release-artifacts/arm64/* unified-assets/arm64/ | |
| cp release-artifacts/x86_64/* unified-assets/x86_64/ | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| uv run python3 -c " | |
| from pathlib import Path | |
| from capsem.builder.docker import generate_checksums | |
| generate_checksums(Path('unified-assets'), '$VERSION') | |
| " | |
| cp unified-assets/manifest.json release-artifacts/manifest.json | |
| # Accumulate manifest.json: merge new version's entry with previous releases. | |
| - name: Build accumulated manifest | |
| run: | | |
| gh release download --pattern manifest.json -D /tmp/prev-manifest 2>/dev/null || true | |
| if [ -f /tmp/prev-manifest/manifest.json ]; then | |
| python3 -c " | |
| import json, sys | |
| with open('release-artifacts/manifest.json') as f: | |
| new = json.load(f) | |
| with open('/tmp/prev-manifest/manifest.json') as f: | |
| prev = json.load(f) | |
| for ver, entry in prev.get('releases', {}).items(): | |
| if ver not in new['releases']: | |
| new['releases'][ver] = entry | |
| with open('release-artifacts/manifest.json', 'w') as f: | |
| json.dump(new, f, indent=2) | |
| print(f'Merged manifest: {len(new[\"releases\"])} releases') | |
| " | |
| else | |
| echo "No previous manifest found, using fresh manifest" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Sign manifest | |
| run: | | |
| sudo apt-get update && sudo apt-get install -y minisign | |
| echo "$MINISIGN_SECRET_KEY" > /tmp/manifest-sign.key | |
| minisign -S -s /tmp/manifest-sign.key -m release-artifacts/manifest.json | |
| rm /tmp/manifest-sign.key | |
| env: | |
| MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }} | |
| - name: Attest build provenance (DMG + deb + rootfs per arch) | |
| uses: actions/attest-build-provenance@v4 | |
| with: | |
| subject-path: | | |
| release-artifacts/*.dmg | |
| release-artifacts/*.deb | |
| release-artifacts/arm64/rootfs.squashfs | |
| release-artifacts/x86_64/rootfs.squashfs | |
| - name: Attest SBOM | |
| uses: actions/attest@v4 | |
| with: | |
| subject-path: | | |
| release-artifacts/*.dmg | |
| release-artifacts/*.deb | |
| predicate-type: https://spdx.dev/Document/v2.3 | |
| predicate-path: release-artifacts/capsem-sbom.spdx.json | |
| - name: Build summary | |
| run: | | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| DMG=$(ls -1 release-artifacts/*.dmg 2>/dev/null | head -1) | |
| DMG_NAME=$(basename "$DMG" 2>/dev/null || echo "N/A") | |
| DMG_SIZE=$(du -h "$DMG" 2>/dev/null | cut -f1 || echo "N/A") | |
| ARM64_ROOTFS=$(du -h release-artifacts/arm64/rootfs.squashfs 2>/dev/null | cut -f1 || echo "N/A") | |
| X86_ROOTFS=$(du -h release-artifacts/x86_64/rootfs.squashfs 2>/dev/null | cut -f1 || echo "N/A") | |
| SBOM_PKGS=$(python3 -c "import json; d=json.load(open('release-artifacts/capsem-sbom.spdx.json')); print(len(d.get('packages',[])))" 2>/dev/null || echo "?") | |
| # Build artifact table rows for all debs | |
| LINUX_ROWS="" | |
| for f in release-artifacts/*.deb; do | |
| [ -f "$f" ] || continue | |
| NAME=$(basename "$f") | |
| SIZE=$(du -h "$f" | cut -f1) | |
| LINUX_ROWS="${LINUX_ROWS}| ${NAME} | ${SIZE} | | |
| " | |
| done | |
| cat >> "$GITHUB_STEP_SUMMARY" << EOF | |
| ## Release $VERSION | |
| ### Artifacts | |
| | File | Size | | |
| |------|------| | |
| | $DMG_NAME | $DMG_SIZE | | |
| ${LINUX_ROWS}| rootfs.squashfs (arm64) | $ARM64_ROOTFS | | |
| | rootfs.squashfs (x86_64) | $X86_ROOTFS | | |
| | manifest.json | signed (minisign) | | |
| | capsem-sbom.spdx.json | $SBOM_PKGS packages | | |
| ### Security | |
| - Apple codesigned (Developer ID) | |
| - Notarization submitted (skip-stapling) | |
| - SLSA build provenance attested (DMG + deb + rootfs) | |
| - SBOM attested (SPDX 2.3, DMG + deb) | |
| - Manifest signed (minisign) | |
| EOF | |
| - name: Create GitHub release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # Build release notes with tool versions | |
| NOTES="See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details." | |
| NOTES="${NOTES} | |
| ### VM Environment | |
| " | |
| if [ -f release-artifacts/arm64/tool-versions.txt ]; then | |
| NOTES="${NOTES}| Tool | Version | | |
| |------|---------| | |
| " | |
| while IFS='=' read -r tool version; do | |
| [ -n "$tool" ] && NOTES="${NOTES}| ${tool} | ${version} | | |
| " | |
| done < release-artifacts/arm64/tool-versions.txt | |
| fi | |
| # Create release with platform-independent artifacts first | |
| gh release create ${{ github.ref_name }} \ | |
| release-artifacts/*.dmg release-artifacts/*.deb \ | |
| release-artifacts/manifest.json release-artifacts/manifest.json.minisig \ | |
| release-artifacts/capsem-sbom.spdx.json \ | |
| --title "Capsem ${{ github.ref_name }}" \ | |
| --notes "$NOTES" | |
| # Upload per-arch VM assets with arch prefix (both have vmlinuz, initrd.img, etc.) | |
| # gh release upload uses the filename as the asset name, so we must | |
| # rename files to ${arch}-${base} before uploading to avoid collisions. | |
| for arch in arm64 x86_64; do | |
| for f in release-artifacts/$arch/*; do | |
| [ -f "$f" ] || continue | |
| base=$(basename "$f") | |
| mv "$f" "release-artifacts/$arch/${arch}-${base}" | |
| gh release upload ${{ github.ref_name }} "release-artifacts/$arch/${arch}-${base}" | |
| done | |
| done |