Skip to content

Commit fdb009b

Browse files
authored
Merge branch 'main' into feat/20-homebrew-formula
2 parents 7cd6853 + 5d46ab0 commit fdb009b

23 files changed

Lines changed: 736 additions & 9 deletions
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: Duplicate discussion check
2+
3+
on:
4+
discussion:
5+
types: [created]
6+
7+
permissions:
8+
discussions: write
9+
10+
jobs:
11+
check-duplicate:
12+
runs-on: ubuntu-latest
13+
timeout-minutes: 5
14+
steps:
15+
- name: Find similar discussions
16+
uses: actions/github-script@v7
17+
with:
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
script: |
20+
const discussion = context.payload.discussion;
21+
const title = discussion.title || '';
22+
23+
// Extract meaningful keywords from the title
24+
const STOP_WORDS = new Set([
25+
'is', 'a', 'the', 'for', 'in', 'on', 'with', 'how', 'do',
26+
'does', 'can', 'to', 'of', 'and', 'or', 'it', 'at', 'by',
27+
'an', 'be', 'as', 'from', 'that', 'this', 'what', 'why',
28+
'when', 'where', 'which', 'who', 'will', 'are', 'was',
29+
'not', 'my', 'we', 'i', 'you',
30+
]);
31+
32+
const keywords = title
33+
.toLowerCase()
34+
.replace(/[^a-z0-9\s]/g, ' ')
35+
.split(/\s+/)
36+
.filter(w => w.length > 1 && !STOP_WORDS.has(w))
37+
.slice(0, 5);
38+
39+
if (keywords.length < 2) {
40+
core.info('Title too short or no meaningful keywords; skipping duplicate check.');
41+
return;
42+
}
43+
44+
const searchQuery = `repo:opendecree/decree is:discussion ${keywords.join(' ')}`;
45+
core.info(`Search query: ${searchQuery}`);
46+
47+
let items;
48+
try {
49+
const results = await github.rest.search.issuesAndPullRequests({
50+
q: searchQuery,
51+
per_page: 5,
52+
});
53+
items = results.data.items;
54+
} catch (err) {
55+
core.warning(`Search API error (skipping): ${err.message}`);
56+
return;
57+
}
58+
59+
// Filter out the current discussion
60+
const matches = items
61+
.filter(item => item.number !== discussion.number)
62+
.slice(0, 3);
63+
64+
if (matches.length === 0) {
65+
core.info('No similar discussions found; no comment posted.');
66+
return;
67+
}
68+
69+
const bulletList = matches
70+
.map(item => `- [${item.title}](${item.html_url})`)
71+
.join('\n');
72+
73+
const commentBody = [
74+
'👋 Before the community answers, here are some existing discussions that might cover your question:',
75+
'',
76+
bulletList,
77+
'',
78+
'If none of these answer your question, feel free to continue the discussion!',
79+
].join('\n');
80+
81+
await github.graphql(`
82+
mutation AddComment($discussionId: ID!, $body: String!) {
83+
addDiscussionComment(input: {discussionId: $discussionId, body: $body}) {
84+
comment { id }
85+
}
86+
}
87+
`, {
88+
discussionId: discussion.node_id,
89+
body: commentBody,
90+
});
91+
92+
core.info(`Posted duplicate-discussion comment on discussion #${discussion.number}`);

.github/workflows/release.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,39 @@ jobs:
461461
path: |
462462
bench-e2e.txt
463463
bench-e2e-stats.txt
464+
465+
# Fires repository_dispatch events to decree-python, decree-typescript,
466+
# and demos so they can regenerate proto stubs and open PRs automatically.
467+
dispatch-sdk-regen:
468+
name: Dispatch SDK stub regen
469+
needs: [binaries, release-notes]
470+
runs-on: ubuntu-latest
471+
permissions:
472+
contents: read
473+
steps:
474+
- name: Dispatch to decree-python
475+
# peter-evans/repository-dispatch@v3
476+
uses: peter-evans/repository-dispatch@0e0cf047c08f936c436da4399814cdb4880c8cbf
477+
with:
478+
token: ${{ secrets.PROJECT_TOKEN }}
479+
repository: opendecree/decree-python
480+
event-type: decree-released
481+
client-payload: '{"version": "${{ github.ref_name }}", "sha": "${{ github.sha }}"}'
482+
483+
- name: Dispatch to decree-typescript
484+
# peter-evans/repository-dispatch@v3
485+
uses: peter-evans/repository-dispatch@0e0cf047c08f936c436da4399814cdb4880c8cbf
486+
with:
487+
token: ${{ secrets.PROJECT_TOKEN }}
488+
repository: opendecree/decree-typescript
489+
event-type: decree-released
490+
client-payload: '{"version": "${{ github.ref_name }}", "sha": "${{ github.sha }}"}'
491+
492+
- name: Dispatch to demos
493+
# peter-evans/repository-dispatch@v3
494+
uses: peter-evans/repository-dispatch@0e0cf047c08f936c436da4399814cdb4880c8cbf
495+
with:
496+
token: ${{ secrets.PROJECT_TOKEN }}
497+
repository: opendecree/demos
498+
event-type: decree-released
499+
client-payload: '{"version": "${{ github.ref_name }}", "sha": "${{ github.sha }}"}'

