# .gitea/workflows/release-evidence-pack.yml # Generates Release Evidence Pack for customer-facing verification # # This workflow depends on all test pipelines completing successfully before # generating the evidence pack to ensure only verified releases are attested. name: Release Evidence Pack on: workflow_dispatch: inputs: version: description: "Release version (e.g., 1.2.3)" required: true type: string release_tag: description: "Git tag for the release" required: true type: string signing_mode: description: "Signing mode" required: false default: "keyless" type: choice options: - keyless - key-based include_rekor_proofs: description: "Include Rekor transparency log proofs" required: false default: true type: boolean # Trigger after release workflow completes workflow_run: workflows: ["Release Bundle"] types: [completed] branches: [main] env: DOTNET_VERSION: "10.0.100" EVIDENCE_PACK_DIR: ${{ github.workspace }}/evidence-pack jobs: # ============================================================================ # Gate: Ensure all test pipelines have passed # ============================================================================ verify-test-gates: runs-on: ${{ vars.LINUX_RUNNER_LABEL || 'ubuntu-latest' }} if: >- github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') outputs: tests_passed: ${{ steps.check-tests.outputs.passed }} release_version: ${{ steps.meta.outputs.version }} release_tag: ${{ steps.meta.outputs.tag }} steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.release_tag || github.event.workflow_run.head_sha }} fetch-depth: 0 - name: Determine release metadata id: meta run: | set -euo pipefail if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then VERSION="${{ github.event.inputs.version }}" TAG="${{ github.event.inputs.release_tag }}" else # Extract from workflow_run TAG="${{ github.event.workflow_run.head_branch }}" VERSION="${TAG#v}" fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Verify test workflows have passed id: check-tests env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail SHA="${{ steps.meta.outputs.sha || github.sha }}" echo "Checking test status for commit: $SHA" # Required workflows that must pass REQUIRED_WORKFLOWS=( "Build Test Deploy" "test-matrix" "integration-tests-gate" "security-testing" "determinism-gate" ) FAILED=() PENDING=() for workflow in "${REQUIRED_WORKFLOWS[@]}"; do echo "Checking workflow: $workflow" # Get workflow runs for this commit STATUS=$(gh api \ "/repos/${{ github.repository }}/actions/workflows" \ --jq ".workflows[] | select(.name == \"$workflow\") | .id" 2>/dev/null || echo "") if [ -z "$STATUS" ]; then echo " Warning: Workflow '$workflow' not found, skipping..." continue fi # Get latest run for this commit RUN_STATUS=$(gh api \ "/repos/${{ github.repository }}/actions/workflows/$STATUS/runs?head_sha=$SHA&per_page=1" \ --jq '.workflow_runs[0].conclusion // .workflow_runs[0].status' 2>/dev/null || echo "not_found") echo " Status: $RUN_STATUS" case "$RUN_STATUS" in success|skipped) echo " ✓ Passed" ;; in_progress|queued|waiting|pending) PENDING+=("$workflow") ;; not_found) echo " ⚠ No run found for this commit" ;; *) FAILED+=("$workflow ($RUN_STATUS)") ;; esac done if [ ${#FAILED[@]} -gt 0 ]; then echo "::error::The following required workflows have not passed: ${FAILED[*]}" echo "passed=false" >> "$GITHUB_OUTPUT" exit 1 fi if [ ${#PENDING[@]} -gt 0 ]; then echo "::warning::The following workflows are still running: ${PENDING[*]}" echo "::warning::Consider waiting for them to complete before generating evidence pack." fi echo "✓ All required test workflows have passed" echo "passed=true" >> "$GITHUB_OUTPUT" # ============================================================================ # Build Evidence Pack # ============================================================================ build-evidence-pack: runs-on: ${{ vars.LINUX_RUNNER_LABEL || 'ubuntu-latest' }} needs: verify-test-gates if: needs.verify-test-gates.outputs.tests_passed == 'true' permissions: contents: write id-token: write # For keyless signing packages: read env: VERSION: ${{ needs.verify-test-gates.outputs.release_version }} TAG: ${{ needs.verify-test-gates.outputs.release_tag }} steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ env.TAG }} fetch-depth: 0 - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} include-prerelease: true - name: Install Cosign uses: sigstore/cosign-installer@v3.4.0 - name: Install Syft run: | set -euo pipefail SYFT_VERSION="v1.21.0" curl -fsSL "https://github.com/anchore/syft/releases/download/${SYFT_VERSION}/syft_${SYFT_VERSION#v}_linux_amd64.tar.gz" -o /tmp/syft.tgz tar -xzf /tmp/syft.tgz -C /tmp sudo install -m 0755 /tmp/syft /usr/local/bin/syft - name: Install rekor-cli run: | set -euo pipefail REKOR_VERSION="v1.3.6" curl -fsSL "https://github.com/sigstore/rekor/releases/download/${REKOR_VERSION}/rekor-cli-linux-amd64" -o /tmp/rekor-cli sudo install -m 0755 /tmp/rekor-cli /usr/local/bin/rekor-cli - name: Download release artifacts env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail mkdir -p artifacts/ gh release download "$TAG" -D artifacts/ || { echo "::warning::Could not download release artifacts. Using build artifacts instead." # Fallback: download from workflow artifacts gh run download --name "stellaops-release-$VERSION" -D artifacts/ || true } ls -la artifacts/ - name: Compute SOURCE_DATE_EPOCH id: epoch run: | set -euo pipefail EPOCH=$(git show -s --format=%ct HEAD) echo "epoch=$EPOCH" >> "$GITHUB_OUTPUT" echo "SOURCE_DATE_EPOCH=$EPOCH" - name: Generate checksums run: | set -euo pipefail mkdir -p checksums/ cd artifacts/ sha256sum * 2>/dev/null | grep -v '\.sig$' | grep -v '\.cert$' > ../checksums/SHA256SUMS || true sha512sum * 2>/dev/null | grep -v '\.sig$' | grep -v '\.cert$' > ../checksums/SHA512SUMS || true cd .. echo "Generated checksums:" cat checksums/SHA256SUMS - name: Sign checksums env: COSIGN_EXPERIMENTAL: "1" COSIGN_KEY_REF: ${{ secrets.COSIGN_KEY_REF }} COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} run: | set -euo pipefail SIGN_ARGS=(--yes) if [ "${{ github.event.inputs.signing_mode || 'keyless' }}" = "key-based" ] && [ -n "${COSIGN_KEY_REF:-}" ]; then SIGN_ARGS+=(--key "$COSIGN_KEY_REF") fi cosign sign-blob "${SIGN_ARGS[@]}" \ --output-signature checksums/SHA256SUMS.sig \ --output-certificate checksums/SHA256SUMS.cert \ checksums/SHA256SUMS cosign sign-blob "${SIGN_ARGS[@]}" \ --output-signature checksums/SHA512SUMS.sig \ --output-certificate checksums/SHA512SUMS.cert \ checksums/SHA512SUMS echo "✓ Checksums signed" - name: Generate SBOMs run: | set -euo pipefail mkdir -p sbom/ for artifact in artifacts/stella-*.tar.gz artifacts/stella-*.zip; do [ -f "$artifact" ] || continue base=$(basename "$artifact" | sed 's/\.\(tar\.gz\|zip\)$//') echo "Generating SBOM for: $base" syft "$artifact" -o cyclonedx-json > "sbom/${base}.cdx.json" done # Sign SBOMs for sbom in sbom/*.cdx.json; do [ -f "$sbom" ] || continue SIGN_ARGS=(--yes) if [ "${{ github.event.inputs.signing_mode || 'keyless' }}" = "key-based" ] && [ -n "${COSIGN_KEY_REF:-}" ]; then SIGN_ARGS+=(--key "$COSIGN_KEY_REF") fi cosign sign-blob "${SIGN_ARGS[@]}" \ --output-signature "${sbom}.sig" \ --output-certificate "${sbom}.cert" \ "$sbom" done echo "✓ SBOMs generated and signed" - name: Generate SLSA provenance run: | set -euo pipefail mkdir -p provenance/ SOURCE_EPOCH="${{ steps.epoch.outputs.epoch }}" GIT_SHA="${{ github.sha }}" BUILD_TIME=$(date -u -d "@$SOURCE_EPOCH" +"%Y-%m-%dT%H:%M:%SZ") # Generate SLSA v1.0 provenance for each artifact for artifact in artifacts/stella-*.tar.gz artifacts/stella-*.zip; do [ -f "$artifact" ] || continue base=$(basename "$artifact" | sed 's/\.\(tar\.gz\|zip\)$//') ARTIFACT_SHA256=$(sha256sum "$artifact" | awk '{print $1}') cat > "provenance/${base}.slsa.intoto.jsonl" </dev/null | head -1 || echo "") if [ -n "$ENTRY" ]; then UUID=$(basename "$ENTRY") echo " Found entry: $UUID" # Get the full entry rekor-cli get --uuid "$UUID" --format json > "rekor-proofs/log-entries/${UUID}.json" 2>/dev/null || true fi done # Get current checkpoint rekor-cli loginfo --format json > rekor-proofs/checkpoint.json 2>/dev/null || true echo "✓ Rekor proofs collected" - name: Extract signing key fingerprint id: key-fingerprint run: | set -euo pipefail # Extract fingerprint from certificate or key if [ -f checksums/SHA256SUMS.cert ]; then FINGERPRINT=$(openssl x509 -in checksums/SHA256SUMS.cert -noout -fingerprint -sha256 2>/dev/null | cut -d= -f2 | tr -d ':' | tr '[:upper:]' '[:lower:]') elif [ -n "${COSIGN_KEY_REF:-}" ]; then FINGERPRINT="key-based-signing" else FINGERPRINT="keyless-fulcio" fi echo "fingerprint=$FINGERPRINT" >> "$GITHUB_OUTPUT" - name: Build evidence pack using .NET tool run: | set -euo pipefail # Build the EvidencePack library dotnet build src/Attestor/__Libraries/StellaOps.Attestor.EvidencePack/StellaOps.Attestor.EvidencePack.csproj \ --configuration Release # Create evidence pack structure manually for now # (CLI tool would be: dotnet run --project src/Attestor/.../EvidencePack.Cli build-pack ...) PACK_DIR="evidence-pack/stella-release-${VERSION}-evidence-pack" mkdir -p "$PACK_DIR"/{artifacts,checksums,sbom,provenance,attestations,rekor-proofs/log-entries} # Copy files cp -r artifacts/* "$PACK_DIR/artifacts/" 2>/dev/null || true cp -r checksums/* "$PACK_DIR/checksums/" 2>/dev/null || true cp -r sbom/* "$PACK_DIR/sbom/" 2>/dev/null || true cp -r provenance/* "$PACK_DIR/provenance/" 2>/dev/null || true cp -r rekor-proofs/* "$PACK_DIR/rekor-proofs/" 2>/dev/null || true # Copy signing public key if [ -f checksums/SHA256SUMS.cert ]; then # Extract public key from certificate openssl x509 -in checksums/SHA256SUMS.cert -pubkey -noout > "$PACK_DIR/cosign.pub" elif [ -n "${COSIGN_PUBLIC_KEY:-}" ]; then echo "$COSIGN_PUBLIC_KEY" > "$PACK_DIR/cosign.pub" fi # Generate manifest.json cat > "$PACK_DIR/manifest.json" < "$PACK_DIR/VERIFY.md" echo "✓ Evidence pack built" ls -la "$PACK_DIR/" - name: Self-verify evidence pack run: | set -euo pipefail cd "evidence-pack/stella-release-${VERSION}-evidence-pack" echo "Running self-verification..." ./verify.sh --verbose || { echo "::warning::Self-verification had issues (may be expected if artifacts not fully present)" } - name: Create archives run: | set -euo pipefail cd evidence-pack # Create tar.gz tar -czvf "stella-release-${VERSION}-evidence-pack.tgz" "stella-release-${VERSION}-evidence-pack" # Create zip zip -r "stella-release-${VERSION}-evidence-pack.zip" "stella-release-${VERSION}-evidence-pack" echo "✓ Archives created" ls -la *.tgz *.zip - name: Upload evidence pack artifacts uses: actions/upload-artifact@v4 with: name: evidence-pack-${{ env.VERSION }} path: | evidence-pack/*.tgz evidence-pack/*.zip if-no-files-found: error retention-days: 90 - name: Attach to GitHub release if: github.event_name == 'workflow_dispatch' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail gh release upload "$TAG" \ "evidence-pack/stella-release-${VERSION}-evidence-pack.tgz" \ "evidence-pack/stella-release-${VERSION}-evidence-pack.zip" \ --clobber || echo "::warning::Could not attach to release" echo "✓ Evidence pack attached to release $TAG" # ============================================================================ # Notify on completion # ============================================================================ notify: runs-on: ${{ vars.LINUX_RUNNER_LABEL || 'ubuntu-latest' }} needs: [verify-test-gates, build-evidence-pack] if: always() steps: - name: Report status run: | if [ "${{ needs.build-evidence-pack.result }}" = "success" ]; then echo "✅ Evidence pack generated successfully for version ${{ needs.verify-test-gates.outputs.release_version }}" elif [ "${{ needs.verify-test-gates.result }}" = "failure" ]; then echo "❌ Evidence pack generation blocked: test gates not passed" else echo "⚠️ Evidence pack generation failed or skipped" fi