diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 964a6d5..2461592 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - name: Run linter uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: - version: v2.12.2 + version: v2.13.1 - name: Scan for secrets # gitleaks/gitleaks-action requires a paid license for GitHub diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..dd474e2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,68 @@ +name: CodeQL + +# Replaces CodeQL default setup. Default setup pins its own Go toolchain and +# runs the extractor with GOTOOLCHAIN=local, so it cannot build a module whose +# go directive is newer than the bundle. Driving it here lets setup-go install +# the toolchain go.mod asks for, so a Go bump never blocks code scanning. + +on: + push: + branches: + - master + pull_request: + branches: + - master + schedule: + # Weekly, Tuesday 05:44 UTC. Off the top of the hour and off the Security + # workflow's Monday slot. + - cron: '44 5 * * 2' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + # Uploading results to the code scanning API. + security-events: write + strategy: + fail-fast: false + matrix: + include: + - language: go + build-mode: manual + - language: actions + build-mode: none + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Go + if: matrix.language == 'go' + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + + - name: Initialize CodeQL + uses: github/codeql-action/init@c16c0f3f2812ec4bb3750a5ed64873fe2ce0fbef # codeql-bundle-v2.26.3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # Matches the query suite and threat model the previous default + # setup ran with. + queries: security-extended + + - name: Build + if: matrix.build-mode == 'manual' + run: go build ./... + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@c16c0f3f2812ec4bb3750a5ed64873fe2ce0fbef # codeql-bundle-v2.26.3 + with: + category: /language:${{ matrix.language }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c442083..f7d0ae2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +## [1.7.0] - 2026-08-24 + +### Changed + +- Go toolchain moves to 1.27.0, from the 1.26 line. The `go` directive in `go.mod` + is what every workflow feeds to `setup-go`, so this sets the language version and + the standard library CI builds against. The Dockerfile builder image moves to + `golang:1.27.0` to match. +- golangci-lint moves from v2.12.2 to v2.13.1. Its `modernize` linter flagged twelve + issues in the test suite, all now fixed: embedded fields written through the + promoted-field shorthand in composite literals, and one `errors.As` call replaced + with `errors.AsType`, which the 1.27 standard library adds. +- Every GitHub Action was checked against its latest release. All were already + pinned to the newest version by commit SHA, so none needed to move. +- CodeQL moves from default setup to a workflow in the repository. Default setup + pins its own Go toolchain and runs the extractor with `GOTOOLCHAIN=local`, so it + could not build a module whose `go` directive asks for 1.27.0. The workflow runs + `setup-go` against `go.mod` first, so a future Go bump will not block code + scanning. Same languages, same `security-extended` query suite as before. + +### Known limits + +- `errors.AsType` means the test suite no longer builds on Go 1.26. + ## [1.6.1] - 2026-08-24 ### Changed diff --git a/Dockerfile b/Dockerfile index 6f2456e..6417dca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ARG GIT_SHA="no-sha" -FROM --platform=$BUILDPLATFORM golang:1.26.7 AS build-env +FROM --platform=$BUILDPLATFORM golang:1.27.0 AS build-env WORKDIR /app ARG GIT_SHA ARG TARGETARCH diff --git a/go.mod b/go.mod index 5de171e..38ae7c3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/yottabytesolutions/meterlogger -go 1.26.7 +go 1.27.0 require ( github.com/ClickHouse/clickhouse-go/v2 v2.48.0 diff --git a/internal/adapters/sink/mqtt/writers_test.go b/internal/adapters/sink/mqtt/writers_test.go index 302d793..ff8dcbe 100644 --- a/internal/adapters/sink/mqtt/writers_test.go +++ b/internal/adapters/sink/mqtt/writers_test.go @@ -449,9 +449,10 @@ func TestDucoWriter_Nodes(t *testing.T) { ctx := context.Background() // RF sensor node. - rf := domain.DucoRFSensorStatus{BaseDucoNodeStatus: ducoBase(), Temp: 21.5, Co2: 600, Rh: 55} - rf.Node = 3 - rf.DevType = "SENSO" + rfBase := ducoBase() + rfBase.Node = 3 + rfBase.DevType = "SENSO" + rf := domain.DucoRFSensorStatus{BaseDucoNodeStatus: rfBase, Temp: 21.5, Co2: 600, Rh: 55} if err := w.StoreNodeData(ctx, rf); err != nil { t.Fatalf("StoreNodeData rf: %v", err) } @@ -472,8 +473,9 @@ func TestDucoWriter_Nodes(t *testing.T) { } // Box node and valve node land on their own topics. - boxNode := domain.DucoNodeBoxStatus{BaseDucoNodeStatus: ducoBase(), Trgt: 30, Actl: 20, Temp: 20, Co2: 450, Rh: 40} - boxNode.Node = 1 + boxBase := ducoBase() + boxBase.Node = 1 + boxNode := domain.DucoNodeBoxStatus{BaseDucoNodeStatus: boxBase, Trgt: 30, Actl: 20, Temp: 20, Co2: 450, Rh: 40} if err := w.StoreNodeData(ctx, boxNode); err != nil { t.Fatalf("StoreNodeData box node: %v", err) } diff --git a/internal/adapters/sink/qdb/qdb_test.go b/internal/adapters/sink/qdb/qdb_test.go index 271aa99..cd13081 100644 --- a/internal/adapters/sink/qdb/qdb_test.go +++ b/internal/adapters/sink/qdb/qdb_test.go @@ -664,14 +664,12 @@ func TestDucoQuestDBRepository_StoreNodeData_RFSensor(t *testing.T) { client, sender := newTestDBClient() repo := NewDucoQuestDBRepository(client, "ventilation", testLogger()) node := domain.DucoRFSensorStatus{ - BaseDucoNodeStatus: domain.BaseDucoNodeStatus{ - Node: 3, - DevType: "UCCO2", - Location: "living room", - }, - Co2: 800.0, - Temp: 21.5, - Rh: 55.0, + Node: 3, + DevType: "UCCO2", + Location: "living room", + Co2: 800.0, + Temp: 21.5, + Rh: 55.0, } if err := repo.StoreNodeData(context.Background(), node); err != nil { t.Fatalf("StoreNodeData(RFSensor) unexpected error: %v", err) @@ -691,13 +689,11 @@ func TestDucoQuestDBRepository_StoreNodeData_BoxNode(t *testing.T) { client, sender := newTestDBClient() repo := NewDucoQuestDBRepository(client, "ventilation", testLogger()) node := domain.DucoNodeBoxStatus{ - BaseDucoNodeStatus: domain.BaseDucoNodeStatus{ - Node: 1, - DevType: "BOX", - }, - Trgt: 100, - Actl: 80, - Co2: 600.0, + Node: 1, + DevType: "BOX", + Trgt: 100, + Actl: 80, + Co2: 600.0, } if err := repo.StoreNodeData(context.Background(), node); err != nil { t.Fatalf("StoreNodeData(BoxNode) unexpected error: %v", err) @@ -716,12 +712,10 @@ func TestDucoQuestDBRepository_StoreNodeData_Valve(t *testing.T) { client, sender := newTestDBClient() repo := NewDucoQuestDBRepository(client, "ventilation", testLogger()) node := domain.DucoNodeBoxValveStatus{ - BaseDucoNodeStatus: domain.BaseDucoNodeStatus{ - Node: 2, - DevType: "VLV", - }, - Trgt: 50, - Actl: 45, + Node: 2, + DevType: "VLV", + Trgt: 50, + Actl: 45, } if err := repo.StoreNodeData(context.Background(), node); err != nil { t.Fatalf("StoreNodeData(Valve) unexpected error: %v", err) diff --git a/internal/adapters/source/ducobox/ducobox_test.go b/internal/adapters/source/ducobox/ducobox_test.go index cbea395..646ae97 100644 --- a/internal/adapters/source/ducobox/ducobox_test.go +++ b/internal/adapters/source/ducobox/ducobox_test.go @@ -18,10 +18,10 @@ func testLogger() *slog.Logger { func TestParseDucoNodeStatus_Box(t *testing.T) { node := nodeBoxStatusDTO{ - baseNodeStatusDTO: baseNodeStatusDTO{Node: 1, DevType: devTypeBox}, - Trgt: 100, - Actl: 80, - Rh: 55.5, + Node: 1, DevType: devTypeBox, + Trgt: 100, + Actl: 80, + Rh: 55.5, } data, err := json.Marshal(node) if err != nil { @@ -63,9 +63,9 @@ func TestParseDucoNodeStatus_BoxLowercase(t *testing.T) { func TestParseDucoNodeStatus_VLV(t *testing.T) { node := nodeBoxValveStatusDTO{ - baseNodeStatusDTO: baseNodeStatusDTO{Node: 2, DevType: devTypeValve}, - Trgt: 50, - Actl: 45, + Node: 2, DevType: devTypeValve, + Trgt: 50, + Actl: 45, } data, err := json.Marshal(node) if err != nil { @@ -88,9 +88,9 @@ func TestParseDucoNodeStatus_VLV(t *testing.T) { func TestParseDucoNodeStatus_UCCO2(t *testing.T) { node := rfSensorStatusDTO{ - baseNodeStatusDTO: baseNodeStatusDTO{Node: 3, DevType: devTypeUCCO2}, - Co2: 800.0, - Rh: 0, + Node: 3, DevType: devTypeUCCO2, + Co2: 800.0, + Rh: 0, } data, err := json.Marshal(node) if err != nil { @@ -113,9 +113,9 @@ func TestParseDucoNodeStatus_UCCO2(t *testing.T) { func TestParseDucoNodeStatus_UCRH(t *testing.T) { node := rfSensorStatusDTO{ - baseNodeStatusDTO: baseNodeStatusDTO{Node: 4, DevType: devTypeUCRH}, - Co2: 0, - Rh: 65.0, + Node: 4, DevType: devTypeUCRH, + Co2: 0, + Rh: 65.0, } data, err := json.Marshal(node) if err != nil { @@ -259,8 +259,8 @@ func TestDucoReader_ReadBoxStatus_InvalidJSON(t *testing.T) { func TestDucoReader_ReadNodeStatus_Success(t *testing.T) { node := nodeBoxStatusDTO{ - baseNodeStatusDTO: baseNodeStatusDTO{Node: 1, DevType: devTypeBox}, - Trgt: 100, + Node: 1, DevType: devTypeBox, + Trgt: 100, } body, _ := json.Marshal(node) diff --git a/internal/adapters/source/enphase/token_test.go b/internal/adapters/source/enphase/token_test.go index 7aa9fff..9401a39 100644 --- a/internal/adapters/source/enphase/token_test.go +++ b/internal/adapters/source/enphase/token_test.go @@ -249,8 +249,7 @@ func TestCloudClient_RejectsSelfSignedTLS(t *testing.T) { _ = resp.Body.Close() t.Fatal("cloud client should reject self-signed TLS") } - var certErr *tls.CertificateVerificationError - if !errors.As(err, &certErr) { + if _, ok := errors.AsType[*tls.CertificateVerificationError](err); !ok { t.Errorf("expected certificate verification error, got: %v", err) } } diff --git a/internal/service/service_test.go b/internal/service/service_test.go index efb25f5..e6335aa 100644 --- a/internal/service/service_test.go +++ b/internal/service/service_test.go @@ -565,7 +565,7 @@ func TestDucoLoggingService_Start_Flushes(t *testing.T) { func TestDucoLoggingService_Start_WithNodes(t *testing.T) { boxStatus := domain.DucoBoxStatus{} nodeData := domain.DucoNodeBoxStatus{ - BaseDucoNodeStatus: domain.BaseDucoNodeStatus{Node: 1, DevType: "BOX"}, + Node: 1, DevType: "BOX", } reader := &mockDucoReader{ boxStatus: boxStatus,