cmd/decree/go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@ module github.com/opendecree/decree/cmd/decree
33
go 1.24.0
44

55
require (
6-
github.com/opendecree/decree/api v0.1.2
76
github.com/opendecree/decree/sdk/adminclient v0.1.2
87
github.com/opendecree/decree/sdk/configclient v0.1.2
98
github.com/opendecree/decree/sdk/grpctransport v0.1.0
109
github.com/opendecree/decree/sdk/tools v0.1.0
1110
github.com/spf13/cobra v1.10.2
1211
google.golang.org/grpc v1.80.0
13-
google.golang.org/protobuf v1.36.11
1412
gopkg.in/yaml.v3 v3.0.1
1513
)
1614

@@ -19,17 +17,20 @@ require (
1917
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
2018
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2119
github.com/kr/text v0.2.0 // indirect
20+
github.com/opendecree/decree/api v0.1.2 // indirect
2221
github.com/opendecree/decree/sdk/configwatcher v0.1.2 // indirect
2322
github.com/opendecree/decree/sdk/retry v0.0.0 // indirect
2423
github.com/rogpeppe/go-internal v1.14.1 // indirect
2524
github.com/russross/blackfriday/v2 v2.1.0 // indirect
25+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
2626
github.com/spf13/pflag v1.0.9 // indirect
2727
go.yaml.in/yaml/v3 v3.0.4 // indirect
2828
golang.org/x/net v0.49.0 // indirect
2929
golang.org/x/sys v0.41.0 // indirect
3030
golang.org/x/text v0.34.0 // indirect
3131
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
3232
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
33+
google.golang.org/protobuf v1.36.11 // indirect
3334
)
3435

3536
replace github.com/opendecree/decree/api => ../../api

cmd/decree/go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
33
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
44
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
55
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
6+
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
7+
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
68
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
79
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
810
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -25,6 +27,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
2527
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
2628
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
2729
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
30+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
31+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
2832
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
2933
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
3034
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=

codecov.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ coverage:
66
threshold: 0%
77
patch:
88
default:
9-
target: 80%
9+
target: 70%
1010

1111
# Exclusion decisions are documented in scripts/check-coverage.sh (COVERAGE_EXCLUDES).
1212
# Keep this list in sync with that file.

coverage-thresholds.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"sdk/configclient": 87.1,
66
"sdk/configwatcher": 90.9,
77
"sdk/grpctransport": 90.0,
8-
"sdk/tools": 96.1
8+
"sdk/tools": 96.0
99
}

