← back to Cli Printing Press

.github/workflows/lint.yml

369 lines

name: CI

on:
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  lint:
    name: go-lint
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Check whether lint validations are needed
        id: changes
        shell: bash
        run: |
          set -euo pipefail

          git fetch --no-tags --prune origin "${{ github.base_ref }}"
          mapfile -t changed < <(git diff --name-only --diff-filter=ACMR "origin/${{ github.base_ref }}...HEAD")

          echo "Changed files:"
          printf '  %s\n' "${changed[@]}"

          needs_lint=false
          needs_skill_docs=false
          for file in "${changed[@]}"; do
            case "$file" in
              *.go|go.mod|go.sum|.golangci.yml|.github/workflows/lint.yml)
                needs_lint=true
                ;;
            esac

            case "$file" in
              skills/*|docs/SKILLS.md|.github/scripts/validate-skill-docs.sh|.github/workflows/lint.yml)
                needs_skill_docs=true
                ;;
            esac

            if [[ "$needs_lint" == "true" && "$needs_skill_docs" == "true" ]]; then
              break
            fi
          done

          echo "needs_lint=$needs_lint" >> "$GITHUB_OUTPUT"
          echo "needs_skill_docs=$needs_skill_docs" >> "$GITHUB_OUTPUT"

      - name: Skip Go lint
        if: steps.changes.outputs.needs_lint != 'true'
        run: |
          echo "Skipping golangci-lint: no Go, module, lint config, or lint workflow files changed."

      - name: Skip skill docs validation
        if: steps.changes.outputs.needs_skill_docs != 'true'
        run: |
          echo "Skipping skill docs validation: no skill docs, skill guidance, or skill-doc workflow files changed."

      - name: Validate skill docs
        if: steps.changes.outputs.needs_skill_docs == 'true'
        run: bash .github/scripts/validate-skill-docs.sh

      # setup-go's bundled cache restores both GOMODCACHE and GOCACHE as one
      # ~1GB tarball; on this repo that adds ~75s of extract time per job.
      # The lint job sees little payoff from a warm GOCACHE (golangci-lint
      # has its own 1 MB analyzer cache that's already hit cleanly), so we
      # skip the bundled cache and restore only the module cache manually.
      - uses: actions/setup-go@v6
        if: steps.changes.outputs.needs_lint == 'true'
        with:
          go-version-file: go.mod
          cache: false

      - name: Cache Go modules
        if: steps.changes.outputs.needs_lint == 'true'
        uses: actions/cache@v4
        with:
          path: ~/go/pkg/mod
          key: lint-gomodcache-${{ runner.os }}-${{ hashFiles('go.sum') }}
          restore-keys: |
            lint-gomodcache-${{ runner.os }}-

      - uses: golangci/golangci-lint-action@v9
        if: steps.changes.outputs.needs_lint == 'true'
        with:
          version: v2.11.4
          verify: false

  test-changes:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      needs_tests: ${{ steps.changes.outputs.needs_tests }}
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Check whether Go tests are needed
        id: changes
        shell: bash
        run: |
          set -euo pipefail

          git fetch --no-tags --prune origin "${{ github.base_ref }}"
          mapfile -t changed < <(git diff --name-only --diff-filter=ACMR "origin/${{ github.base_ref }}...HEAD")

          echo "Changed files:"
          printf '  %s\n' "${changed[@]}"

          needs_tests=false
          for file in "${changed[@]}"; do
            case "$file" in
              .github/workflows/lint.yml|go.mod|go.sum|cmd/*|catalog/*|internal/*|scripts/*|testdata/*)
                needs_tests=true
                break
                ;;
            esac
          done

          echo "needs_tests=$needs_tests" >> "$GITHUB_OUTPUT"

  test-shard:
    name: test (${{ matrix.shard }})
    runs-on: ubuntu-latest
    needs: test-changes
    if: needs.test-changes.outputs.needs_tests == 'true'
    timeout-minutes: 10
    strategy:
      fail-fast: false
      matrix:
        include:
          - shard: generator
          - shard: pipeline
          - shard: cli
          - shard: rest
    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-go@v6
        with:
          go-version-file: go.mod
          cache: true
          cache-dependency-path: go.sum

      - name: Run Go tests
        shell: bash
        run: |
          set -o pipefail

          case "${{ matrix.shard }}" in
            generator)
              packages=(./internal/generator)
              ;;
            pipeline)
              packages=(./internal/pipeline)
              ;;
            cli)
              packages=(./internal/cli)
              ;;
            rest)
              mapfile -t packages < <(go list ./... | grep -Ev '/internal/(generator|pipeline|cli)$')
              ;;
            *)
              echo "unknown test shard: ${{ matrix.shard }}" >&2
              exit 1
              ;;
          esac

          printf 'Testing packages:\n'
          printf '  %s\n' "${packages[@]}"

          go test -short -json -timeout 12m "${packages[@]}" 2>&1 | tee /tmp/go-test.json
          status=${PIPESTATUS[0]}

          python3 - /tmp/go-test.json <<'PY'
          import json
          import sys

          packages = {}
          tests = 0
          with open(sys.argv[1]) as events:
            for line in events:
              try:
                  event = json.loads(line)
              except Exception:
                  continue
              if event.get("Action") == "pass" and event.get("Test"):
                  tests += 1
              if event.get("Action") in {"pass", "fail"} and event.get("Test") is None:
                  packages[event["Package"]] = event.get("Elapsed", 0)

          print("Slowest Go packages:")
          for pkg, elapsed in sorted(packages.items(), key=lambda item: -item[1])[:15]:
              print(f"{elapsed:6.1f}s {pkg}")
          print(f"test_pass_events {tests}")
          PY

          exit "$status"

  test:
    runs-on: ubuntu-latest
    needs:
      - test-changes
      - test-shard
    if: ${{ always() }}
    timeout-minutes: 5
    steps:
      - name: Skip Go tests
        if: needs.test-changes.outputs.needs_tests != 'true'
        run: |
          echo "Skipping Go tests: no Go, module, CI workflow, catalog, script, or testdata changes."

      - name: Check test shards
        if: needs.test-changes.outputs.needs_tests == 'true'
        shell: bash
        run: |
          if [[ "${{ needs.test-shard.result }}" != "success" ]]; then
            echo "A test shard failed or was cancelled."
            exit 1
          fi

          echo "All test shards passed."

  generated-test-changes:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      needs_full: ${{ steps.changes.outputs.needs_full }}
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - name: Check whether generated compile tests are needed
        id: changes
        shell: bash
        run: |
          set -euo pipefail

          git fetch --no-tags --prune origin "${{ github.base_ref }}"
          mapfile -t changed < <(git diff --name-only --diff-filter=ACMR "origin/${{ github.base_ref }}...HEAD")

          echo "Changed files:"
          printf '  %s\n' "${changed[@]}"

          needs_full=false
          for file in "${changed[@]}"; do
            case "$file" in
              .github/workflows/lint.yml|go.mod|go.sum|cmd/*|catalog/*|internal/cli/*|internal/generator/*|internal/openapi/*|internal/pipeline/*|internal/spec/*|testdata/*)
                needs_full=true
                break
                ;;
            esac
          done

          echo "needs_full=$needs_full" >> "$GITHUB_OUTPUT"

  generated-test-shard:
    name: generated-test (${{ matrix.shard }})
    runs-on: ubuntu-latest
    needs: generated-test-changes
    if: needs.generated-test-changes.outputs.needs_full == 'true'
    timeout-minutes: 15
    strategy:
      fail-fast: false
      matrix:
        include:
          - shard: h3
            packages: ./internal/generator
            pattern: ^(TestGenerateBrowserChromeH3Transport)$
          - shard: generator
            packages: ./internal/generator
            pattern: ^(TestGenerateProjectsCompile|TestGenerateCliutilPackage|TestGenerateFreshnessHelperEmitted|TestGenerateShareEmittedWhenEnabled|TestGenerateAgentContextCommand|TestGenerateHTMLExtractionEndpoint|TestGenerateHTMLExtractionEmbeddedJSONMode|TestGenerateHTMLExtractionPerModeGating|TestGenerateStoreWithBatchResourceDoesNotDuplicateUpsertBatch|TestGenerateStoreUpsertBatchDispatchesToTypedTable|TestGenerateStoreBackfillsIndexedColumnsOnUpgrade|TestGenerateStoreQuotesNumericTableAndDerivedIdentifiers|TestGeneratedOutput_PromotedCommandCompiles|TestGeneratedHelpers_AuthWithKeyURL_Compiles|TestGenerate_CookieAuthUsesBrowserTemplate|TestGenerate_ComposedAuthUsesBrowserTemplate|TestJSONStringParamRejectsInvalidValueBeforeClient|TestGenerateGraphQLBFFUsesSemanticCommandSurface|TestGenerateWhichFallsBackToCommandTree|TestGenerateDependentSyncCompiles|TestGenerateDependentSyncReservedWordCompiles|TestGeneratedSyncTreatsAccessDeniedAsWarning|TestGenerateGraphQLCompiles|TestGenerateMCPCodeOrchestrationEmitsSearchExecute|TestGenerateMCPIntentsEmittedWhenDeclared|TestGenerateMCPEndpointToolsHiddenSuppressesEndpointTools|TestGenerateMCPMainRemoteRuntime|TestGenerateMCPMainRemoteCompiles)$
          - shard: openapi
            packages: ./internal/openapi
            pattern: ^(TestGenerateFromOpenAPICompiles)$
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - uses: actions/setup-go@v6
        with:
          go-version-file: go.mod
          cache: true
          cache-dependency-path: go.sum

      - name: Run generated compile tests
        shell: bash
        run: |
          set -o pipefail

          read -r -a packages <<< "${{ matrix.packages }}"

          printf 'Testing packages:\n'
          printf '  %s\n' "${packages[@]}"
          echo 'Test pattern: ${{ matrix.pattern }}'

          go test -json -timeout 12m -run '${{ matrix.pattern }}' "${packages[@]}" 2>&1 | tee /tmp/go-test-generated.json
          status=${PIPESTATUS[0]}

          python3 - /tmp/go-test-generated.json <<'PY'
          import json
          import sys

          packages = {}
          tests = 0
          with open(sys.argv[1]) as events:
            for line in events:
              try:
                  event = json.loads(line)
              except Exception:
                  continue
              if event.get("Action") == "pass" and event.get("Test"):
                  tests += 1
              if event.get("Action") in {"pass", "fail"} and event.get("Test") is None:
                  packages[event["Package"]] = event.get("Elapsed", 0)

          print("Slowest Go packages:")
          for pkg, elapsed in sorted(packages.items(), key=lambda item: -item[1])[:15]:
              print(f"{elapsed:6.1f}s {pkg}")
          print(f"test_pass_events {tests}")
          PY

          exit "$status"

  generated-test:
    runs-on: ubuntu-latest
    needs:
      - generated-test-changes
      - generated-test-shard
    if: ${{ always() }}
    timeout-minutes: 5
    steps:
      - name: Skip generated compile tests
        if: needs.generated-test-changes.outputs.needs_full != 'true'
        run: |
          echo "Skipping generated compile tests: no generator, parser, pipeline, CLI, module, workflow, catalog, or testdata changes."

      - name: Check generated compile shards
        if: needs.generated-test-changes.outputs.needs_full == 'true'
        shell: bash
        run: |
          if [[ "${{ needs.generated-test-shard.result }}" != "success" ]]; then
            echo "A generated compile shard failed or was cancelled."
            exit 1
          fi

          echo "All generated compile shards passed."

  skill-docs:
    name: skill-docs
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - name: Compatibility check
        run: |
          echo "Skill docs validation runs inside go-lint."