Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail

commit_msg_file="${1:-}"

if [[ -z "$commit_msg_file" ]]; then
echo "ERROR: commit-msg hook requires a message file path." >&2
exit 1
fi

if [[ ! -f "$commit_msg_file" ]]; then
echo "ERROR: Commit message file not found: $commit_msg_file" >&2
exit 1
fi

subject_line=""
while IFS= read -r line || [[ -n "$line" ]]; do
[[ -z "$line" ]] && continue
[[ "$line" == \#* ]] && continue
subject_line="$line"
break
done < "$commit_msg_file"

if [[ -z "$subject_line" ]]; then
echo "ERROR: Commit message cannot be empty." >&2
exit 1
fi

if ! printf '%s\n' "$subject_line" | grep -Eq '^[a-z][a-z0-9-]*(\([^)]+\))?!?:[[:space:]].+'; then
cat >&2 <<'EOF'
ERROR: Commit message must follow Conventional Commits.

Examples:
feat: add product search
fix(api): handle empty catalog response
chore!: drop deprecated endpoint
EOF
exit 1
fi
1 change: 0 additions & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ REPO_ROOT="$(git rev-parse --show-toplevel)"
"$REPO_ROOT/scripts/check-go-license-headers.sh" --staged
"$REPO_ROOT/scripts/check-rust-license-headers.sh" --staged
"$REPO_ROOT/scripts/check-js-license-headers.sh" --staged

52 changes: 43 additions & 9 deletions .github/workflows/ci-integration.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: CI Integration

# Runs ONLY when files relevant to the gRPC contract or service implementations change.
# Runs ONLY when files relevant to the gRPC/datastore contracts or service implementations change.
# This workflow is NOT a required branch-protection status check.
# Core CI (ci.yml) gates merge; these tests provide deeper validation when warranted.

Expand Down Expand Up @@ -31,10 +31,6 @@ concurrency:
group: integration-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1

jobs:
integration-test:
name: Integration Tests
Expand Down Expand Up @@ -88,8 +84,46 @@ jobs:
if: always()
run: docker compose down -v

grpc-integration-test:
name: gRPC Integration Tests (testcontainers)
datastore-contract-test:
name: Datastore Contract Tests (memdb)
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.25.x'
cache-dependency-path: gitstore-api/go.sum

- name: Run datastore contract tests
working-directory: ./gitstore-api
run: go test -v -race -timeout 120s ./tests/contract/datastore/...

datastore-contract-test-scylla:
name: Datastore Contract Tests (ScyllaDB)
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.25.x'
cache-dependency-path: gitstore-api/go.sum

- name: Run ScyllaDB contract tests
working-directory: ./gitstore-api
run: go test -tags scylla -v -timeout 10m ./tests/contract/datastore/... ./internal/datastore/scylla/...

grpc-contract-test:
name: gRPC Contract Tests (testcontainers)
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -106,6 +140,6 @@ jobs:
- name: Build git-service Docker image
run: docker build -f docker/git-service.Dockerfile -t gitstore-git-service:latest .

- name: Run gRPC integration tests
- name: Run gRPC contract tests
working-directory: ./gitstore-api
run: go test -tags grpc -v -timeout 5m ./tests/integration/...
run: go test -tags grpc -v -timeout 5m ./tests/contract/grpc/...
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Auto-generated from all feature plans. Last updated: 2026-03-26
## Active Technologies
- Go 1.25 (`gitstore-api`), Rust edition 2021 (`gitstore-git-service`) (005-structured-config-mgmt)
- N/A — configuration is in-memory after startup load (005-structured-config-mgmt)
- `go-memdb` (in-memory backend) / ScyllaDB 5.x+ (production backend) (006-api-datastore-abstraction)

- (001-git-backed-ecommerce)

Expand All @@ -17,6 +18,7 @@ Auto-generated from all feature plans. Last updated: 2026-03-26
: Follow standard conventions

## Recent Changes
- 006-api-datastore-abstraction: Added Go 1.25 (`gitstore-api`)
- 005-structured-config-mgmt: Added Go 1.25 (`gitstore-api`), Rust edition 2021 (`gitstore-git-service`)

- 001-git-backed-ecommerce: Added
Expand Down
46 changes: 46 additions & 0 deletions compose.scylla.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
services:
scylla:
image: scylladb/scylla:5.4
container_name: gitstore-scylla
ports:
- "9042:9042" # CQL native transport
command: --developer-mode=1 --overprovisioned=1 --smp=1
volumes:
- scylla-data:/var/lib/scylla
networks:
- gitstore-network
healthcheck:
test: ["CMD-SHELL", "cqlsh -e 'describe cluster' 2>/dev/null || exit 1"]
interval: 15s
timeout: 10s
retries: 10
start_period: 30s

scylla-init:
image: scylladb/scylla:5.4
depends_on:
scylla:
condition: service_healthy
networks:
- gitstore-network
entrypoint: []
command: >
cqlsh scylla -e "
CREATE KEYSPACE IF NOT EXISTS gitstore
WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': '1'}
AND durable_writes = true;
"
restart: "no"

api:
depends_on:
scylla-init:
condition: service_completed_successfully
environment:
- GITSTORE_DATASTORE_BACKEND=scylla
- GITSTORE_DATASTORE_SCYLLA_HOSTS=scylla:9042
- GITSTORE_DATASTORE_SCYLLA_KEYSPACE=gitstore

volumes:
scylla-data:
driver: local
3 changes: 2 additions & 1 deletion docs/developer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ Install repository hooks once per clone:
./scripts/install-git-hooks.sh
```

This enables `.githooks/pre-commit`, which blocks commits when staged Go files are missing headers or use an outdated year.
This installs `.git/hooks/pre-commit`, which blocks commits when staged Go files are missing headers or use an outdated year, and `.git/hooks/commit-msg`, which rejects non-Conventional Commit messages.

CI also enforces this via:
- `.github/workflows/go-license-headers.yml`
Expand All @@ -481,6 +481,7 @@ CI also enforces this via:
- `./scripts/check-rust-license-headers.sh --staged`
- `./scripts/check-js-license-headers.sh --staged`
- Optionally add an External Tool that runs the same command for one-click validation.
- Use Conventional Commits for the commit summary, for example `feat: add product search`.

---

Expand Down
101 changes: 101 additions & 0 deletions docs/implementation/006-api-datastore-abstraction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Feature 006: API Datastore Abstraction

## Overview

Introduces a pluggable `Datastore` interface that decouples the GraphQL API from
its persistence layer. The first two backends are **memdb** (in-memory, default)
and **ScyllaDB** (production). All resolver reads and writes go through the
interface; backends are selected at startup via configuration.

---

## Configuration

All settings follow the `GITSTORE_` Viper prefix (feature 005).

| Config key | Env var | Default | Description |
|-----------------------------|--------------------------------------|------------------|------------------------------------------|
| `datastore.backend` | `GITSTORE_DATASTORE_BACKEND` | `memdb` | `memdb` or `scylla` |
| `datastore.scylla.hosts` | `GITSTORE_DATASTORE_SCYLLA_HOSTS` | `localhost:9042` | Comma-separated host:port pairs |
| `datastore.scylla.keyspace` | `GITSTORE_DATASTORE_SCYLLA_KEYSPACE` | `gitstore` | ScyllaDB keyspace |
| `datastore.scylla.username` | `GITSTORE_DATASTORE_SCYLLA_USERNAME` | _(empty)_ | Optional authentication username |
| `datastore.scylla.password` | `GITSTORE_DATASTORE_SCYLLA_PASSWORD` | _(empty)_ | Optional authentication password |
| `datastore.scylla.tls` | `GITSTORE_DATASTORE_SCYLLA_TLS` | `false` | Enable TLS for ScyllaDB connections |

An unrecognised `backend` value causes the service to exit at startup with a
clear message naming the invalid value and listing valid options.
The `password` field is always redacted in structured startup logs.

---

## Schema migrations (ScyllaDB only)

Migrations are embedded via `//go:embed migrations/*.cql` and applied
automatically at startup via `gocqlx/v3/migrate.FromFS`.

**Distributed lock** — before applying migrations each instance acquires
a lease with `INSERT INTO gitstore.schema_migrations_lock … IF NOT EXISTS USING TTL 120`.
If the lock is already held, the runner retries up to 3 times with
exponential back-off (2 s, 4 s, 8 s). After success the lock row is deleted.
The 120-second TTL self-expires if the holder crashes before releasing.

Migration files live in `internal/datastore/scylla/migrations/`:

| File | Purpose |
|--------------------------|--------------------------------------------------------------------------------------------------------|
| `001_initial_schema.cql` | Creates `gitstore` keyspace + `products`, `categories`, `collections`, `schema_migrations_lock` tables |
| `002_add_indexes.cql` | Secondary indexes for SKU, category, and slug lookups |

---

## Observability

Both backends are wrapped in `InstrumentedDatastore` which records:

| Metric | Type | Labels |
|-------------------------------------------------|-----------|------------------------|
| `gitstore_datastore_operation_duration_seconds` | Histogram | `operation`, `backend` |
| `gitstore_datastore_operation_errors_total` | Counter | `operation`, `backend` |

On error, the decorator additionally logs a structured `ERROR` entry with
`operation`, `backend`, `error`, and `duration_ms` fields via zap.

---

## Adding a fourth backend

1. Create `internal/datastore/<name>/backend.go` implementing all 19 methods of
`datastore.Datastore` (18 CRUD + `Close`).
2. Add a `case "<name>":` branch in `internal/datastore/factory/factory.go`.
3. Add `tests/contract/datastore/<name>_test.go` calling
`RunContractSuite(t, newYourBackend(t))`.
4. Optionally add a build tag (e.g. `//go:build <name>`) if the backend requires
a live external service.

No existing backend code changes are needed. `InstrumentedDatastore` wraps the
new backend automatically via the factory.

---

## Contract test suite

The shared suite in `tests/contract/datastore/contract_test.go` verifies
behavioural parity between backends:

```bash
# memdb (no external services)
go test ./tests/contract/datastore/...

# ScyllaDB (testcontainers — pulls image automatically)
go test -tags scylla -timeout 10m ./tests/contract/datastore/...

# ScyllaDB backend unit tests + migration tests
go test -tags scylla -timeout 10m ./internal/datastore/scylla/...
```

To run against a pre-started ScyllaDB instance instead of testcontainers,
start the override compose stack at the repo root:

```bash
docker compose -f compose.yml -f compose.scylla.yml up -d scylla
```
12 changes: 6 additions & 6 deletions docs/implementation/T152-PROPER-GIT-PROTOCOL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# T152: Implement Proper Git Protocol Solution (Production Architecture)

**Date**: 2026-03-22
**Status**: 📝 PLANNED (Deferred to post-MVP)
**Date Updated**: 2026-05-09
**Status**: 🚫 SUPERSEDED by 004 (gRPC git service)
**Priority**: MEDIUM (not blocking MVP, but needed for production)

---
Expand All @@ -22,7 +22,7 @@ api:

**How it works**:
- Both git-server and API mount same volume
- API reads catalog directly from filesystem
- API reads catalogue directly from filesystem
- Fast and simple for single-host deployment

---
Expand All @@ -46,7 +46,7 @@ From T145 investigation:

> **Option 1: Git Protocol (Network-Based)** ✅ RECOMMENDED
> - API clones from `git://git-server:9418/catalog.git`
> - Requires implementing clone logic in catalog loader
> - Requires implementing clone logic in catalogue loader
> - True microservices architecture
> - Works in distributed deployment

Expand Down Expand Up @@ -89,7 +89,7 @@ From T145 investigation:

### Changes Required

#### 1. Update API Catalog Loader
#### 1. Update API Catalogue Loader

**File**: `api/internal/catalog/loader.go`

Expand Down Expand Up @@ -367,7 +367,7 @@ docker compose up --scale api=3

### Phase 1: Make Code Support Both (Backward Compatible)

1. Update catalog loader to detect URL type
1. Update catalogue loader to detect URL type
2. Support both filesystem and git protocol
3. Test with filesystem path (current setup)
4. Test with git protocol
Expand Down
25 changes: 18 additions & 7 deletions gitstore-api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,25 @@ GITSTORE_AUTH_JWT_SECRET= # min 32 chars random string
# ── Required with defaults ────────────────────────────────────────────────────

# Git service connection (default: localhost ports of gitstore-git-service)
GITSTORE_GIT_GRPC=localhost:50051 # string — git service gRPC address
GITSTORE_GIT_WS=ws://localhost:8080 # string — git service WebSocket URL
GITSTORE_GIT_HTTP_URL=http://localhost:9418 # string — git service HTTP URL
GITSTORE_GIT_GRPC=localhost:50051 # string — git service gRPC address
GITSTORE_GIT_WS=ws://localhost:8080 # string — git service WebSocket URL
GITSTORE_GIT_HTTP_URL=http://localhost:9418 # string — git service HTTP URL

# ── Optional ──────────────────────────────────────────────────────────────────

GITSTORE_API_PORT=4000 # int — HTTP API listen port
GITSTORE_CACHE_TTL=300 # int — catalog cache TTL in seconds
GITSTORE_LOG_LEVEL=info # string — debug | info | warn | error
GITSTORE_API_PORT=4000 # int — HTTP API listen port
GITSTORE_CACHE_TTL=300 # int — catalog cache TTL in seconds
GITSTORE_LOG_LEVEL=info # string — debug | info | warn | error
GITSTORE_AUTH_JWT_DURATION=24h # duration — JWT token lifetime
GITSTORE_AUTH_JWT_ISSUER=gitstore # string — JWT issuer claim
GITSTORE_AUTH_JWT_ISSUER=gitstore # string — JWT issuer claim

# ── Datastore ─────────────────────────────────────────────────────────────────

GITSTORE_DATASTORE_BACKEND=memdb # string — memdb (default) | scylla

# ScyllaDB configuration (only used when GITSTORE_DATASTORE_BACKEND=scylla)
GITSTORE_DATASTORE_SCYLLA_HOSTS=localhost:9042 # string — comma-separated host:port list
GITSTORE_DATASTORE_SCYLLA_KEYSPACE=gitstore # string — keyspace name
GITSTORE_DATASTORE_SCYLLA_USERNAME= # string — authentication username
GITSTORE_DATASTORE_SCYLLA_PASSWORD= # string — authentication password
GITSTORE_DATASTORE_SCYLLA_TLS=false # bool — enable TLS
Loading
Loading