Skip to content

GitHub Actions

Badge

Show the latest master status in your README.md:

[![CRAP analysis](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/crap.yml/badge.svg?branch=master)](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/crap.yml)

The badge reflects the workflow result on master. Make sure the workflow triggers on that branch:

on:
  push:
    branches: [master]

Fail on high CRAP scores

name: crap
on:
  push:
    branches: [main, master]
  pull_request:

jobs:
  crap:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v8
      - uses: actions/setup-go@v7
        with:
          go-version: '1.23'
          cache: true
      - name: Install go-crap
        run: curl -fsSL https://padiazg.github.io/go-crap/install.sh | sh
      - name: Run go-crap
        run: go-crap scan --fail-above --threshold 30 --exclude 'testdata/.*\.go' --exclude '\.pb\.go$'

PR annotations with JSON report

      - name: Install go-crap
        run: curl -fsSL https://padiazg.github.io/go-crap/install.sh | sh
      - name: Run go-crap with annotations
        run: go-crap scan --format github --threshold 30
      - name: Generate JSON report
        run: go-crap scan --format json > crap-report.json
      - uses: actions/upload-artifact@v8
        with:
          name: crap-report
          path: crap-report.json

Matrix builds across Go versions

name: crap
on: [push, pull_request]

jobs:
  crap:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        go-version: ['1.22', '1.23', '1.24']
    steps:
      - uses: actions/checkout@v8
      - uses: actions/setup-go@v7
        with:
          go-version: ${{ matrix.go-version }}
          cache: true
      - name: Install go-crap
        run: curl -fsSL https://padiazg.github.io/go-crap/install.sh | sh
      - name: Run go-crap
        run: go-crap scan --fail-above --threshold 30

SARIF report with code scanning

      - name: Run go-crap with SARIF output
        run: go-crap scan --format sarif --threshold 30 > report.sarif
      - name: Upload SARIF report
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: report.sarif

SARIF output is compatible with GitHub Advanced Security code scanning, Azure DevOps, and other tools that consume SARIF 2.1.0 reports.

PR comment with CRAP report

      - name: Run go-crap for PR comment
        run: go-crap scan --format pr-comment --threshold 30 --output pr-comment.md
      - name: Get PR number
        run: echo "${{ github.event.pull_request.number }}" > pr-number.txt
      - name: Upload artifacts
        uses: actions/upload-artifact@v8
        with:
          name: crap-comment
          path: |
            pr-comment.md
            pr-number.txt
          if-no-files-found: ignore

The pr-comment format generates a markdown table suitable for pull request comments, showing status symbols, CRAP scores, complexity, coverage, function names, and file locations.

The artifact is posted by a fork-safe workflow using workflow_run + marker-based upsert (the <!-- go-crap-report --> marker ensures updates in place).

Fork-safe PR comment with mutation testing

When accepting PRs from forks, the CI job's GITHUB_TOKEN is read-only and cannot post comments. The solution is a two-workflow pattern:

  1. Workflow 1 (crap.yaml) -- runs gremlins + go-crap, generates a PR comment as a markdown file, and uploads it as an artifact.
  2. Workflow 2 (post-pr-comment.yaml) -- triggered by workflow_run, runs in the base repo context with write permissions, downloads the artifact, and posts (or updates) the comment.

Workflow 1: generate the comment

name: crap
on:
  push:
    branches: [main, master]
  pull_request:

jobs:
  threshold:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v8
      - uses: actions/setup-go@v7
        with:
          go-version: '1.23'
          cache: true
      - name: Install go-crap
        run: curl -fsSL https://padiazg.github.io/go-crap/install.sh | sh
      - name: Score
        run: go-crap scan --fail-above --threshold 30

  pr-comment:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v8
      - uses: actions/setup-go@v7
        with:
          go-version: '1.23'
          cache: true
      - name: Install gremlins
        run: go install github.com/go-gremlins/gremlins/cmd/gremlins@latest
      - name: Mutation testing
        run: >-
          gremlins unleash
          --timeout-coefficient 20
          -S "l"
          --integration
          --output=mutation-report.json
      - name: Install go-crap
        run: curl -fsSL https://padiazg.github.io/go-crap/install.sh | sh
      - name: Generate PR comment
        run: >-
          go-crap scan
          --format pr-comment
          --threshold 30
          --output pr-comment.md
          --mutation-report mutation-report.json || true
      - name: Get PR number
        run: echo "${{ github.event.pull_request.number }}" > pr-number.txt
      - name: Upload artifacts
        uses: actions/upload-artifact@v8
        with:
          name: crap-comment
          path: |
            pr-comment.md
            pr-number.txt
            mutation-report.json
          if-no-files-found: ignore

