From e744c7c7e537f1fd6e14a76a02c8e8b2d84c4cb6 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 12 May 2026 07:04:28 +0200 Subject: [PATCH 1/4] ci: move Go test pipeline from CircleCI to GitHub Actions Replace .circleci/config.yml with a go-test.yaml workflow that runs go test -race and uploads coverage to Qlty Cloud via the official qlty-action with OIDC. Update README build-status badges. Closes #4249 Assisted by AI --- .circleci/config.yml | 60 ---------------------------------- .github/workflows/go-test.yaml | 39 ++++++++++++++++++++++ README.rst | 4 +-- README_template.rst | 4 +-- 4 files changed, 43 insertions(+), 64 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/go-test.yaml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index e0d1df4b3..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,60 +0,0 @@ -version: 2.1 - -workflows: - tests: - jobs: - - build - - report: - requires: - - build -orbs: - qlty: qltysh/qlty-orb@0.0 -jobs: - build: - parallelism: 8 - docker: - - image: cimg/go:1.26 - steps: - - checkout - - - restore_cache: - keys: - - go-mod-v2-{{ checksum "go.sum" }} - - - run: go get -t -v ./... - - - save_cache: - key: go-mod-v2-{{ checksum "go.sum" }} - paths: - - "/go/pkg/mod" - - - run: mkdir cov - - run: go test -p 1 -v $(go list ./... | circleci tests split) -race -coverprofile=cov/c_raw_$CIRCLE_NODE_INDEX.out - - persist_to_workspace: - root: . - paths: - - cov - - report: - docker: - - image: cimg/go:1.26 - steps: - - checkout - - attach_workspace: - at: . - - run: - name: Merge test files - command: | - cat "cov/c_raw_0.out" >> c_raw.out - for f in $(seq 1 7) - do - tail -n +2 "cov/c_raw_$f.out" >> c_raw.out - done - - run: - name: Remove test, mock and generated code - command: | - cat c_raw.out | grep -v generated | grep -v mock | grep -v test > c.out - # Qlty Coverage Orb - - qlty/coverage_publish: - files: c.out - format: coverprofile diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml new file mode 100644 index 000000000..52aaefbe1 --- /dev/null +++ b/.github/workflows/go-test.yaml @@ -0,0 +1,39 @@ +name: 'Go test' + +on: + push: + branches: + - 'master' + - 'V*' + pull_request: + branches: + - 'master' + - 'V*' + +jobs: + test: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + + - name: Test + run: go test -p 1 ./... -race -coverprofile=c_raw.out + + - name: Filter coverage + run: grep -v generated c_raw.out | grep -v mock | grep -v test > c.out + + - name: Upload coverage to Qlty + uses: qltysh/qlty-action/coverage@v2 + with: + oidc: true + files: c.out + format: coverprofile diff --git a/README.rst b/README.rst index dfb0847da..b01864c2d 100644 --- a/README.rst +++ b/README.rst @@ -7,8 +7,8 @@ It contains all the necessary components for secure discovery and authorization. See the `documentation `_ for how to set up, integrate and use the Nuts node. -.. image:: https://circleci.com/gh/nuts-foundation/nuts-node.svg?style=svg - :target: https://circleci.com/gh/nuts-foundation/nuts-node +.. image:: https://github.com/nuts-foundation/nuts-node/actions/workflows/go-test.yaml/badge.svg + :target: https://github.com/nuts-foundation/nuts-node/actions/workflows/go-test.yaml :alt: Build Status .. image:: https://readthedocs.org/projects/nuts-node/badge/?version=latest diff --git a/README_template.rst b/README_template.rst index 1ff1dcc22..b2f71317c 100644 --- a/README_template.rst +++ b/README_template.rst @@ -7,8 +7,8 @@ It contains all the necessary components for secure discovery and authorization. See the `documentation `_ for how to set up, integrate and use the Nuts node. -.. image:: https://circleci.com/gh/nuts-foundation/nuts-node.svg?style=svg - :target: https://circleci.com/gh/nuts-foundation/nuts-node +.. image:: https://github.com/nuts-foundation/nuts-node/actions/workflows/go-test.yaml/badge.svg + :target: https://github.com/nuts-foundation/nuts-node/actions/workflows/go-test.yaml :alt: Build Status .. image:: https://readthedocs.org/projects/nuts-node/badge/?version=latest From dd84addbb1ebee414607816de34b7265a89da7a8 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 12 May 2026 09:40:03 +0200 Subject: [PATCH 2/4] ci: address Qlty/zizmor findings and fix DNS-error test mismatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Run tests inside golang:1.26.3 container so DNS errors come from glibc ("no such host"), matching the regex used by tests in crypto/storage/vault, didman/api/v1, policy and others — the GHA ubuntu-latest runner uses systemd-resolved which returns "server misbehaving". - Set persist-credentials: false on actions/checkout (zizmor: credential persistence through GitHub Actions artifacts). - Pin qltysh/qlty-action/coverage to v2.2.0 commit SHA (zizmor: unpinned action reference). Assisted by AI --- .github/workflows/go-test.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml index 52aaefbe1..4f28f3fcc 100644 --- a/.github/workflows/go-test.yaml +++ b/.github/workflows/go-test.yaml @@ -13,17 +13,18 @@ on: jobs: test: runs-on: ubuntu-latest + # Run inside a debian-based Go container so DNS resolution errors come from + # glibc ("no such host") instead of the runner's systemd-resolved + # ("server misbehaving"), which several unit tests assert against verbatim. + container: golang:1.26.3 permissions: contents: read id-token: write steps: - name: Checkout uses: actions/checkout@v6 - - - name: Set up Go - uses: actions/setup-go@v6 with: - go-version-file: 'go.mod' + persist-credentials: false - name: Test run: go test -p 1 ./... -race -coverprofile=c_raw.out @@ -32,7 +33,7 @@ jobs: run: grep -v generated c_raw.out | grep -v mock | grep -v test > c.out - name: Upload coverage to Qlty - uses: qltysh/qlty-action/coverage@v2 + uses: qltysh/qlty-action/coverage@a19242102d17e497f437d7466aa01b528537e899 # v2.2.0 with: oidc: true files: c.out From 7edfeb5ca1eeb1ac48bf026f08d936a46d2f2d15 Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 12 May 2026 11:17:48 +0200 Subject: [PATCH 3/4] fix: deterministic policy load order, drop brittle DNS-error assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - policy: switch loadFromDirectory to os.ReadDir so .json files load in lexical order. dir.Readdir(0) returned entries in filesystem-defined order, making duplicate-scope detection non-deterministic across platforms (the duplicate-detection test asserts which file is reported as the dupe, which only held on CircleCI's filesystem). - vault, didman client tests: drop assert.Regexp on DNS error strings — these matched against Go net package output ("no such host" / "Temporary failure in name resolution") and didn't verify any behavior of the code under test. The "got an error + nil result" assertions cover the actual contract. Aligns with the other "wrong address" subtests in the same didman file. - Drop the golang:1.26.3 container workaround from go-test.yaml now that the underlying brittleness is gone. Assisted by AI --- .github/workflows/go-test.yaml | 9 +++++---- crypto/storage/vault/vault_test.go | 1 - didman/api/v1/client_test.go | 6 +++--- policy/local.go | 14 +++----------- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml index 4f28f3fcc..5f5fdac9d 100644 --- a/.github/workflows/go-test.yaml +++ b/.github/workflows/go-test.yaml @@ -13,10 +13,6 @@ on: jobs: test: runs-on: ubuntu-latest - # Run inside a debian-based Go container so DNS resolution errors come from - # glibc ("no such host") instead of the runner's systemd-resolved - # ("server misbehaving"), which several unit tests assert against verbatim. - container: golang:1.26.3 permissions: contents: read id-token: write @@ -26,6 +22,11 @@ jobs: with: persist-credentials: false + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: 'go.mod' + - name: Test run: go test -p 1 ./... -race -coverprofile=c_raw.out diff --git a/crypto/storage/vault/vault_test.go b/crypto/storage/vault/vault_test.go index cf7db0ea9..57b9095d0 100644 --- a/crypto/storage/vault/vault_test.go +++ b/crypto/storage/vault/vault_test.go @@ -292,7 +292,6 @@ func TestNewVaultKVStorage(t *testing.T) { t.Run("error - wrong URL", func(t *testing.T) { storage, err := NewVaultKVStorage(Config{Address: "http://non-existing"}) require.Error(t, err) - assert.Regexp(t, `no such host|Temporary failure in name resolution`, err.Error()) assert.Nil(t, storage) }) } diff --git a/didman/api/v1/client_test.go b/didman/api/v1/client_test.go index db7404940..3520f291a 100644 --- a/didman/api/v1/client_test.go +++ b/didman/api/v1/client_test.go @@ -115,7 +115,7 @@ func TestHTTPClient_AddEndpoint(t *testing.T) { Address: "not_an_address", Timeout: time.Second}, } endpoint, err := c.AddEndpoint("abc", "type", "some-url") - assert.Regexp(t, `no such host|Temporary failure in name resolution`, err.Error()) + assert.Error(t, err) assert.Nil(t, endpoint) }) } @@ -139,7 +139,7 @@ func TestHTTPClient_DeleteEndpointsByType(t *testing.T) { Address: "not_an_address", Timeout: time.Second}, } err := c.DeleteEndpointsByType("did:nuts:123", "eOverdracht") - assert.Regexp(t, `no such host|Temporary failure in name resolution`, err.Error()) + assert.Error(t, err) }) } @@ -228,7 +228,7 @@ func TestHTTPClient_GetCompoundServices(t *testing.T) { Address: "not_an_address", Timeout: time.Second}, } res, err := c.GetCompoundServices("did:nuts:123") - assert.Regexp(t, `no such host|Temporary failure in name resolution`, err.Error()) + assert.Error(t, err) assert.Nil(t, res) }) diff --git a/policy/local.go b/policy/local.go index 81719eb7d..66e462b1e 100644 --- a/policy/local.go +++ b/policy/local.go @@ -87,22 +87,14 @@ func (b *LocalPDP) PresentationDefinitions(_ context.Context, scope string) (pe. return result, nil } -// loadFromDirectory traverses all .json files in the given directory and loads them +// loadFromDirectory traverses all .json files in the given directory and loads them. +// Entries are processed in lexical order so duplicate-scope detection is deterministic. func (b *LocalPDP) loadFromDirectory(directory string) error { - // open the directory - dir, err := os.Open(directory) + files, err := os.ReadDir(directory) if err != nil { return err } - defer dir.Close() - // read all the files in the directory - files, err := dir.Readdir(0) - if err != nil { - return err - } - - // load all the files for _, file := range files { if file.IsDir() { continue From 0281af7ea2491d7ccd996610daf6d5c53ddda55b Mon Sep 17 00:00:00 2001 From: Rein Krul Date: Tue, 12 May 2026 13:18:46 +0200 Subject: [PATCH 4/4] ci: run Go tests on every PR, not just PRs targeting master/V* Per review feedback: tests should fire on PRs to feature branches too. Assisted by AI --- .github/workflows/go-test.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/go-test.yaml b/.github/workflows/go-test.yaml index 5f5fdac9d..90a8dbb9d 100644 --- a/.github/workflows/go-test.yaml +++ b/.github/workflows/go-test.yaml @@ -6,9 +6,6 @@ on: - 'master' - 'V*' pull_request: - branches: - - 'master' - - 'V*' jobs: test: