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
85 changes: 85 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: release

# Pushing a version tag turns the GoReleaser config (.goreleaser.yaml) into a
# GitHub release with the archives, Linux packages (deb, rpm, apk), checksums,
# SBOMs and a cosign signature, and pushes the multi-arch container image to
# GHCR. Pull requests and pushes to main run `goreleaser check` only, so a
# config that would fail a real release is caught long before the tag.

on:
push:
branches: [main]
tags: ["v*"]
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true

jobs:
# Fast gate on every PR and push: the config parses and is valid for the
# installed GoReleaser version. No artifacts are built here.
check:
if: github.ref_type != 'tag'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: "~> v2"
args: check

# The real release, only on a version tag.
release:
if: github.ref_type == 'tag'
runs-on: ubuntu-latest
permissions:
contents: write # create the GitHub release
packages: write # push the image to ghcr.io
id-token: write # keyless cosign signing
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true

# Build and ship the linux/arm64 image from the amd64 runner.
- 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 }}

# Tools GoReleaser shells out to for signing and SBOMs.
- uses: sigstore/cosign-installer@v3
- uses: anchore/sbom-action/download-syft@v0

- uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: "~> v2"
args: release --clean
env:
# Creating the release and pushing the GHCR image use the built-in
# token. The package-manager publish steps each read their own secret;
# any that is unset leaves that manager skipped (the artifact is still
# produced), so the release never fails for a tap that is not set up.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
SCOOP_BUCKET_GITHUB_TOKEN: ${{ secrets.SCOOP_BUCKET_GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
/dbrest
/cmd/dbrest/dbrest

# GoReleaser build output
dist/

# Local databases
*.sqlite
*.sqlite-shm
Expand Down
189 changes: 189 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# GoReleaser turns one tag push into everything a user might install from: raw
# archives, Linux packages (deb, rpm, apk), a multi-arch container image, and
# entries for the package managers (Homebrew, Scoop). `git tag vX.Y.Z && git
# push --tags` fans out to all of them through .github/workflows/release.yml.
#
# Publish steps that push to a repository we do not own yet (the Homebrew tap,
# the Scoop bucket) self-disable when their token is absent. A release with no
# extra secrets still produces every downloadable artifact and the container
# image; each manager lights up the moment its repository and token exist.
version: 2

project_name: dbrest

before:
# Only fetch modules; never `go mod tidy` during a release, the tree is kept
# tidy in CI.
hooks:
- go mod download

builds:
- id: dbrest
binary: dbrest
main: ./cmd/dbrest
env:
- CGO_ENABLED=0
flags:
- -trimpath
ldflags:
- -s -w
- -X main.version={{ .Version }}
- -X main.commit={{ .ShortCommit }}
- -X main.date={{ .CommitDate }}
mod_timestamp: "{{ .CommitTimestamp }}"
targets:
- linux_amd64
- linux_arm64
- linux_arm_7
- linux_386
- darwin_amd64
- darwin_arm64
- windows_amd64
- windows_arm64
- freebsd_amd64
- freebsd_arm64

archives:
# tar.gz everywhere except a zip on Windows.
- id: default
name_template: "dbrest_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}"
format_overrides:
- goos: windows
formats: [zip]
files:
- LICENSE
- README.md

nfpms:
# One nfpm definition emits the deb, rpm, and apk for every Linux build. The
# package is the binary and its license; the database it serves is whatever
# the operator points db-uri at, so there are no package dependencies.
- id: linux-packages
package_name: dbrest
file_name_template: "{{ .ConventionalFileName }}"
vendor: tamnd
homepage: https://github.com/tamnd/dbrest
maintainer: Duc-Tam Nguyen <tamnd87@gmail.com>
description: A PostgREST-compatible REST API for any database.
license: Apache-2.0
formats:
- deb
- rpm
- apk
bindir: /usr/bin
section: utils
contents:
- src: ./LICENSE
dst: /usr/share/doc/dbrest/LICENSE

dockers_v2:
# One multi-platform image built with buildx. GoReleaser stages the prebuilt
# binaries under per-platform directories in the build context and the
# Dockerfile copies the right one through $TARGETPLATFORM.
- images:
- ghcr.io/tamnd/dbrest
tags:
- "{{ .Version }}"
- latest
dockerfile: Dockerfile
platforms:
- linux/amd64
- linux/arm64
labels:
org.opencontainers.image.title: "{{ .ProjectName }}"
org.opencontainers.image.description: "A PostgREST-compatible REST API for any database"
org.opencontainers.image.url: "https://github.com/tamnd/dbrest"
org.opencontainers.image.source: "https://github.com/tamnd/dbrest"
org.opencontainers.image.version: "{{ .Version }}"
org.opencontainers.image.revision: "{{ .FullCommit }}"
org.opencontainers.image.licenses: "Apache-2.0"

homebrew_casks:
# Homebrew cask pushed to the tap repository. Self-disables until
# HOMEBREW_TAP_GITHUB_TOKEN (a PAT with write to tamnd/homebrew-tap) is set,
# so a tokenless release still writes the cask into dist for inspection.
- name: dbrest
repository:
owner: tamnd
name: homebrew-tap
token: '{{ envOrDefault "HOMEBREW_TAP_GITHUB_TOKEN" "" }}'
directory: Casks
homepage: https://github.com/tamnd/dbrest
description: A PostgREST-compatible REST API for any database
skip_upload: '{{ if envOrDefault "HOMEBREW_TAP_GITHUB_TOKEN" "" }}false{{ else }}true{{ end }}'
commit_author:
name: Duc-Tam Nguyen
email: tamnd87@gmail.com

scoops:
# Scoop manifest for Windows, pushed to the bucket repository.
- repository:
owner: tamnd
name: scoop-bucket
token: '{{ envOrDefault "SCOOP_BUCKET_GITHUB_TOKEN" "" }}'
homepage: https://github.com/tamnd/dbrest
description: A PostgREST-compatible REST API for any database
license: Apache-2.0
skip_upload: '{{ if envOrDefault "SCOOP_BUCKET_GITHUB_TOKEN" "" }}false{{ else }}true{{ end }}'
commit_author:
name: Duc-Tam Nguyen
email: tamnd87@gmail.com

checksum:
name_template: "checksums.txt"
algorithm: sha256

sboms:
# A CycloneDX SBOM per archive via syft; the release workflow installs it.
- id: archive
artifacts: archive

signs:
# Keyless cosign signature over the checksum file. It runs only on a real
# release in CI, where the workflow grants the OIDC token cosign needs.
- cmd: cosign
certificate: "${artifact}.pem"
args:
- sign-blob
- "--output-certificate=${certificate}"
- "--output-signature=${signature}"
- "${artifact}"
- "--yes"
artifacts: checksum
output: true

docker_signs:
- cmd: cosign
artifacts: manifests
args:
- sign
- "${artifact}@${digest}"
- "--yes"

changelog:
sort: asc
use: github
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
- "^ci:"
- Merge pull request
- Merge branch
groups:
- title: Features
regexp: '^.*?feat(\(.+\))??!?:.+$'
order: 0
- title: Fixes
regexp: '^.*?fix(\(.+\))??!?:.+$'
order: 1
- title: Other
order: 999

release:
github:
owner: tamnd
name: dbrest
draft: false
prerelease: auto
45 changes: 31 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
# Build the dbrest binary in a Go 1.26 builder, then copy it into a minimal
# Alpine image. The binary is statically linked (CGO_ENABLED=0) so it runs on
# any Linux base image without a matching libc.
FROM docker.io/library/golang:1.26-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /bin/dbrest ./cmd/dbrest

FROM docker.io/library/alpine:3.21
COPY --from=builder /bin/dbrest /usr/local/bin/dbrest
EXPOSE 3001
ENTRYPOINT ["/usr/local/bin/dbrest"]
# Consumed by GoReleaser: it copies the already cross-compiled binary out of the
# build context rather than compiling, so the image build is fast and uses the
# same static binary every other artifact ships.
#
# GoReleaser builds one multi-platform image with buildx and stages each
# platform's binary under a $TARGETPLATFORM directory (e.g. linux/amd64/) in the
# build context, so the COPY line selects the right one through the automatic
# TARGETPLATFORM build arg.
FROM alpine:3.21

ARG TARGETPLATFORM

# ca-certificates for TLS to the database; tzdata for sane timestamps.
RUN apk add --no-cache ca-certificates tzdata \
&& adduser -D -H -u 10001 dbrest

COPY $TARGETPLATFORM/dbrest /usr/bin/dbrest

USER dbrest

# 3000 is the API; 3001 is the admin server (/live, /ready, /metrics) when it is
# enabled. Configure the server with a mounted config file or the DBREST_*
# environment, for example:
#
# docker run -p 3000:3000 \
# -e DBREST_DB_BACKEND=postgres \
# -e DBREST_DB_URI="postgres://web@db/app" \
# ghcr.io/tamnd/dbrest
EXPOSE 3000 3001

ENTRYPOINT ["/usr/bin/dbrest"]
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ Early, and built subsystem by subsystem against a complete design spec. What wor

The capability model, the backend SPI, and the error envelope are in place. The PostgreSQL dialect and its version-computed capabilities have landed (`backend/postgres`), the reference oracle the conformance harness diffs against. The MySQL/MariaDB dialect has landed too (`backend/mysql`), the first real divergence from the oracle: an explicit IS NULL sort key for NULL placement, a no-conflict-target upsert with a no-op ignore, restricted CAST targets, `REGEXP_LIKE`, and MATCH/AGAINST boolean-mode full text. The SQL Server (T-SQL) dialect has landed as well (`backend/sqlserver`), the quirkiest on syntax and the closest to the oracle on the security model: bracket-quoted identifiers, named `@pN` placeholders, OFFSET/FETCH paging that injects an ORDER BY when the client gave none, a CASE NULL sort key, OUTPUT in place of RETURNING, a multi-statement upsert that the data plane drives, and CONTAINS/FREETEXT full text, with native roles, RLS, and a session-context store. Each driver data plane (Execute and introspection over a live server) is a follow-on slice, since it needs a running database to test. The MongoDB backend has landed too (`backend/mongo`), and it is the one engine that does not use the SQL compiler: it lowers a filter to a `$match` query document, a read to a `$match`/`$sort`/`$skip`/`$limit`/`$project` pipeline, casts to `$convert`, and NULLS placement to an `$addFields` sort key, with the array and range operators Unsupported and the security model emulated app-side. Its live driver data plane (`$lookup`/`$graphLookup` embedding, writes, sampling-based introspection) is the follow-on slice. Each backend joins the conformance harness by adding its fixture and a CI job, with no harness changes.

## Install

Every release ships prebuilt binaries, Linux packages, and a container image. Grab the archive for your platform from the [releases page](https://github.com/tamnd/dbrest/releases), or pull the image:

```sh
docker run -p 3000:3000 \
-e DBREST_DB_BACKEND=sqlite \
-e DBREST_DB_URI='file:/data/example.sqlite' \
ghcr.io/tamnd/dbrest
```

On Debian or Red Hat, install the `.deb` or `.rpm` from the release. With the Go toolchain:

```sh
go install github.com/tamnd/dbrest/cmd/dbrest@latest
```

The Homebrew tap and Scoop bucket come online as those repositories are set up. Each release also carries a checksums file, a CycloneDX SBOM per archive, and a keyless cosign signature.

## Quick start

Write a config file naming the backend and the database:
Expand Down
25 changes: 24 additions & 1 deletion cmd/dbrest/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,37 @@ func resolveConfigPath(flagPath string, args []string) (string, error) {
return args[0], nil
}

// versionString is the module version when built with one, "dev" otherwise.
// Build metadata. The release pipeline stamps these with -ldflags -X; an
// unstamped build (go build, go run) leaves the defaults and falls back to the
// module version recorded in the build info.
var (
version = "dev"
commit = "none"
date = "unknown"
)

// versionString is the stamped release version, the module version when built
// from a checkout that carries one, or "dev".
func versionString() string {
if version != "dev" {
return version
}
if bi, ok := debug.ReadBuildInfo(); ok && bi.Main.Version != "" && bi.Main.Version != "(devel)" {
return bi.Main.Version
}
return "dev"
}

// versionLine is the full --version line. It adds the commit and build date
// when the binary was stamped by the release pipeline.
func versionLine() string {
v := "dbrest " + versionString()
if commit != "none" || date != "unknown" {
v += fmt.Sprintf(" (commit %s, built %s)", commit, date)
}
return v
}

// probeReady asks a running instance's admin server whether it is ready, the
// --ready verb orchestrators use as a health command. A non-200 answer or an
// unreachable admin server is an error, which main turns into exit status 1.
Expand Down
Loading
Loading