ci: pin golangci-lint instead of inheriting latest #113
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| # AX-compliant CI workflow for dAppCore/agent | |
| # RFC-CORE-008-AGENT-EXPERIENCE.md (Agent Experience Design Principles) | |
| # | |
| # Usage examples: | |
| # - To add a new test language: create test-{lang} job following test-go/test-php pattern | |
| # - To add a new linter: create lint-{tool} job following golangci-lint pattern | |
| # - To add SonarCloud project: update sonarcloud job args with new project key | |
| # | |
| # Core primitives applied: | |
| # - Declarative job structure (YAML workflow definition) | |
| # - Predictable job names (test-go, test-php, not test1, test2) | |
| # - Path as documentation (workflow file location signals purpose) | |
| on: | |
| push: | |
| branches: [dev, main] | |
| pull_request: | |
| branches: [dev, main] | |
| permissions: | |
| contents: read | |
| env: | |
| # AX: Explicit environment variables with descriptive names | |
| GOFLAGS: -buildvcs=false | |
| GOWORK: "off" | |
| GOPROXY: "direct" | |
| GOSUMDB: "off" | |
| jobs: | |
| # Test jobs - each language runs independently for parallel coverage uploads | |
| # AX Principle: Declarative job structure with predictable names | |
| test-go: | |
| name: Go Tests + Coverage | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: '1.26' | |
| - name: Test with coverage | |
| working-directory: go | |
| # AX: Race detector for thread-safety, atomic mode for accuracy | |
| run: go test -race -coverprofile=coverage.out -covermode=atomic -count=1 ./... | |
| - name: Upload Go coverage to Codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: go/coverage.out | |
| flags: go-unittests | |
| fail_ci_if_error: false | |
| name: go-coverage | |
| test-php: | |
| name: PHP Tests + Coverage | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: shivammathur/setup-php@v2 | |
| with: | |
| # 8.4, not 8.3: composer.lock pins symfony/clock, symfony/string, | |
| # symfony/translation, symfony/css-selector and symfony/event-dispatcher | |
| # at v8.1.x, all of which require php >=8.4.1. On 8.3 the install step | |
| # aborted with "Your lock file does not contain a compatible set of | |
| # packages" before a single test ran, so this job has been failing at | |
| # install — not at the suite — on every push since the lock was written. | |
| # | |
| # composer.json still declares php ^8.2. That floor is now untested and | |
| # unreachable with this lock; reconciling the two means either pinning | |
| # config.platform.php and resolving the tree back down, or raising the | |
| # declared minimum. Tracked separately — this change only gets the | |
| # suite running again. | |
| php-version: '8.4' | |
| # pcov, not the default none: --coverage silently produces nothing | |
| # without a coverage driver installed. | |
| coverage: pcov | |
| # composer.json is at the repo root — it moved out of php/ and these | |
| # steps kept pointing at the old location, so every `[ -f composer.json ]` | |
| # guard was false and the whole PHP job passed by doing nothing. | |
| - name: Install dependencies | |
| run: composer install --no-progress --prefer-dist --no-interaction | |
| - name: Run Pest tests with coverage | |
| # --test-directory=php/tests is required, not cosmetic: Pest looks for | |
| # its Pest.php (the TestCase binding and the createWorkspace helpers) | |
| # in the default ./tests, which in this repo holds unrelated CLI | |
| # fixtures. Without it nothing is bound and every test errors. | |
| # | |
| # continue-on-error while the suite is brought up: as of 2026-08-08 it | |
| # runs 1311 tests, 1155 passing and 156 failing, with nothing skipped or | |
| # excluded. The failures are real and tracked, not flakes — the largest | |
| # remaining groups are the Core\Mcp tools (Core\Mcp\Tools\Concerns\ | |
| # ValidatesDependencies and Core\Mcp\Dependencies\HasDependencies live in | |
| # dappcore/mcp, which is not a dependency here and collides with this | |
| # package's own Core\Mcp\ PSR-4 root), the admin Livewire view stubs, and | |
| # tests that reach a live Qdrant or Elasticsearch. Drop this flag once | |
| # the count reaches zero. | |
| continue-on-error: true | |
| run: ./vendor/bin/pest --test-directory=php/tests --coverage --coverage-clover=coverage.xml | |
| - name: Upload PHP coverage to Codecov | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: coverage.xml | |
| flags: php-unittests | |
| fail_ci_if_error: false | |
| name: php-coverage | |
| lint: | |
| name: golangci-lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: '1.26' | |
| - uses: golangci/golangci-lint-action@v9 | |
| with: | |
| # Pinned, not `latest`. "Merge on green" only means something if | |
| # green is a property of this repo rather than of the calendar: with | |
| # `latest`, a golangci-lint release that enables a linter by default | |
| # or tightens an existing one turns this gate red on a commit that | |
| # touched nothing, and it reads as "your PR broke lint" to whoever is | |
| # unlucky enough to be next. Bump this pin when we choose a new | |
| # linter; never inherit one. | |
| # | |
| # v2.12.2 is what `latest` resolved to for the run that took this | |
| # repo to 0 findings, so the pin is the version that was actually | |
| # verified, not a guess. | |
| version: v2.12.2 | |
| working-directory: go | |
| # Tests are linted, not skipped. --tests=false was hiding two things | |
| # at once: it reported 21 production symbols as unused because the | |
| # only callers are in _test.go files — every one of them a | |
| # test-injection seam like syncPull, mcpInitialize or newCoreAgent — | |
| # while never reporting the dead scaffolding actually in the test | |
| # files. Linting tests swaps 21 false positives for 28 real ones. | |
| args: --timeout=5m | |
| # Guards the one rule that has silently cost delivered features: the sibling | |
| # dappco.re/* modules are dependencies, not workspace files. | |
| # | |
| # When external/ existed, an agent would "fix" a bug by editing | |
| # external/go/... — the file was right there and looked local. The build | |
| # resolves dappco.re/go from go.mod regardless, so the edit changed nothing, | |
| # the tests still passed, and the feature shipped broken. A submodule tree, | |
| # a go.work that reaches outside this repo, or a replace directive all | |
| # recreate that trap, so all three fail the build here. | |
| no-vendored-ecosystem: | |
| name: No vendored ecosystem | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Assert dappco.re/* resolves from go.mod only | |
| run: | | |
| fail=0 | |
| for path in external .gitmodules go.work go/go.work; do | |
| if [ -e "$path" ]; then | |
| echo "::error file=$path::$path must not exist — dappco.re/* modules come from go.mod, not from a checkout in this repo" | |
| fail=1 | |
| fi | |
| done | |
| if grep -qE '^\s*replace\s+dappco\.re/' go/go.mod; then | |
| echo "::error file=go/go.mod::replace directive for a dappco.re module — release a tag upstream instead" | |
| grep -nE '^\s*replace\s+dappco\.re/' go/go.mod | |
| fail=1 | |
| fi | |
| [ "$fail" -eq 0 ] && echo "clean: no vendored ecosystem, no replace directives" | |
| exit "$fail" | |
| # Quality analysis - runs after tests pass | |
| # AX Principle: Composition - SonarCloud consumes test results from previous jobs | |
| sonarcloud: | |
| name: SonarCloud | |
| runs-on: ubuntu-latest | |
| needs: [test-go, test-php] | |
| # AX Principle: Explicit dependencies - waits for both language tests | |
| steps: | |
| - uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: '1.26' | |
| - uses: shivammathur/setup-php@v2 | |
| with: | |
| php-version: '8.3' | |
| coverage: pcov | |
| # Same correction as test-php: composer.json is at the repo root, and | |
| # Pest needs to be pointed at php/tests to find its Pest.php. | |
| - name: Install PHP dependencies | |
| run: composer install --no-progress --prefer-dist --no-interaction | |
| - name: Test Go for coverage | |
| working-directory: go | |
| run: go test -coverprofile=coverage.out -covermode=atomic -count=1 ./... | |
| - name: Test PHP for coverage | |
| continue-on-error: true | |
| run: ./vendor/bin/pest --test-directory=php/tests --coverage --coverage-clover=coverage.xml | |
| - name: SonarCloud Scan | |
| uses: SonarSource/sonarqube-scan-action@v6 | |
| env: | |
| SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} | |
| # Exclusions mirror the ignore list in codecov.yml. | |
| # | |
| # No `#` lines inside the block below: `>` is a folded scalar, so a | |
| # comment there is not a comment — it was being folded into the string | |
| # and handed to sonar-scanner as arguments. | |
| with: | |
| args: > | |
| -Dsonar.organization=dappcore | |
| -Dsonar.projectKey=dappcore_agent | |
| -Dsonar.sources=go,php | |
| -Dsonar.exclusions=**/vendor/**,**/third_party/**,**/.tmp/**,**/*_test.go,**/php/tests/**,**/php/vendor/** | |
| -Dsonar.tests=go,php | |
| -Dsonar.test.inclusions=**/*_test.go,**/*Test.php | |
| -Dsonar.go.coverage.reportPaths=go/coverage.out | |
| -Dsonar.php.coverage.reportPaths=coverage.xml | |
| # Final coverage aggregation job. | |
| # | |
| # NOTE: this job runs on its own fresh runner with no checkout and no | |
| # artifact download, so the files it names have never existed in its | |
| # workspace — it has always been a no-op that only looked like a gate | |
| # (fail_ci_if_error: false hid it). The real uploads happen in test-go and | |
| # test-php. Either wire it to actions/upload-artifact/download-artifact or | |
| # delete it; it is left here, honestly labelled, rather than silently | |
| # changing what CI reports. | |
| finalize-coverage: | |
| name: Finalize Coverage | |
| runs-on: ubuntu-latest | |
| needs: [test-go, test-php] | |
| steps: | |
| - name: Finalize Codecov upload | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: go/coverage.out,coverage.xml | |
| fail_ci_if_error: false | |
| name: combined-coverage |