Documentation

GitHub Action Workflow Example

Complete CI/CD automation for automated evidence collection using GitHub Actions.

Use Case

Perfect for:

  • Automated monthly/quarterly collections
  • Multi-environment deployments (dev, staging, production)
  • Scheduled compliance evidence gathering
  • Integration with existing CI/CD pipelines

Coverage: Fully automated evidence collection with notifications and artifact storage


What This Automates

TaskAutomation
Evidence collectionScheduled monthly runs
Credential managementGitHub Secrets integration
Bundle verificationAutomated integrity checks
Artifact storageGitHub Artifacts + optional S3/GCS
NotificationsSlack/email on success/failure
Multi-environmentDev, staging, production workflows

Result: Zero-touch compliance evidence collection


Prerequisites

1. GitHub Repository

  • Repository with evidence.yaml configuration
  • Admin access to configure secrets
  • GitHub Actions enabled

2. Credentials

Required secrets:

  • GITHUB_TOKEN (automatically provided by GitHub Actions)
  • EVIDENCE_SIGNING_KEY (private key contents)
  • EVIDENCE_API_KEY (optional, for platform upload)

Optional secrets (multi-source):

  • AWS_ACCESS_KEY_ID
  • AWS_SECRET_ACCESS_KEY
  • GOOGLE_APPLICATION_CREDENTIALS (JSON contents)

3. Configuration File

Repository structure:

your-repo/
├── .github/
│   └── workflows/
│       └── evidence-collection.yml
├── evidence.yaml
└── README.md

Basic Workflow - Monthly Collection

Workflow File

Create .github/workflows/evidence-collection.yml:

name: Evidence Collection

on:
  # Run on first day of each month at 2 AM UTC
  schedule:
    - cron: '0 2 1 * *'

  # Allow manual triggering
  workflow_dispatch:

