Skip to content

.github/workflows/go-ci.yaml

:material-github: View on GitHub · 309 lines · live source, included at build time

name: Go CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    # Run weekly security scans and fuzzing on Mondays at 9 AM UTC
    - cron: '0 9 * * 1'
  workflow_dispatch: # allow manual fuzz/CI runs

permissions:
  contents: read
  pull-requests: write
  security-events: write

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        module:
          - jsmn-go
          - stb-image-go
          - stb-truetype-go
          - tinyxml2-go
          - cjson-go
          - cgltf-go
          - dr-wav-go
          - miniz-go
          - linenoise-go
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.23'
          cache: true

      - name: Restore Go module cache
        uses: actions/cache@v4
        with:
          path: ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Download dependencies
        working-directory: ${{ matrix.module }}
        run: go mod download

      - name: Verify dependencies
        working-directory: ${{ matrix.module }}
        run: go mod verify

      - name: Run tests
        working-directory: ${{ matrix.module }}
        run: go test -v -race -timeout 5m ./...

      - name: Run tests with coverage
        working-directory: ${{ matrix.module }}
        run: go test -coverprofile=coverage.txt -covermode=atomic -v ./...

      - name: Check coverage threshold
        working-directory: ${{ matrix.module }}
        run: |
          coverage=$(go tool cover -func=coverage.txt | grep total | awk '{print $3}' | sed 's/%//')
          echo "Coverage: $coverage%"
          if (( $(echo "$coverage < 70.0" | bc -l) )); then
            echo "❌ Coverage $coverage% is below 70% threshold"
            exit 1
          else
            echo "✅ Coverage $coverage% meets threshold"
          fi

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: ./${{ matrix.module }}/coverage.txt
          flags: ${{ matrix.module }}
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: false
          verbose: true

  lint:
    name: Lint
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        module:
          - jsmn-go
          - stb-image-go
          - stb-truetype-go
          - tinyxml2-go
          - cjson-go
          - cgltf-go
          - dr-wav-go
          - miniz-go
          - linenoise-go
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.23'
          cache: true

      - name: Install golangci-lint
        # golangci-lint v2 — must match the schema version of .golangci.yml.
        # The install script is pinned to the same tag so its CLI contract matches.
        run: |
          curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/v2.2.2/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.2.2

      - name: Run golangci-lint
        working-directory: ${{ matrix.module }}
        run: golangci-lint run --config ../.golangci.yml --timeout 5m

  security:
    name: Security Scan
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        module:
          - jsmn-go
          - stb-image-go
          - stb-truetype-go
          - tinyxml2-go
          - cjson-go
          - cgltf-go
          - dr-wav-go
          - miniz-go
          - linenoise-go
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.23'
          cache: true

      - name: Run gosec security scanner
        uses: securego/gosec@v2.21.4 # pinned (not @master); Dependabot bumps it
        with:
          args: '-fmt sarif -out results.sarif ${{ matrix.module }}/...'

      - name: Upload SARIF file
        uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: results.sarif
          category: ${{ matrix.module }}

      - name: Run govulncheck
        run: |
          go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
          cd ${{ matrix.module }}
          govulncheck ./...

  benchmark:
    name: Benchmark
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    strategy:
      matrix:
        module:
          - jsmn-go
          - stb-image-go
          - stb-truetype-go
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.23'
          cache: true

      - name: Run benchmarks
        working-directory: ${{ matrix.module }}
        run: |
          go test -bench=. -benchmem -benchtime=5s -run=^$ > new.txt
          cat new.txt

      - name: Comment PR with benchmark results
        uses: actions/github-script@v7
        if: github.event_name == 'pull_request'
        with:
          script: |
            const fs = require('fs');
            const benchmarkResults = fs.readFileSync('${{ matrix.module }}/new.txt', 'utf8');

            const body = `## Benchmark Results - ${{ matrix.module }}

            \`\`\`
            ${benchmarkResults}
            \`\`\`
            `;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: body
            });

  fuzz:
    name: Fuzz
    runs-on: ubuntu-latest
    # Fuzzing is time-consuming; run only on the weekly schedule and manual dispatch.
    if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
    strategy:
      fail-fast: false
      matrix:
        include:
          - module: jsmn-go
            target: FuzzParse
          - module: tinyxml2-go
            target: FuzzParse
          - module: dr-wav-go
            target: FuzzParse
          - module: miniz-go
            target: FuzzExtract
          - module: cgltf-go
            target: FuzzParse
          - module: cjson-go
            target: FuzzUnmarshal
          - module: stb-truetype-go
            target: FuzzLoadFont
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.23'
          cache: true

      - name: Fuzz ${{ matrix.module }} (${{ matrix.target }})
        working-directory: ${{ matrix.module }}
        run: go test -run='^$' -fuzz="^${{ matrix.target }}$" -fuzztime=120s .

      - name: Upload crash corpus on failure
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: fuzz-crash-${{ matrix.module }}
          path: ${{ matrix.module }}/testdata/fuzz/

  examples:
    name: Examples
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.23'
          cache: true

      - name: Build and run examples
        # Each example is a standalone module; `make examples` builds/vets them
        # all and runs the non-interactive demos so they cannot silently rot.
        run: make examples

  build:
    name: Build
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        module:
          - jsmn-go
          - stb-image-go
          - stb-truetype-go
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.23'
          cache: true

      - name: Build
        working-directory: ${{ matrix.module }}
        run: go build -v ./...

      - name: Test build on ${{ matrix.os }}
        working-directory: ${{ matrix.module }}
        run: go test -v -short ./...