The || true on the go-crap step ensures the comment artifact is still uploaded even when functions exceed the threshold.

See Recommended configuration for an explanation of the gremlins flags.

Workflow 2: post the comment

This workflow triggers after crap completes. It runs in the base repo context, so GITHUB_TOKEN has write access to pull requests -- even on fork PRs.

name: post pr comment
on:
  workflow_run:
    workflows: [crap]
    types: [completed]

permissions:
  pull-requests: write
  actions: read

jobs:
  comment:
    name: Post or update PR comment
    runs-on: ubuntu-latest
    steps:
      - name: Download PR comment artifact
        uses: actions/download-artifact@v8
        with:
          name: crap-comment
          path: .
          run-id: ${{ github.event.workflow_run.id }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
        continue-on-error: true

      - name: Post or update PR comment
        uses: actions/github-script@v9
        with:
          script: |
            const fs = require('fs');
            if (!fs.existsSync('pr-comment.md') || !fs.existsSync('pr-number.txt')) return;
            const body = fs.readFileSync('pr-comment.md', 'utf8');
            const prNumber = parseInt(fs.readFileSync('pr-number.txt', 'utf8').trim(), 10);
            if (!prNumber) return;
            const marker = '<!-- go-crap-report -->';
            const { data: comments } = await github.rest.issues.listComments({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: prNumber,
            });
            const existing = comments.find(c => c.body.startsWith(marker));
            if (existing) {
              await github.rest.issues.updateComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                comment_id: existing.id,
                body,
              });
            } else {
              await github.rest.issues.createComment({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: prNumber,
                body,
              });
            }

The <!-- go-crap-report --> marker (emitted by --format pr-comment) ensures the comment is updated in place on subsequent pushes instead of creating duplicates.

Baseline comparison with --baseline

Compare PR CRAP scores against a baseline to detect regressions. See Baseline Comparison for full documentation.

name: crap
on:
  push:
    branches: [main, master]
  pull_request:

permissions:
  contents: read

jobs:
  baseline:
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/master'
    steps:
      - uses: actions/checkout@v8
      - uses: actions/setup-go@v7
        with:
          go-version: '1.23'
          cache: true
      - name: Install go-crap
        run: curl -fsSL https://padiazg.github.io/go-crap/install.sh | sh
      - name: Generate baseline
        run: go-crap scan --format json --output crap-current.json
      - uses: actions/upload-artifact@v8
        with:
          name: crap-baseline
          path: crap-current.json

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v8
      - uses: actions/setup-go@v7
        with:
          go-version: '1.23'
          cache: true
      - run: go test -race -count=1 ./...

  crap:
    runs-on: ubuntu-latest
    needs: test
    if: always() && needs.test.result == 'success'
    permissions:
      contents: read
      actions: read
    steps:
      - uses: actions/checkout@v8
      - uses: actions/setup-go@v7
        with:
          go-version: '1.23'
          cache: true
      - name: Install go-crap
        run: curl -fsSL https://padiazg.github.io/go-crap/install.sh | sh

      - name: Find latest master baseline run
        id: get-run
        run: |
          RUN_ID=$(gh run list \
            --workflow crap.yml \
            --branch master \
            --status success \
            --json databaseId \
            --jq '.[0].databaseId' \
            --limit 1)
          echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
        env:
          GH_TOKEN: ${{ github.token }}
        continue-on-error: true

      - name: Download baseline
        uses: actions/download-artifact@v8
        with:
          name: crap-baseline
          path: baseline
          run-id: ${{ steps.get-run.outputs.run_id }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
        continue-on-error: true

      - name: Check for regressions
        if: github.event_name == 'pull_request' && hashFiles('baseline/crap-current.json') != ''
        run: go-crap scan --baseline baseline/crap-current.json --fail-regression --fail-regression-ignore-covered

      - name: Generate PR comment
        if: github.event_name == 'pull_request'
        run: |
          if [ -f baseline/crap-current.json ]; then
            go-crap scan --baseline baseline/crap-current.json --format pr-comment --threshold 30 --output pr-comment.md
          else
            go-crap scan --format pr-comment --threshold 30 --output pr-comment.md
          fi

Example output

Before fixing -- functions above the threshold with unreliable coverage from survived mutants:

PR comment before

After fixing -- all functions below the threshold, no survived mutants:

PR comment after