docs/adr/ADR-001-grpc-over-rest.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# ADR-001: gRPC over REST
2+
3+
**Date:** 2026-06-05
4+
**Status:** Accepted
5+
**Deciders:** OpenDecree maintainers
6+
7+
## Context
8+
9+
OpenDecree is a multi-tenant configuration service that needs to support client SDKs in multiple languages (Go, Python, TypeScript). The API must support:
10+
11+
- Strongly typed contracts shared across the server and all SDK clients
12+
- Efficient binary serialization for high-throughput config reads
13+
- Streaming for real-time config change notifications (config watcher pattern)
14+
- Automatic client code generation to keep SDKs in sync with the server
15+
16+
REST/JSON APIs require manual schema maintenance (OpenAPI), are weakly typed at the transport layer, and lack native streaming. HTTP/2 multiplexing and binary framing make gRPC a better fit for an SDK-centric, performance-sensitive service.
17+
18+
## Decision
19+
20+
Use Protocol Buffers (proto3) as the API schema language and gRPC as the primary transport. Proto files in `proto/centralconfig/v1/` are the single source of truth for the API contract. Generated code is committed to `api/centralconfig/v1/`.
21+
22+
`buf` is used for linting, breaking-change detection, and code generation (running plugins locally via Docker, not remote registries).
23+
24+
## Consequences
25+
26+
**Positive:**
27+
28+
- Strong typing enforced at compile time across all languages
29+
- Automatic client stub generation via `buf generate` — SDKs are always in sync
30+
- Native bidirectional streaming for config watch notifications
31+
- Compact binary encoding (Protocol Buffers) reduces latency and bandwidth vs. JSON
32+
- `buf lint` and `buf breaking` catch API contract violations before they ship
33+
34+
**Negative:**
35+
36+
- Browser clients cannot call gRPC directly; grpc-web or a transcoding gateway is required
37+
- Not curl-friendly — human debugging requires tooling such as grpcurl or a gRPC reflection client
38+
- Proto schema changes must follow strict backward-compatibility rules (or be coordinated with a breaking migration during alpha)
39+
- Adds `buf` and proto toolchain to the developer setup (mitigated by Docker-based generation)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# ADR-002: Specs-First Workflow
2+
3+
**Date:** 2026-06-05
4+
**Status:** Accepted
5+
**Deciders:** OpenDecree maintainers
6+
7+
## Context
8+
9+
OpenDecree has two distinct schema layers that must remain consistent:
10+
11+
1. **API contracts** — defined in `.proto` files, which drive gRPC server interfaces and all SDK clients
12+
2. **Database contracts** — defined in `.sql` query files, which drive the Go database access layer via `sqlc`
13+
14+
Without a clear source-of-truth policy, there is a risk that hand-written Go code diverges from the API or DB schema, especially as the project evolves across multiple contributors and multiple language SDKs. A "code-first" approach would mean maintaining three or more representations of the same concept in sync by hand.
15+
16+
## Decision
17+
18+
Proto files (`proto/centralconfig/v1/`) and SQL query files (`db/queries/`) are the canonical sources of truth. Go implementation code is written after — and in response to — changes in those files.
19+
20+
The standard workflow is:
21+
22+
1. Edit `.proto` or `.sql` files
23+
2. Run `make generate` (runs `buf generate` and `sqlc generate` in Docker)
24+
3. Implement or update Go business logic in `internal/`
25+
4. Run `make test` and `make lint`
26+
5. Commit — generated files (`*.pb.go`, `*.gen.go`) are checked into git
27+
28+
Generated files are marked as `linguist-generated` in `.gitattributes` so they are excluded from language statistics and diff noise on GitHub.
29+
30+
## Consequences
31+
32+
**Positive:**
33+
34+
- Single source of truth for both the API surface and the DB access layer
35+
- Consistency is mechanically enforced — divergence between spec and implementation is a compile error
36+
- Multi-language SDK clients are always in sync with the server API without manual effort
37+
- `buf breaking` catches accidental API regressions at lint time
38+
- `sqlc` type-checks SQL at generation time, catching query errors before runtime
39+
40+
**Negative:**
41+
42+
- Extra codegen step is required before implementing any change — developers must run `make generate` before editing business logic
43+
- Generated files are committed to git, which increases diff size and requires discipline to avoid manual edits to generated files
44+
- Mistakes in a proto or SQL file can break generation for all downstream modules until fixed
45+
- Local development requires Docker to run `buf` and `sqlc` generators
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# ADR-003: PostgreSQL + Redis Architecture
2+
3+
**Date:** 2026-06-05
4+
**Status:** Accepted
5+
**Deciders:** OpenDecree maintainers
6+
7+
## Context
8+
9+
OpenDecree needs to:
10+
11+
1. Durably store configuration values, schema definitions, and an immutable audit log across multiple tenants
12+
2. Serve config reads with low latency — config lookups are on the hot path for every service that depends on this system
13+
3. Propagate configuration changes in real time to connected clients (config watcher subscriptions)
14+
15+
A single data store would force tradeoffs: a relational database handles durable storage and ACID guarantees well but is not the right tool for low-latency cache reads or fan-out pub/sub; an in-memory store handles cache and messaging well but lacks the durability and query capabilities needed for the config history and audit log.
16+
17+
## Decision
18+
19+
Use **PostgreSQL 17** as the primary durable store and **Redis 7** as the cache and pub/sub layer.
20+
21+
- PostgreSQL stores all config values (versioned), schema definitions, tenant records, and the audit log. Row-Level Security enforces tenant isolation at the database layer (see ADR-005).
22+
- Redis caches resolved config values for fast reads. On a cache miss, the server fetches from PostgreSQL and repopulates the cache.
23+
- Redis pub/sub propagates change notifications to all server instances when a config value is written, so connected config-watcher clients receive updates promptly across a horizontally scaled deployment.
24+
25+
Both dependencies are placed behind Go interfaces (`cache.Cache`, `pubsub.PubSub`) so they can be swapped or mocked in tests without touching business logic.
26+
27+
## Consequences
28+
29+
**Positive:**
30+
31+
- Proven, widely-adopted technologies with strong operational tooling and hosting options
32+
- Full ACID guarantees from PostgreSQL for config mutations and audit entries
33+
- Optimistic read path: most config reads are served from Redis without hitting the database
34+
- Real-time change propagation across server replicas with minimal coupling
35+
- Interfaces-behind-abstraction pattern keeps the business logic testable and the dependencies replaceable
36+
37+
**Negative:**
38+
39+
- Two external infrastructure dependencies increase operational complexity compared to a single-store solution
40+
- Redis is a single point of failure for pub/sub — if Redis is unavailable, change notifications are not delivered (reads can still be served from PostgreSQL, but watchers will not see updates until Redis recovers)
41+
- Cache invalidation logic must be kept in sync with write paths; a missed invalidation leads to stale reads until TTL expiry
42+
- Local development and CI require both PostgreSQL and Redis to be running (addressed via Docker Compose)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# ADR-004: Metadata-Headers-First Authentication
2+
3+
**Date:** 2026-06-05
4+
**Status:** Accepted
5+
**Deciders:** OpenDecree maintainers
6+
7+
## Context
8+
9+
OpenDecree serves two very different deployment contexts:
10+
11+
1. **Internal / developer environments** — services on a private network or inside a Kubernetes cluster where callers are trusted and setting up a full JWT/JWKS infrastructure is unnecessary overhead
12+
2. **Production / multi-tenant environments** — where callers must be cryptographically authenticated and tenant isolation must be enforced
13+
14+
A design that mandates JWT from day one creates friction for adopters who just want to integrate quickly in a trusted network. A design that defaults to no authentication at all provides no path toward production hardening. The auth mechanism must also work cleanly over gRPC, where the standard carrier is request metadata (headers).
15+
16+
## Decision
17+
18+
The default authentication mode uses gRPC metadata headers:
19+
20+
- `x-tenant-id` — identifies the calling tenant
21+
- `x-role` — declares the caller's role (`superadmin`, `admin`, `viewer`)
22+
23+
In default mode these values are accepted as-is without cryptographic validation. The server applies RBAC via the Guard chain (`TenantScopeGuard`, `RolePolicyGuard`, `FieldLockGuard`) based on the declared values.
24+
25+
JWT/JWKS authentication is opt-in: when configured, an interceptor validates the bearer token against the configured JWKS endpoint and extracts tenant ID and role from token claims, overriding the metadata headers.
26+
27+
## Consequences
28+
29+
**Positive:**
30+
31+
- Zero-config for internal services and development environments — no key management, no JWKS endpoint required
32+
- Progressive security model: operators can start with header-based auth on a trusted network and migrate to JWT for external-facing deployments without changing client code
33+
- Clean integration with gRPC metadata conventions
34+
- Auth logic is isolated in interceptors, keeping service business logic auth-agnostic
35+
36+
**Negative:**
37+
38+
- Default mode is **not safe** without a network trust boundary — any caller that can reach the gRPC port can claim any tenant ID or role; operators must be explicit about this in their deployment security model
39+
- The distinction between "trusted network mode" and "validated JWT mode" must be clearly communicated in documentation to avoid misconfiguration in production
40+
- Mixing metadata-header auth with JWT requires careful interceptor ordering to avoid header spoofing when JWT is enabled

0 commit comments

Comments
 (0)