Files
git.stella-ops.org/.gitea/workflows/release-evidence-pack.yml
2026-01-28 02:30:48 +02:00

538 lines
19 KiB
YAML

# .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" <<EOF
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "$(basename "$artifact")",
"digest": {
"sha256": "$ARTIFACT_SHA256"
}
}
],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://stella-ops.io/ReleaseBuilder/v1",
"externalParameters": {
"version": "$VERSION",
"target": "$base"
},
"internalParameters": {},
"resolvedDependencies": [
{
"uri": "git+https://git.stella-ops.org/stella-ops.org/git.stella-ops.org@$TAG",
"digest": {
"gitCommit": "$GIT_SHA"
}
}
]
},
"runDetails": {
"builder": {
"id": "https://ci.stella-ops.org/builder/v1",
"version": {
"ci": "${{ github.run_id }}"
}
},
"metadata": {
"invocationId": "${{ github.run_id }}/${{ github.run_attempt }}",
"startedOn": "$BUILD_TIME",
"finishedOn": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
},
"byproducts": []
}
}
}
EOF
# Sign provenance
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 "provenance/${base}.slsa.intoto.jsonl.sig" \
--output-certificate "provenance/${base}.slsa.intoto.jsonl.cert" \
"provenance/${base}.slsa.intoto.jsonl"
done
echo "✓ SLSA provenance generated and signed"
- name: Collect Rekor proofs
if: github.event.inputs.include_rekor_proofs != 'false'
run: |
set -euo pipefail
mkdir -p rekor-proofs/log-entries/
# Collect Rekor entries for signed artifacts
for sig in artifacts/*.sig checksums/*.sig sbom/*.sig provenance/*.sig; do
[ -f "$sig" ] || continue
artifact="${sig%.sig}"
[ -f "$artifact" ] || continue
echo "Looking up Rekor entry for: $artifact"
# Search Rekor for this artifact
ENTRY=$(rekor-cli search --artifact "$artifact" 2>/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" <<EOF
{
"bundleFormatVersion": "1.0.0",
"releaseVersion": "$VERSION",
"createdAt": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"sourceCommit": "${{ github.sha }}",
"sourceDateEpoch": ${{ steps.epoch.outputs.epoch }},
"signingKeyFingerprint": "${{ steps.key-fingerprint.outputs.fingerprint }}"
}
EOF
# Copy verification scripts from templates
cp src/Attestor/__Libraries/StellaOps.Attestor.EvidencePack/Templates/verify.sh.template "$PACK_DIR/verify.sh"
cp src/Attestor/__Libraries/StellaOps.Attestor.EvidencePack/Templates/verify.ps1.template "$PACK_DIR/verify.ps1"
chmod +x "$PACK_DIR/verify.sh"
# Generate VERIFY.md
sed -e "s/{{VERSION}}/$VERSION/g" \
-e "s/{{SOURCE_COMMIT}}/${{ github.sha }}/g" \
-e "s/{{SOURCE_DATE_EPOCH}}/${{ steps.epoch.outputs.epoch }}/g" \
-e "s/{{KEY_FINGERPRINT}}/${{ steps.key-fingerprint.outputs.fingerprint }}/g" \
-e "s/{{TIMESTAMP}}/$(date -u +"%Y-%m-%dT%H:%M:%SZ")/g" \
-e "s/{{BUNDLE_VERSION}}/1.0.0/g" \
-e "s/{{REKOR_LOG_ID}}/sigstore/g" \
-e "s/{{REKOR_ENTRIES}}/See rekor-proofs\/ directory/g" \
src/Attestor/__Libraries/StellaOps.Attestor.EvidencePack/Templates/VERIFY.md.template \
> "$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