diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdfd429..f5f65e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ permissions: contents: read jobs: - test: - name: Build, vet, and test + lint: + name: Format, vet, and race tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -22,22 +22,58 @@ jobs: - name: Check formatting run: | - unformatted=$(gofmt -l ./cmd ./internal) + unformatted=$(gofmt -l ./cmd ./internal ./ui) if [ -n "$unformatted" ]; then echo "The following files are not gofmt-formatted:" echo "$unformatted" exit 1 fi - - name: Build - run: go build ./... - - name: Vet run: go vet ./... - - name: Test + - name: Test (race) run: go test -race ./... + test: + name: Build and test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Build + run: go build ./... + + - name: Test + run: go test ./... + + cross-compile: + name: Cross-compile all targets + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Build every OS/arch + run: | + for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64; do + echo "building $target" + GOOS=${target%/*} GOARCH=${target#*/} CGO_ENABLED=0 go build -o /dev/null ./cmd/gateway + done + ui: name: Build dashboard runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c13102e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,65 @@ +name: Release + +on: + push: + tags: ["v*"] + +permissions: + contents: write + packages: write + +jobs: + binaries: + name: Release binaries + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: goreleaser/goreleaser-action@v6 + with: + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + docker: + name: Multi-arch Docker image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/metadata-action@v5 + id: meta + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ github.ref_name }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..eb1fd85 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,55 @@ +version: 2 + +project_name: gtc + +before: + hooks: + - go mod tidy + +builds: + - id: gtc + main: ./cmd/gateway + binary: gtc + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + ldflags: + - -s -w -X main.version={{.Version}} + +archives: + - id: gtc + formats: ["tar.gz"] + format_overrides: + - goos: windows + formats: ["zip"] + name_template: >- + {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }} + files: + - README.md + - LICENSE + - config/sinks.example.yaml + +checksum: + name_template: checksums.txt + +changelog: + use: github + filters: + exclude: + - "^docs" + - "^chore" + - "^test" + +release: + footer: | + ## Docker + + ``` + docker pull ghcr.io/emoss08/gtc:{{ .Version }} + ``` diff --git a/CLAUDE.md b/CLAUDE.md index 106e3c1..2f4f7c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,6 +20,12 @@ cd ui && npm ci && npm run build cd ui && npm run dev ``` +The codebase is pure Go (CGO_ENABLED=0) and must keep building for +linux/darwin/windows on amd64/arm64 — CI cross-compiles all six targets and +runs the test suite on Linux, Windows, and macOS. Releases are cut by +pushing a `v*` tag (goreleaser builds the binaries; a multi-arch Docker +image goes to ghcr.io). + ## Delivery Semantics GTC provides **at-least-once** delivery. The WAL position is only confirmed to diff --git a/Dockerfile b/Dockerfile index 34c74e9..dec6369 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,6 @@ -FROM golang:1.25.3-alpine AS builder +# Multi-arch build: docker buildx sets BUILDPLATFORM/TARGETOS/TARGETARCH so +# the (fast, native) builder cross-compiles for the requested target. +FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder WORKDIR /app @@ -9,7 +11,11 @@ RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /gateway ./cmd/gateway +ARG TARGETOS +ARG TARGETARCH +ARG VERSION=dev +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} \ + go build -ldflags="-s -w -X main.version=${VERSION}" -o /gateway ./cmd/gateway FROM alpine:3.21 diff --git a/README.md b/README.md index 77b7d1e..96a8724 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,11 @@ docker compose -f docker-compose-local.yml exec redis \ ### Binary +GTC is a single, pure-Go binary and runs natively on **Linux, macOS, and +Windows** (amd64 and arm64). Grab a prebuilt binary from the +[releases page](https://github.com/emoss08/gtc/releases), pull the multi-arch +Docker image (`ghcr.io/emoss08/gtc`), or build from source: + ```bash go build -o gateway ./cmd/gateway @@ -102,9 +107,19 @@ REDIS_URL="redis://localhost:6379" \ ./gateway ``` +On Windows (PowerShell): + +```powershell +go build -o gateway.exe ./cmd/gateway + +$env:DATABASE_URL = "postgres://postgres:postgres@localhost:5432/mydb?replication=database" +$env:REDIS_URL = "redis://localhost:6379" +.\gateway.exe +``` + That's the whole setup: with no further configuration GTC mirrors every table in the publication to Redis. Add `REDIS_JSON_URL` and/or `MEILISEARCH_URL` to -enable the other sinks. +enable the other sinks. `gateway --version` prints the build version. ### PostgreSQL requirements diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go index d244dfb..062481e 100644 --- a/cmd/gateway/main.go +++ b/cmd/gateway/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log/slog" "os" "strings" @@ -9,11 +10,24 @@ import ( "go.uber.org/fx" ) +// Overridden at release time via -ldflags "-X main.version=...". +var version = "dev" + func main() { + for _, arg := range os.Args[1:] { + if arg == "--version" || arg == "-v" { + fmt.Printf("gtc %s\n", version) + return + } + } + _ = godotenv.Load() + logger := newLogger() + logger.Info("starting GTC", slog.String("version", version)) + fx.New( - fx.Provide(newLogger), + fx.Provide(func() *slog.Logger { return logger }), Module(), ).Run() }