jobs:
  collect:
    name: Collect Evidence
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install evidence CLI
        run: npm install -g @evidence-oss/cli

      - name: Setup signing key
        run: |
          mkdir -p ~/.evidence/keys
          echo '${{ secrets.EVIDENCE_SIGNING_KEY }}' > ~/.evidence/keys/private.pem
          chmod 600 ~/.evidence/keys/private.pem

      - name: Collect evidence
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: evidence collect

      - name: Verify bundle
        run: evidence verify evidence-bundles/evidence-bundle-*.tar.gz

      - name: Upload bundle artifact
        uses: actions/upload-artifact@v4
        with:
          name: evidence-bundle-${{ github.run_number }}
          path: evidence-bundles/*.tar.gz
          retention-days: 90

Configuration

evidence.yaml:

framework: soc2_type1

controls:
  - CC6.1
  - CC6.6
  - CC7.2

sources:
  github:
    mode: token
    token_env: GITHUB_TOKEN
    org: your-org
    repos: '*'

bundle:
  signing:
    private_key_path: ~/.evidence/keys/private.pem
  output_path: ./evidence-bundles

Setup Secrets

1. Generate signing key locally:

evidence init --generate-keys
cat ~/.evidence/keys/private.pem

2. Add to GitHub Secrets:

  1. Go to repository → Settings → Secrets and variables → Actions
  2. Click "New repository secret"
  3. Name: EVIDENCE_SIGNING_KEY
  4. Value: Paste entire contents of private.pem
  5. Click "Add secret"

Expected Output

Successful run:

Run evidence collect
Validating configuration...
  ✓ Configuration valid (1 source)
  ✓ Signing key found

Validating credentials...
  ✓ GitHub token valid (scopes: repo, read:org)

Collecting from GitHub...
  → Fetching organization settings
  → Discovering repositories (23 found)
  → Fetching branch protection (23 repos)
  → Fetching CODEOWNERS (19 repos)
  ✓ GitHub: 45 artifacts collected

Creating bundle...
  → Generating manifest
  → Computing checksums (45 files)
  → Signing bundle
  ✓ Bundle created: evidence-bundle-20260110-020145.tar.gz

✓ Collection complete

Bundle ID: evidence-bundle-20260110-020145
Size: 18.3 KB
Artifacts: 45
Duration: 8.2 seconds

Common Workflow - Multi-Source with Upload

Workflow File

name: Evidence Collection (Production)

on:
  schedule:
    - cron: '0 2 1 * *'  # Monthly
  workflow_dispatch:

jobs:
  collect:
    name: Collect and Upload Evidence
    runs-on: ubuntu-latest

    permissions:
      contents: read
      id-token: write  # For OIDC if using AWS assume role

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install evidence CLI
        run: npm install -g @evidence-oss/cli

      - name: Setup signing key
        run: |
          mkdir -p ~/.evidence/keys
          echo '${{ secrets.EVIDENCE_SIGNING_KEY }}' > ~/.evidence/keys/private.pem
          chmod 600 ~/.evidence/keys/private.pem

      - name: Setup Google credentials
        run: |
          echo '${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}' > /tmp/google-sa.json
          chmod 600 /tmp/google-sa.json

      - name: Collect evidence
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: us-east-1
          GOOGLE_APPLICATION_CREDENTIALS: /tmp/google-sa.json
        run: evidence collect

      - name: Verify bundle integrity
        run: evidence verify evidence-bundles/evidence-bundle-*.tar.gz

      - name: Upload to evidence platform
        env:
          EVIDENCE_API_KEY: ${{ secrets.EVIDENCE_API_KEY }}
        run: evidence upload evidence-bundles/evidence-bundle-*.tar.gz

      - name: Upload to GitHub Artifacts (backup)
        uses: actions/upload-artifact@v4
        with:
          name: evidence-bundle-${{ github.run_number }}
          path: evidence-bundles/*.tar.gz
          retention-days: 180

      - name: Cleanup credentials
        if: always()
        run: |
          rm -f /tmp/google-sa.json
          rm -f ~/.evidence/keys/private.pem

Configuration

evidence.yaml:

framework: soc2_type1

controls:
  - CC6.1
  - CC6.6
  - CC7.2

sources:
  github:
    mode: token
    token_env: GITHUB_TOKEN
    org: acme
    repos: '*'
    branch: main

  aws:
    mode: env
    region: us-east-1
    cloudtrail:
      trails:
        - production-audit-trail
    log_groups:
      - /aws/lambda/production-*

  google_workspace:
    mode: service_account
    credentials_env: GOOGLE_APPLICATION_CREDENTIALS
    customer_id: C01234567
    admin_email: evidence-admin@acme.com
    domains:
      - acme.com

bundle:
  signing:
    private_key_path: ~/.evidence/keys/private.pem
  max_size_mb: 100
  naming_pattern: '{org}-{framework}-{timestamp}'

upload:
  enabled: true
  retention_days: 730
  tags:
    environment: production
    automation: github-actions
  verify_before_upload: true

Setup All Secrets

Required secrets:

EVIDENCE_SIGNING_KEY            # Private key contents
EVIDENCE_API_KEY                # API key from evidence platform
AWS_ACCESS_KEY_ID               # AWS access key
AWS_SECRET_ACCESS_KEY           # AWS secret key
GOOGLE_APPLICATION_CREDENTIALS  # Google service account JSON

Add each via: Repository → Settings → Secrets and variables → Actions → New repository secret


Advanced Workflow - Multi-Environment

Workflow File

name: Evidence Collection (All Environments)

on:
  schedule:
    # Development: Weekly on Monday 2 AM
    - cron: '0 2 * * 1'
    # Production: Monthly on 1st at 2 AM
    - cron: '0 2 1 * *'

  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment to collect from'
        required: true
        type: choice
        options:
          - development
          - staging
          - production

jobs:
  determine-environment:
    name: Determine Environment
    runs-on: ubuntu-latest
    outputs:
      environment: ${{ steps.set-env.outputs.environment }}

    steps:
      - name: Set environment based on schedule or input
        id: set-env
        run: |
          if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
            echo "environment=${{ inputs.environment }}" >> $GITHUB_OUTPUT
          elif [ "$(date +%d)" == "01" ]; then
            echo "environment=production" >> $GITHUB_OUTPUT
          else
            echo "environment=development" >> $GITHUB_OUTPUT
          fi

  collect:
    name: Collect Evidence - ${{ needs.determine-environment.outputs.environment }}
    runs-on: ubuntu-latest
    needs: determine-environment
    environment: ${{ needs.determine-environment.outputs.environment }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install evidence CLI
        run: npm install -g @evidence-oss/cli

      - name: Setup signing key
        run: |
          mkdir -p ~/.evidence/keys
          echo '${{ secrets.EVIDENCE_SIGNING_KEY }}' > ~/.evidence/keys/private.pem
          chmod 600 ~/.evidence/keys/private.pem

      - name: Setup Google credentials
        run: |
          echo '${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}' > /tmp/google-sa.json
          chmod 600 /tmp/google-sa.json

      - name: Select configuration file
        run: |
          cp evidence.${{ needs.determine-environment.outputs.environment }}.yaml evidence.yaml

      - name: Collect evidence
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_REGION: ${{ vars.AWS_REGION }}
          GOOGLE_APPLICATION_CREDENTIALS: /tmp/google-sa.json
        run: evidence collect

      - name: Verify bundle
        run: evidence verify evidence-bundles/evidence-bundle-*.tar.gz

      - name: Upload to evidence platform
        env:
          EVIDENCE_API_KEY: ${{ secrets.EVIDENCE_API_KEY }}
        run: evidence upload evidence-bundles/evidence-bundle-*.tar.gz

      - name: Upload to S3 (long-term storage)
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          BUNDLE_FILE=$(ls evidence-bundles/evidence-bundle-*.tar.gz)
          BUNDLE_NAME=$(basename $BUNDLE_FILE)
          aws s3 cp $BUNDLE_FILE \
            s3://acme-evidence-bundles/${{ needs.determine-environment.outputs.environment }}/$BUNDLE_NAME \
            --region us-east-1

      - name: Send Slack notification (success)
        if: success()
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "✅ Evidence collection succeeded",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Evidence Collection Succeeded*\n\n*Environment:* ${{ needs.determine-environment.outputs.environment }}\n*Workflow:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>\n*Time:* $(date -u +%Y-%m-%dT%H:%M:%SZ)"
                  }
                }
              ]
            }

      - name: Send Slack notification (failure)
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "❌ Evidence collection failed",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Evidence Collection Failed*\n\n*Environment:* ${{ needs.determine-environment.outputs.environment }}\n*Workflow:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>\n*Time:* $(date -u +%Y-%m-%dT%H:%M:%SZ)\n\n@channel Please investigate"
                  }
                }
              ]
            }

      - name: Cleanup credentials
        if: always()
        run: |
          rm -f /tmp/google-sa.json
          rm -f ~/.evidence/keys/private.pem

Per-Environment Configurations

evidence.development.yaml:

framework: soc2_type1
controls: [CC6.1]
sources:
  github:
    mode: token
    token_env: GITHUB_TOKEN
    org: acme
    repos:
      - acme/backend-dev
bundle:
  signing:
    private_key_path: ~/.evidence/keys/private.pem
  naming_pattern: 'dev-{timestamp}'
upload:
  enabled: true
  retention_days: 90
  tags:
    environment: development

evidence.staging.yaml:

framework: soc2_type1
controls: [CC6.1, CC6.6]
sources:
  github:
    mode: token
    token_env: GITHUB_TOKEN
    org: acme
    repos:
      - acme/backend-staging
  aws:
    mode: env
    region: us-east-1
    cloudtrail:
      trails: [staging-audit-trail]
bundle:
  signing:
    private_key_path: ~/.evidence/keys/private.pem
  naming_pattern: 'staging-{timestamp}'
upload:
  enabled: true
  retention_days: 180
  tags:
    environment: staging

evidence.production.yaml:

framework: soc2_type1
controls: [CC6.1, CC6.6, CC7.2]
sources:
  github:
    mode: token
    token_env: GITHUB_TOKEN
    org: acme
    repos: '*'
  aws:
    mode: env
    region: us-east-1
    cloudtrail:
      trails: [production-audit-trail]
    log_groups: [/aws/lambda/production-*]
  google_workspace:
    mode: service_account
    credentials_env: GOOGLE_APPLICATION_CREDENTIALS
    customer_id: C01234567
    admin_email: evidence-admin@acme.com
bundle:
  signing:
    private_key_path: ~/.evidence/keys/private.pem
  max_size_mb: 100
  naming_pattern: 'prod-{timestamp}'
upload:
  enabled: true
  retention_days: 730
  tags:
    environment: production

Notifications

Slack Integration

Setup:

  1. Create Slack webhook: Slack → Apps → Incoming Webhooks
  2. Add to GitHub Secrets: SLACK_WEBHOOK_URL

Basic notification step:

- name: Notify Slack
  if: always()
  uses: slackapi/slack-github-action@v1
  with:
    webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
    payload: |
      {
        "text": "${{ job.status == 'success' && '✅' || '❌' }} Evidence collection ${{ job.status }}",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Evidence Collection ${{ job.status }}*\n\n*Repository:* ${{ github.repository }}\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"
            }
          }
        ]
      }

Email Notification

Using GitHub Actions email:

- name: Send email notification
  if: failure()
  uses: dawidd6/action-send-mail@v3
  with:
    server_address: smtp.gmail.com
    server_port: 465
    username: ${{ secrets.EMAIL_USERNAME }}
    password: ${{ secrets.EMAIL_PASSWORD }}
    subject: Evidence Collection Failed - ${{ github.repository }}
    to: security@acme.com
    from: github-actions@acme.com
    body: |
      Evidence collection failed in ${{ github.repository }}.

      Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

      Please investigate immediately.

Storage Options

GitHub Artifacts (Built-in)

- name: Upload bundle to GitHub Artifacts
  uses: actions/upload-artifact@v4
  with:
    name: evidence-bundle-${{ github.run_number }}
    path: evidence-bundles/*.tar.gz
    retention-days: 90  # Max 90 days for free tier
    compression-level: 9

Retrieval:

# Via GitHub CLI
gh run download <run-id> -n evidence-bundle-<run-number>

# Via web UI
Repository Actions Workflow run Artifacts section

AWS S3 Storage

- name: Upload to S3
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
    AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
  run: |
    BUNDLE_FILE=$(ls evidence-bundles/evidence-bundle-*.tar.gz)
    BUNDLE_NAME=$(basename $BUNDLE_FILE)

    aws s3 cp $BUNDLE_FILE \
      s3://acme-evidence-bundles/$(date +%Y)/$(date +%m)/$BUNDLE_NAME \
      --region us-east-1 \
      --storage-class STANDARD_IA \
      --server-side-encryption AES256

S3 bucket configuration:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:user/github-actions"
      },
      "Action": [
        "s3:PutObject",
        "s3:PutObjectAcl"
      ],
      "Resource": "arn:aws:s3:::acme-evidence-bundles/*"
    }
  ]
}

Google Cloud Storage

- name: Upload to GCS
  run: |
    echo '${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}' > /tmp/gcp-key.json
    gcloud auth activate-service-account --key-file=/tmp/gcp-key.json

    BUNDLE_FILE=$(ls evidence-bundles/evidence-bundle-*.tar.gz)
    BUNDLE_NAME=$(basename $BUNDLE_FILE)

    gsutil cp $BUNDLE_FILE \
      gs://acme-evidence-bundles/$(date +%Y)/$(date +%m)/$BUNDLE_NAME

    rm -f /tmp/gcp-key.json

Error Handling

Retry on Failure

- name: Collect evidence (with retry)
  uses: nick-invision/retry@v2
  with:
    timeout_minutes: 30
    max_attempts: 3
    retry_wait_seconds: 300
    command: evidence collect
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Continue on Error (Non-Critical Steps)

- name: Upload to secondary storage
  continue-on-error: true
  run: |
    # Upload to backup location
    # Failure won't stop the workflow

Conditional Steps

- name: Verify bundle
  id: verify
  run: evidence verify evidence-bundles/evidence-bundle-*.tar.gz

- name: Upload only if verified
  if: steps.verify.outcome == 'success'
  run: evidence upload evidence-bundles/evidence-bundle-*.tar.gz

Security Best Practices

1. Least Privilege Secrets

Create dedicated GitHub PAT:

  • Scopes: repo:read, read:org only
  • Expiration: 90 days
  • Regular rotation schedule

Create dedicated AWS IAM user:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iam:GetAccountPasswordPolicy",
        "cloudtrail:DescribeTrails",
        "cloudtrail:GetTrailStatus"
      ],
      "Resource": "*"
    }
  ]
}

2. Environment Isolation

Use GitHub Environments:

jobs:
  collect:
    environment: production
    # Requires approval for production runs

Configure in repository: Settings → Environments → New environment → Add protection rules


3. Credential Cleanup

Always cleanup sensitive files:

- name: Cleanup credentials
  if: always()
  run: |
    rm -f /tmp/google-sa.json
    rm -f ~/.evidence/keys/private.pem
    rm -f /tmp/gcp-key.json

4. Audit Logging

Log all collection runs:

- name: Log collection metadata
  run: |
    cat > collection-log.json <<EOF
    {
      "workflow_run_id": "${{ github.run_id }}",
      "workflow_run_number": "${{ github.run_number }}",
      "triggered_by": "${{ github.actor }}",
      "event": "${{ github.event_name }}",
      "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
      "repository": "${{ github.repository }}",
      "ref": "${{ github.ref }}"
    }
    EOF

    # Upload to audit log storage
    aws s3 cp collection-log.json \
      s3://acme-audit-logs/evidence-collections/$(date +%Y%m%d)-${{ github.run_number }}.json

Monitoring

Check Last Successful Run

Add status badge to README:

[![Evidence Collection](https://github.com/acme/backend/actions/workflows/evidence-collection.yml/badge.svg)](https://github.com/acme/backend/actions/workflows/evidence-collection.yml)

Create Monitoring Dashboard

GitHub CLI script:

#!/bin/bash
# check-evidence-status.sh

# Get last 10 workflow runs
gh run list --workflow=evidence-collection.yml --limit=10 --json status,conclusion,createdAt

# Check for recent failures
RECENT_FAILURES=$(gh run list --workflow=evidence-collection.yml --limit=5 --json conclusion --jq '[.[] | select(.conclusion=="failure")] | length')

if [ "$RECENT_FAILURES" -gt 2 ]; then
  echo "WARNING: $RECENT_FAILURES failures in last 5 runs"
  # Send alert
fi

Troubleshooting

Workflow Fails at "Collect evidence"

Check logs:

- name: Collect evidence (with debug)
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    DEBUG: evidence:*
  run: evidence collect --verbose

Common issues:

  • Missing environment variable
  • Invalid GitHub token
  • Rate limiting
  • Network timeout

Bundle Verification Fails

Debug steps:

- name: Debug bundle contents
  if: failure()
  run: |
    ls -lh evidence-bundles/
    tar -tzf evidence-bundles/evidence-bundle-*.tar.gz

- name: Verify with verbose output
  if: failure()
  run: evidence verify evidence-bundles/evidence-bundle-*.tar.gz --verbose

Upload Timeout

Increase timeout:

- name: Upload to evidence platform
  timeout-minutes: 15  # Default is 6 hours, but set reasonable limit
  env:
    EVIDENCE_API_KEY: ${{ secrets.EVIDENCE_API_KEY }}
  run: evidence upload evidence-bundles/evidence-bundle-*.tar.gz

Rate Limiting (GitHub API)

Solution: Workflow automatically pauses and retries

⚠ Rate limit approaching (98 remaining)
  Pausing collection...
  Rate limit resets at 2026-01-10T14:00:00Z (in 5 minutes)
  Waiting...

✓ Rate limit reset
  Resuming collection...

Or schedule during off-hours:

on:
  schedule:
    - cron: '0 2 * * 1'  # 2 AM UTC (low activity)

Testing Workflows

Manual Trigger

Trigger via GitHub UI:

  1. Go to Actions tab
  2. Select "evidence Collection" workflow
  3. Click "Run workflow"
  4. Select branch
  5. Click "Run workflow"

Trigger via GitHub CLI:

gh workflow run evidence-collection.yml

Test with Dry Run

Add test job:

jobs:
  test:
    name: Test Configuration
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install evidence CLI
        run: npm install -g @evidence-oss/cli

      - name: Validate configuration
        run: evidence collect --dry-run
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Complete Production Example

Repository structure:

acme-backend/
├── .github/
│   └── workflows/
│       ├── evidence-collection.yml
│       └── evidence-test.yml
├── evidence.yaml
├── evidence.development.yaml
├── evidence.staging.yaml
├── evidence.production.yaml
└── README.md

Main workflow (.github/workflows/evidence-collection.yml):

name: Evidence Collection (Production)

on:
  schedule:
    - cron: '0 2 1 * *'  # Monthly on 1st
  workflow_dispatch:

jobs:
  collect:
    name: Collect and Upload
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install evidence CLI
        run: npm install -g @evidence-oss/cli

      - name: Setup signing key
        run: |
          mkdir -p ~/.evidence/keys
          echo '${{ secrets.EVIDENCE_SIGNING_KEY }}' > ~/.evidence/keys/private.pem
          chmod 600 ~/.evidence/keys/private.pem

      - name: Setup Google credentials
        run: |
          echo '${{ secrets.GOOGLE_APPLICATION_CREDENTIALS }}' > /tmp/google-sa.json
          chmod 600 /tmp/google-sa.json

      - name: Collect evidence
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          GOOGLE_APPLICATION_CREDENTIALS: /tmp/google-sa.json
        run: evidence collect

      - name: Verify bundle
        run: evidence verify evidence-bundles/evidence-bundle-*.tar.gz

      - name: Upload to evidence platform
        env:
          EVIDENCE_API_KEY: ${{ secrets.EVIDENCE_API_KEY }}
        run: evidence upload evidence-bundles/evidence-bundle-*.tar.gz

      - name: Backup to S3
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          BUNDLE_FILE=$(ls evidence-bundles/evidence-bundle-*.tar.gz)
          aws s3 cp $BUNDLE_FILE s3://acme-evidence-bundles/production/ --region us-east-1

      - name: Upload to GitHub Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: evidence-bundle-${{ github.run_number }}
          path: evidence-bundles/*.tar.gz
          retention-days: 90

      - name: Notify Slack (success)
        if: success()
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "✅ Evidence collection succeeded",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Evidence Collection Succeeded*\n\n*Environment:* production\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"
                  }
                }
              ]
            }

      - name: Notify Slack (failure)
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
          payload: |
            {
              "text": "❌ Evidence collection failed - @channel",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Evidence Collection Failed*\n\n*Environment:* production\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>\n\nPlease investigate immediately."
                  }
                }
              ]
            }

      - name: Cleanup
        if: always()
        run: |
          rm -f /tmp/google-sa.json
          rm -f ~/.evidence/keys/private.pem

See Also