diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 7ba405aa..f0803b98 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -17,6 +17,7 @@ updates:
- "/uiprojector"
- "/trace"
- "/storage"
+ - "/notifications"
- "/json"
- "/xmlgen"
- "/htmlgen"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 67360984..0a449902 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -398,6 +398,41 @@ jobs:
- name: Run Tests (includes build)
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
+ # The notifications/ package is its own Go module, so the root ./... jobs above do not
+ # cover it. Run its quality checks and tests independently.
+ notifications-module:
+ name: Notifications Module
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ defaults:
+ run:
+ working-directory: notifications
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ persist-credentials: false
+ - uses: actions/setup-go@v7
+ with:
+ go-version-file: 'notifications/go.mod'
+ cache-dependency-path: notifications/go.sum
+ cache: true
+
+ - name: Verify go.mod
+ run: |
+ go mod tidy
+ git diff --exit-code go.mod go.sum
+
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@v9
+ with:
+ version: v2.11.2
+ args: --timeout=5m
+ working-directory: notifications
+
+ - name: Run Tests (includes build)
+ run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
+
# The uiprojector/ package is its own Go module, so the root ./... jobs above do not
# cover it. Run its quality checks and tests independently.
uiprojector-module:
diff --git a/README.md b/README.md
index 44ff729a..58f42b57 100644
--- a/README.md
+++ b/README.md
@@ -16,30 +16,30 @@ This SDK provides all the infrastructure pieces to build such a system, while ke
## Packages
-| Package | Purpose |
-|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| [`artifact`](artifact/README.md) | Versioned configuration registry — load workflow definitions, form schemas, and templates by ID (includes [`artifact/adapter`](artifact/adapter/README.md), bridge adapters for domain types) |
-| [`authn`](authn/README.md) | JWT validation, identity context injection, and HTTP middleware |
-| [`authz`](authz/README.md) | Scope-based authorization middleware and predicates, decoupled from authn |
-| [`configyaml`](configyaml/README.md) | Loads a YAML config file into a Go struct, resolving `{{env:NAME}}` / `{{file:/path}}` placeholders via `secret.SecretRef` |
-| [`cors`](cors/README.md) | CORS HTTP middleware |
-| [`database`](database/README.md) | `database/sql` connection factory (PostgreSQL, MySQL, SQLite) with pooling and health checks |
-| [`htmlgen`](htmlgen/README.md) | Print-ready HTML document generation from a static template and structured data, with contextual escaping |
-| [`httputil`](httputil/README.md) | Shared HTTP response helpers for JSON payloads and correlation-ID-tagged API error bodies |
-| [`json`](json/README.md) | Packages for working with decoded JSON documents (includes [`json/jsonpointer`](json/jsonpointer/README.md), RFC 6901 JSON Pointer get/set) |
-| [`notification`](notification/README.md) | Multi-channel notification router (SMS, email) with pluggable providers |
-| [`pagination`](pagination/README.md) | Standard pagination envelope and query parameter parsing |
-| [`payment`](payment/README.md) | Pluggable payment gateway orchestration with webhook processing and idempotency |
-| [`remote`](remote/README.md) | Registry-based outbound HTTP client with pluggable auth (API key, Bearer, OAuth2) |
-| [`secret`](secret/README.md) | Secret-bearing config values (`SecretRef`) resolved from env, file, or literal sources |
-| [`shared`](shared/README.md) | Small, dependency-free helpers (deep copy, nested map access, config validation) used across other modules |
-| [`storage`](storage/README.md) | File storage abstraction (local filesystem and AWS S3) with presigned URLs |
-| [`taskflow`](taskflow/README.md) | Micro-interactive task orchestration — the core engine for human-in-the-loop steps |
-| [`temporal`](temporal/README.md) | Temporal client factory |
-| [`trace`](trace/README.md) | HTTP request trace propagation and context-based trace ID correlation |
-| [`uiprojector`](uiprojector/README.md) | Zone-based, metadata-driven UI rendering from workflow state and business data |
-| [`workflow`](workflow/README.md) | JSON DSL-driven Temporal workflow graph interpreter |
-| [`xmlgen`](xmlgen/README.md) | XML document generation from a static template and structured data, with automatic escaping |
+| Package | Purpose |
+|--------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [`artifact`](artifact/README.md) | Versioned configuration registry — load workflow definitions, form schemas, and templates by ID (includes [`artifact/adapter`](artifact/adapter/README.md), bridge adapters for domain types) |
+| [`authn`](authn/README.md) | JWT validation, identity context injection, and HTTP middleware |
+| [`authz`](authz/README.md) | Scope-based authorization middleware and predicates, decoupled from authn |
+| [`configyaml`](configyaml/README.md) | Loads a YAML config file into a Go struct, resolving `{{env:NAME}}` / `{{file:/path}}` placeholders via `secret.SecretRef` |
+| [`cors`](cors/README.md) | CORS HTTP middleware |
+| [`database`](database/README.md) | `database/sql` connection factory (PostgreSQL, MySQL, SQLite) with pooling and health checks |
+| [`htmlgen`](htmlgen/README.md) | Print-ready HTML document generation from a static template and structured data, with contextual escaping |
+| [`httputil`](httputil/README.md) | Shared HTTP response helpers for JSON payloads and correlation-ID-tagged API error bodies |
+| [`json`](json/README.md) | Packages for working with decoded JSON documents (includes [`json/jsonpointer`](json/jsonpointer/README.md), RFC 6901 JSON Pointer get/set) |
+| [`notifications`](notifications/README.md) | Multi-channel notification router (SMS, email) with pluggable providers |
+| [`pagination`](pagination/README.md) | Standard pagination envelope and query parameter parsing |
+| [`payment`](payment/README.md) | Pluggable payment gateway orchestration with webhook processing and idempotency |
+| [`remote`](remote/README.md) | Registry-based outbound HTTP client with pluggable auth (API key, Bearer, OAuth2) |
+| [`secret`](secret/README.md) | Secret-bearing config values (`SecretRef`) resolved from env, file, or literal sources |
+| [`shared`](shared/README.md) | Small, dependency-free helpers (deep copy, nested map access, config validation) used across other modules |
+| [`storage`](storage/README.md) | File storage abstraction (local filesystem and AWS S3) with presigned URLs |
+| [`taskflow`](taskflow/README.md) | Micro-interactive task orchestration — the core engine for human-in-the-loop steps |
+| [`temporal`](temporal/README.md) | Temporal client factory |
+| [`trace`](trace/README.md) | HTTP request trace propagation and context-based trace ID correlation |
+| [`uiprojector`](uiprojector/README.md) | Zone-based, metadata-driven UI rendering from workflow state and business data |
+| [`workflow`](workflow/README.md) | JSON DSL-driven Temporal workflow graph interpreter |
+| [`xmlgen`](xmlgen/README.md) | XML document generation from a static template and structured data, with automatic escaping |
## Requirements
@@ -76,7 +76,7 @@ flowchart TD
P["USER_INPUT · PAYMENT · EXTERNAL_REVIEW · API_CALL · custom"]
end
- SS["payment · remote · notification · storage"]
+ SS["payment · remote · notifications · storage"]
AR[("artifact registry
workflow defs · task templates
form schemas · render templates")]
Portal -->|HTTP| server
diff --git a/go.mod b/go.mod
index 1d290b82..44270b99 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,6 @@ go 1.26.0
require (
github.com/OpenNSW/core/remote v0.8.0
- github.com/OpenNSW/core/secret v0.2.0
github.com/OpenNSW/core/shared v0.3.0
github.com/OpenNSW/core/uiprojector v0.3.0
github.com/aws/aws-sdk-go-v2 v1.47.1
@@ -21,6 +20,7 @@ require (
)
require (
+ github.com/OpenNSW/core/secret v0.2.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.4 // indirect
diff --git a/notification/README.md b/notifications/README.md
similarity index 85%
rename from notification/README.md
rename to notifications/README.md
index a7f05160..9e277277 100644
--- a/notification/README.md
+++ b/notifications/README.md
@@ -1,11 +1,17 @@
-# notification
+# notifications
+
+`github.com/OpenNSW/core/notifications` is its own Go module. It moved from `github.com/OpenNSW/core/notification` in the root module. The package is still named `notification`, so only the import path changes; code keeps using `notification.X`. Import it with the name written out, as `goimports` writes it when a package's name differs from its path:
+
+```go
+import notification "github.com/OpenNSW/core/notifications"
+```
A multi-channel notification router with a pluggable provider model. Your application registers one provider per channel type (email, SMS, etc.); the manager dispatches each `Request` to the correct provider at runtime.
## Usage
```go
-import "github.com/OpenNSW/core/notification"
+import notification "github.com/OpenNSW/core/notifications"
manager, err := notification.NewManager(
notification.Config{
diff --git a/notification/config.go b/notifications/config.go
similarity index 100%
rename from notification/config.go
rename to notifications/config.go
diff --git a/notification/config_test.go b/notifications/config_test.go
similarity index 100%
rename from notification/config_test.go
rename to notifications/config_test.go
diff --git a/notifications/go.mod b/notifications/go.mod
new file mode 100644
index 00000000..555ec1c8
--- /dev/null
+++ b/notifications/go.mod
@@ -0,0 +1,8 @@
+module github.com/OpenNSW/core/notifications
+
+go 1.26
+
+require (
+ github.com/OpenNSW/core/remote v0.8.0
+ github.com/OpenNSW/core/secret v0.2.0
+)
diff --git a/notifications/go.sum b/notifications/go.sum
new file mode 100644
index 00000000..4460318d
--- /dev/null
+++ b/notifications/go.sum
@@ -0,0 +1,12 @@
+github.com/OpenNSW/core/remote v0.8.0 h1:Ot44DtkEtzqKffy77EI0S6BCH1xDspQ2y2lSFKcZpPg=
+github.com/OpenNSW/core/remote v0.8.0/go.mod h1:BzfC7ppzEAd5fUQmIsgeR2lZ6LzygNU4prNjvDpDH+4=
+github.com/OpenNSW/core/secret v0.2.0 h1:7kxJYbVNJhN9X9/YyTUci4bv9oYKcm0nT/wBVrxBChQ=
+github.com/OpenNSW/core/secret v0.2.0/go.mod h1:MEkX9vnlPRVTn+JQQsb192PoY6p3hbFSOIV+FxIMrc0=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/notification/manager.go b/notifications/manager.go
similarity index 100%
rename from notification/manager.go
rename to notifications/manager.go
diff --git a/notification/manager_test.go b/notifications/manager_test.go
similarity index 100%
rename from notification/manager_test.go
rename to notifications/manager_test.go
diff --git a/notification/provider.go b/notifications/provider.go
similarity index 100%
rename from notification/provider.go
rename to notifications/provider.go
diff --git a/notification/providers/email.go b/notifications/providers/email.go
similarity index 97%
rename from notification/providers/email.go
rename to notifications/providers/email.go
index 24384164..b9903863 100644
--- a/notification/providers/email.go
+++ b/notifications/providers/email.go
@@ -10,7 +10,7 @@ import (
"fmt"
"net/http"
- "github.com/OpenNSW/core/notification"
+ notification "github.com/OpenNSW/core/notifications"
"github.com/OpenNSW/core/remote"
"github.com/OpenNSW/core/remote/auth"
"github.com/OpenNSW/core/secret"
diff --git a/notification/providers/helpers.go b/notifications/providers/helpers.go
similarity index 100%
rename from notification/providers/helpers.go
rename to notifications/providers/helpers.go
diff --git a/notification/providers/sms.go b/notifications/providers/sms.go
similarity index 97%
rename from notification/providers/sms.go
rename to notifications/providers/sms.go
index a92b39c2..5eeba2ca 100644
--- a/notification/providers/sms.go
+++ b/notifications/providers/sms.go
@@ -10,7 +10,7 @@ import (
"fmt"
"net/http"
- "github.com/OpenNSW/core/notification"
+ notification "github.com/OpenNSW/core/notifications"
"github.com/OpenNSW/core/remote"
)
diff --git a/notification/test/integration/sms_integration_test.go b/notifications/test/integration/sms_integration_test.go
similarity index 95%
rename from notification/test/integration/sms_integration_test.go
rename to notifications/test/integration/sms_integration_test.go
index 36f1bfcf..0cf6c51a 100644
--- a/notification/test/integration/sms_integration_test.go
+++ b/notifications/test/integration/sms_integration_test.go
@@ -11,8 +11,8 @@ import (
"testing"
"time"
- "github.com/OpenNSW/core/notification"
- "github.com/OpenNSW/core/notification/providers"
+ notification "github.com/OpenNSW/core/notifications"
+ "github.com/OpenNSW/core/notifications/providers"
)
func TestSMSIntegration(t *testing.T) {
diff --git a/notification/types.go b/notifications/types.go
similarity index 100%
rename from notification/types.go
rename to notifications/types.go