538 lines
19 KiB
YAML
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
|