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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **GitHub OAuth author verification for `version commit`** (issue #77)
- `version init` now prompts to authenticate via GitHub device flow; verified username stored in `.deepdiffdb/config` (`0o600` permissions, token never persisted)
- `version init --skip-auth` bypasses the prompt for CI/scripted environments
- `version commit` reads verified identity from config and uses `github:<username>` as author automatically; `--author` flag still accepted when no config identity exists (backward compatible)
- Client ID configured via `DEEPDIFFDB_GITHUB_CLIENT_ID` env var or build-time `-ldflags` injection
- New `internal/version/auth.go`: `RunGitHubDeviceFlow`, `LoadIdentity`, `SaveIdentity`, `ResolveClientID`
- 6 new unit tests in `tests/version/auth_test.go`

## [1.1.0] - 2026-04-01

### Added
Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,22 +505,24 @@ deepdiffdb version <subcommand> [flags]
#### Example Workflow

```bash
# Initialise once per project
# Initialise once per project — authenticate with GitHub for verified authorship
deepdiffdb version init
# → GitHub device flow: visit URL, enter code, author stored as github:<username>
# → use --skip-auth in CI or if GitHub auth is not needed

# Commit a baseline snapshot
deepdiffdb version commit --config deepdiffdb.config.yaml --message "V1: baseline" --author "Alice"
# Commit a baseline snapshot (author resolved automatically when authenticated)
deepdiffdb version commit --config deepdiffdb.config.yaml --message "V1: baseline"

# Create a feature branch and switch to it
deepdiffdb version branch feature
deepdiffdb version checkout feature

# After applying schema changes to dev, commit on the feature branch
deepdiffdb version commit --config deepdiffdb.config.yaml --message "V2: add reviews table" --author "Bob"
deepdiffdb version commit --config deepdiffdb.config.yaml --message "V2: add reviews table"

# Switch back to main and commit a hotfix
deepdiffdb version checkout main
deepdiffdb version commit --config deepdiffdb.config.yaml --message "V2: hotfix index" --author "Alice"
deepdiffdb version commit --config deepdiffdb.config.yaml --message "V2: hotfix index"

# Browse history with an ASCII branch graph
deepdiffdb version tree
Expand All @@ -532,7 +534,9 @@ deepdiffdb version diff <hash_v1> <hash_v2>
deepdiffdb version rollback --out rollback.sql <hash_v2>
```

**Storage:** Commits are stored as zlib-compressed objects in `.deepdiffdb/objects/<2-char>/<62-char>` (Git-style fanout, content-addressable by SHA-256). Branch tips live in `.deepdiffdb/refs/heads/<name>`; HEAD is a symbolic ref (`ref: refs/heads/main`). Add `.deepdiffdb/` to your `.gitignore` or commit it to share history with your team.
**Storage:** Commits are stored as zlib-compressed objects in `.deepdiffdb/objects/<2-char>/<62-char>` (Git-style fanout, content-addressable by SHA-256). Branch tips live in `.deepdiffdb/refs/heads/<name>`; HEAD is a symbolic ref (`ref: refs/heads/main`). `.deepdiffdb/config` holds the verified GitHub identity (token never stored). Add `.deepdiffdb/config` to `.gitignore` to keep your identity local; commit the rest to share history with your team.

**Verified authorship:** `version init` authenticates via GitHub device flow. Once authenticated, every commit is attributed to `github:<username>` automatically — no `--author` flag needed. Pass `--author` explicitly when not authenticated.

**Rollback SQL:** Each commit stores full schema snapshots, so rollback SQL can be generated at any time without a live database. Destructive operations (DROP TABLE, DROP COLUMN) are commented out by default — identical safety behaviour to `schema-migrate`.

Expand Down
13 changes: 10 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,18 @@ We release a new version every **Saturday**. Each release includes one or more f
- ~~Store diff history~~ → `version commit` — SHA-256 content-addressable commit objects in `.deepdiffdb/objects/`
- ~~Diff between any two versions~~ → `version diff <h1> <h2>` — schema evolution comparison
- ~~Rollback capabilities~~ → `version rollback <hash>` — driver-aware rollback SQL generation
- `version init` — repository initialisation
- `version log` — full commit history with drift markers
- ~~`version init`~~ — repository initialisation
- ~~`version log`~~ — full commit history with drift markers
- ~~`version branch` / `version checkout` / `version tree`~~ — branching and ASCII graph
- See [Sample 17](https://github.com/iamvirul/deepdiff-db/tree/main/samples/17-git-like-versioning) for end-to-end demo

2. **CI/CD Integration**
2. **GitHub OAuth Author Verification** ✅ **Done (issue #77)**
- ~~`version init` GitHub device flow authentication~~
- ~~`version commit` reads verified `github:<username>` from `.deepdiffdb/config`~~
- ~~Build-time client ID injection via `-ldflags` in `release.yml`~~
- ~~`DEEPDIFFDB_GITHUB_CLIENT_ID` env var override for local use~~

3. **CI/CD Integration**
- GitHub Actions plugin
- GitLab CI integration
- Jenkins plugin
Expand Down
65 changes: 56 additions & 9 deletions cmd/deepdiffdb/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"log/slog"
"os"
"path/filepath"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -1542,10 +1543,13 @@ Subcommands:
checkout <branch> Switch to a branch
tree Show ASCII commit graph for all branches

Flags for init:
--skip-auth Skip GitHub authentication prompt

Flags for commit:
--config Path to config file (default: deepdiffdb.config.yaml)
--message Commit message (required)
--author Author name (default: current user)
--author Author name (used only when not authenticated via GitHub)

Flags for log:
--limit Maximum number of commits to show (default: 20)
Expand All @@ -1563,17 +1567,49 @@ Flags for branch:
func runVersionInit(args []string) error {
fs := flag.NewFlagSet("version init", flag.ContinueOnError)
dir := fs.String("dir", ".", "Directory to initialise (default: current directory)")
skipAuth := fs.Bool("skip-auth", false, "Skip GitHub authentication (use --author flag on commits instead)")
if err := fs.Parse(args); err != nil {
return err
}
if vcs.IsInitialized(*dir) {
alreadyInit := vcs.IsInitialized(*dir)
if alreadyInit {
fmt.Println("Version repository already initialised.")
} else {
if err := vcs.Init(*dir); err != nil {
return err
}
fmt.Printf("Initialised version repository in %s/%s\n", *dir, vcs.RepoDirName)
}

if *skipAuth {
fmt.Println("Skipping GitHub authentication. Use --author on version commit to set your name.")
return nil
}
if err := vcs.Init(*dir); err != nil {
return err

clientID := vcs.ResolveClientID()
if clientID == "" {
fmt.Println("\nNote: GitHub authentication unavailable (DEEPDIFFDB_GITHUB_CLIENT_ID not set).")
fmt.Println(" Use --author on version commit to identify yourself.")
return nil
}

fmt.Print("\nAuthenticate with GitHub to verify commit authorship? [Y/n]: ")
var answer string
fmt.Scanln(&answer) //nolint:errcheck
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "" && answer != "y" {
fmt.Println("Skipped. Use --author on version commit to set your name.")
return nil
}

username, err := vcs.RunGitHubDeviceFlow(clientID)
if err != nil {
return fmt.Errorf("GitHub authentication failed: %w", err)
}
fmt.Printf("Initialised version repository in %s/%s\n", *dir, vcs.RepoDirName)
if err := vcs.SaveIdentity(*dir, username); err != nil {
return fmt.Errorf("saving identity: %w", err)
}
fmt.Printf("Authenticated as github:%s — your commits will be signed automatically.\n", username)
return nil
}

Expand Down Expand Up @@ -1668,11 +1704,22 @@ func runVersionCommit(args []string) error {
return fmt.Errorf("read HEAD: %w", err)
}

authorName := *author
if authorName == "" {
authorName = os.Getenv("USER")
// Resolve author: verified GitHub identity takes priority over the --author flag.
// If neither is set, fall back to $USER for convenience. Unauthenticated commits
// are allowed but the author is clearly unverified.
authorName, err := vcs.LoadIdentity(dir)
if err != nil {
return fmt.Errorf("loading identity: %w", err)
}
if authorName != "" {
authorName = "github:" + authorName
if *author != "" {
fmt.Fprintf(os.Stderr, "Warning: --author flag ignored; using verified identity %s\n", authorName)
}
} else {
authorName = *author
if authorName == "" {
authorName = "unknown"
return fmt.Errorf("no author set: authenticate with 'version init' or pass --author <name>")
}
}

Expand Down
Loading
Loading