# .github/workflows/examples/stellaops-verify.yml # StellaOps Verification Gate Reusable Workflow # # This reusable workflow verifies attestations before deployment. # Use it as a gate in your CI/CD pipeline to ensure only properly # signed artifacts are deployed. # # Usage: # jobs: # verify: # uses: stella-ops/templates/.github/workflows/stellaops-verify.yml@v1 # with: # artifact-digest: sha256:abc123... # certificate-identity: 'repo:myorg/myrepo:ref:refs/heads/main' # certificate-oidc-issuer: 'https://token.actions.githubusercontent.com' # # See: docs/modules/signer/guides/keyless-signing.md name: StellaOps Verify Gate on: workflow_call: inputs: artifact-digest: description: 'SHA256 digest of artifact to verify' required: true type: string stellaops-url: description: 'StellaOps API URL' required: false type: string default: 'https://api.stella-ops.org' certificate-identity: description: 'Expected OIDC identity pattern (supports regex)' required: true type: string certificate-oidc-issuer: description: 'Expected OIDC issuer URL' required: true type: string require-rekor: description: 'Require Rekor transparency log inclusion proof' required: false type: boolean default: true strict: description: 'Fail workflow on any verification issue' required: false type: boolean default: true max-cert-age-hours: description: 'Maximum age of signing certificate in hours (0 = no limit)' required: false type: number default: 0 require-sbom: description: 'Require SBOM attestation' required: false type: boolean default: false require-verdict: description: 'Require passing policy verdict attestation' required: false type: boolean default: false cli-version: description: 'StellaOps CLI version to use' required: false type: string default: 'latest' outputs: verified: description: 'Whether all verifications passed' value: ${{ jobs.verify.outputs.verified }} attestation-count: description: 'Number of attestations found' value: ${{ jobs.verify.outputs.attestation-count }} verification-details: description: 'JSON details of verification results' value: ${{ jobs.verify.outputs.verification-details }} jobs: verify: runs-on: ubuntu-latest permissions: contents: read packages: read outputs: verified: ${{ steps.verify.outputs.verified }} attestation-count: ${{ steps.verify.outputs.attestation-count }} verification-details: ${{ steps.verify.outputs.verification-details }} steps: - name: Validate Inputs run: | if [[ ! "${{ inputs.artifact-digest }}" =~ ^sha256:[a-f0-9]{64}$ ]] && \ [[ ! "${{ inputs.artifact-digest }}" =~ ^sha512:[a-f0-9]{128}$ ]]; then echo "::error::Invalid artifact-digest format. Expected sha256:... or sha512:..." exit 1 fi if [[ -z "${{ inputs.certificate-identity }}" ]]; then echo "::error::certificate-identity is required" exit 1 fi if [[ -z "${{ inputs.certificate-oidc-issuer }}" ]]; then echo "::error::certificate-oidc-issuer is required" exit 1 fi - name: Install StellaOps CLI uses: stella-ops/setup-cli@v1 with: version: ${{ inputs.cli-version }} - name: Verify Attestation id: verify env: STELLAOPS_URL: ${{ inputs.stellaops-url }} run: | set +e # Don't exit on error - we handle it VERIFY_ARGS=( --artifact "${{ inputs.artifact-digest }}" --certificate-identity "${{ inputs.certificate-identity }}" --certificate-oidc-issuer "${{ inputs.certificate-oidc-issuer }}" --output json ) # Add optional flags if [[ "${{ inputs.require-rekor }}" == "true" ]]; then VERIFY_ARGS+=(--require-rekor) fi if [[ "${{ inputs.max-cert-age-hours }}" -gt 0 ]]; then VERIFY_ARGS+=(--max-cert-age-hours "${{ inputs.max-cert-age-hours }}") fi if [[ "${{ inputs.require-sbom }}" == "true" ]]; then VERIFY_ARGS+=(--require-sbom) fi if [[ "${{ inputs.require-verdict }}" == "true" ]]; then VERIFY_ARGS+=(--require-verdict) fi echo "::group::Verifying attestations" RESULT=$(stella attest verify "${VERIFY_ARGS[@]}" 2>&1) EXIT_CODE=$? echo "$RESULT" | jq . 2>/dev/null || echo "$RESULT" echo "::endgroup::" set -e # Parse results VERIFIED=$(echo "$RESULT" | jq -r '.valid // false') ATTESTATION_COUNT=$(echo "$RESULT" | jq -r '.attestationCount // 0') echo "verified=${VERIFIED}" >> $GITHUB_OUTPUT echo "attestation-count=${ATTESTATION_COUNT}" >> $GITHUB_OUTPUT echo "verification-details=$(echo "$RESULT" | jq -c '.')" >> $GITHUB_OUTPUT # Handle verification failure if [[ "$VERIFIED" != "true" ]]; then echo "::warning::Verification failed" # Extract and report issues ISSUES=$(echo "$RESULT" | jq -r '.issues[]? | "\(.code): \(.message)"' 2>/dev/null) if [[ -n "$ISSUES" ]]; then while IFS= read -r issue; do echo "::error::$issue" done <<< "$ISSUES" fi if [[ "${{ inputs.strict }}" == "true" ]]; then echo "::error::Verification failed in strict mode" exit 1 fi fi - name: Generate Summary if: always() run: | VERIFIED="${{ steps.verify.outputs.verified }}" if [[ "$VERIFIED" == "true" ]]; then ICON="white_check_mark" STATUS="Passed" else ICON="x" STATUS="Failed" fi cat >> $GITHUB_STEP_SUMMARY << EOF ## :${ICON}: Verification ${STATUS} | Field | Value | |-------|-------| | **Artifact** | \`${{ inputs.artifact-digest }}\` | | **Expected Identity** | \`${{ inputs.certificate-identity }}\` | | **Expected Issuer** | \`${{ inputs.certificate-oidc-issuer }}\` | | **Attestations Found** | ${{ steps.verify.outputs.attestation-count }} | | **Rekor Required** | ${{ inputs.require-rekor }} | | **Strict Mode** | ${{ inputs.strict }} | EOF # Add issues if any DETAILS='${{ steps.verify.outputs.verification-details }}' ISSUES=$(echo "$DETAILS" | jq -r '.issues[]? | "- **\(.code)**: \(.message)"' 2>/dev/null) if [[ -n "$ISSUES" ]]; then cat >> $GITHUB_STEP_SUMMARY << EOF ### Issues $ISSUES EOF fi