Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8915b71
Add hybrid roadmap and Go CLI bridge foundation
cursoragent Apr 7, 2026
36109dd
Fix wrapper smoke test edge cases and Java build config
cursoragent Apr 7, 2026
776d38b
Add reverse spec-kit scaffold and conformance checks
cursoragent Apr 7, 2026
223fc2b
Normalize to canonical spec-kit enforcement and CI gates
cursoragent Apr 7, 2026
199142a
Fix CLI stream deadlock and stdout-only git queries
cursoragent Apr 7, 2026
c2cf9b0
Address PR review findings and harden bridge behavior
cursoragent Apr 7, 2026
8c1a543
Fix CLI bridge JSON parsing and branch helper consistency
cursoragent Apr 7, 2026
b70b94c
Fix JSON escaping and always emit snapshotSize
cursoragent Apr 7, 2026
0dd9a6c
Resolve remaining PR feedback on CLI JSON responses
cursoragent Apr 8, 2026
59b16b4
Add runnable Python/Java smoke samples and CI validation
cursoragent Apr 8, 2026
65dcb89
Resolve PR feedback: path safety and portability
cursoragent Apr 8, 2026
ca494d5
Make Java parser tests use in-memory CLI stub
cursoragent Apr 8, 2026
bb70df4
Normalize Java sample workspace root path
cursoragent Apr 8, 2026
9763225
Enhance Java wrapper API with typed repo options
cursoragent Apr 8, 2026
3af8349
Harden bridge portability and expand CI OS coverage
cursoragent Apr 8, 2026
58969a6
Resolve latest PR feedback on bridge robustness
cursoragent Apr 8, 2026
aee8e65
Harden Java sample cleanup for Windows file locks
cursoragent Apr 8, 2026
112878b
Make Java sample cleanup tolerant to Windows locks
cursoragent Apr 8, 2026
054f0b3
Use TempDir in Java sample to avoid temp leaks
cursoragent Apr 8, 2026
087e484
Restore symlink entries in snapshot roundtrips
cursoragent Apr 8, 2026
b87d1d8
Add gofmt check to CI test job
cursoragent Apr 8, 2026
366c69e
Fix snapshot archive/restore entry symmetry
cursoragent Apr 8, 2026
372011c
Optimize CI wrapper tests with prebuilt CLI
cursoragent Apr 8, 2026
33df51f
Fix wrapper CI CLI path after directory changes
cursoragent Apr 8, 2026
347c4e8
Fix wrapper CLI path resolution across CI jobs
cursoragent Apr 8, 2026
6cc6ae3
fix: apply CodeRabbit auto-fixes
coderabbitai[bot] Apr 8, 2026
21de822
Fix CodeRabbit Java lambda compile regression
cursoragent Apr 8, 2026
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
115 changes: 115 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,63 @@ on:
pull_request:

jobs:
spec-kit-conformance:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Validate spec-kit artifact set
run: |
test -f testkit/.specify/memory/constitution.md
test -f testkit/.specify/specs/001-polyglot-testkit/spec.md
test -f testkit/.specify/specs/001-polyglot-testkit/plan.md
test -f testkit/.specify/specs/001-polyglot-testkit/tasks.md
test -f testkit/.specify/specs/001-polyglot-testkit/contracts/cli-protocol.json
test -f testkit/.specify/specs/001-polyglot-testkit/checklists/quality.md

- name: Validate spec-kit scaffold and status
run: ./testkit/.specify/scripts/validate_specify.sh

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "stable"
- name: Build git-testkit CLI binary once
run: go build ./cmd/git-testkit-cli

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven

- name: Run Python spec conformance smoke tests
run: |
cd testkit/python
python -m pip install -e ".[dev]"
python -m pytest tests/ -v
- name: Run Python sample smoke implementations
run: |
cd testkit/python
python -m samples.smoke_repo_flow
python -m samples.smoke_snapshot_flow

- name: Run Java spec conformance smoke tests
run: |
cd testkit/java
mvn test
Comment thread
cursor[bot] marked this conversation as resolved.
- name: Run Java sample smoke implementations
run: |
cd testkit/java
mvn -Dtest=SampleRepoFlowSmoke,SampleSnapshotFlowSmoke test

test:
runs-on: ubuntu-latest
strategy:
Expand All @@ -26,5 +83,63 @@ jobs:
- name: Run vet
run: go vet ./...

- name: Check gofmt
run: test -z "$(gofmt -l .)"

- name: Run tests
run: go test ./...

wrapper-cross-platform:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "stable"

- name: Build git-testkit CLI binary once
shell: bash
run: |
mkdir -p bin
if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
go build -o ./bin/git-testkit-cli.exe ./cmd/git-testkit-cli
else
go build -o ./bin/git-testkit-cli ./cmd/git-testkit-cli
fi

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven

- name: Run Python wrapper smoke tests
shell: bash
env:
GIT_TESTKIT_CLI: ${{ matrix.os == 'windows-latest' && './bin/git-testkit-cli.exe' || './bin/git-testkit-cli' }}
run: |
cd testkit/python
python -m pip install -e ".[dev]"
python -m pytest tests/test_fixtures.py tests/test_snapshots.py -v

- name: Run Java wrapper smoke tests
shell: bash
env:
GIT_TESTKIT_CLI: ${{ matrix.os == 'windows-latest' && './bin/git-testkit-cli.exe' || './bin/git-testkit-cli' }}
run: |
cd testkit/java
mvn -Dtest=CliBridgeTest,SampleRepoFlowSmoke,SampleSnapshotFlowSmoke test
266 changes: 266 additions & 0 deletions cmd/git-testkit-cli/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
package main

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

testutil "github.com/git-fire/git-testkit"
)

type request struct {
Op string `json:"op"`
BaseDir string `json:"baseDir,omitempty"`
RepoPath string `json:"repoPath,omitempty"`
Args []string `json:"args,omitempty"`
Options *repoOptionsInput `json:"options,omitempty"`

SnapshotPath string `json:"snapshotPath,omitempty"`
}

type repoOptionsInput struct {
Name string `json:"name"`
Dirty bool `json:"dirty,omitempty"`
Files map[string]string `json:"files,omitempty"`
Remotes map[string]string `json:"remotes,omitempty"`
Branches []string `json:"branches,omitempty"`
InitialCommit string `json:"initialCommit,omitempty"`
}

type response struct {
OK bool `json:"ok"`

Error string `json:"error,omitempty"`

RepoPath string `json:"repoPath,omitempty"`
RemotePath string `json:"remotePath,omitempty"`
FSRoot string `json:"fsRoot,omitempty"`
Output *string `json:"output,omitempty"`
Dirty *bool `json:"dirty,omitempty"`
Remotes map[string]string `json:"remotes,omitempty"`
SHA string `json:"sha,omitempty"`
Branches []string `json:"branches,omitempty"`
SnapshotName string `json:"snapshotName,omitempty"`
SnapshotSize *int `json:"snapshotSize,omitempty"`
RestorePath string `json:"restorePath,omitempty"`
}

func main() {
req, err := parseRequest()
if err != nil {
writeResponse(response{OK: false, Error: err.Error()})
os.Exit(1)
}

res, err := handle(req)
if err != nil {
writeResponse(response{OK: false, Error: err.Error()})
os.Exit(1)
}
writeResponse(res)
}

func parseRequest() (request, error) {
var req request
if err := json.NewDecoder(os.Stdin).Decode(&req); err != nil {
return request{}, fmt.Errorf("invalid JSON request: %w", err)
}
if strings.TrimSpace(req.Op) == "" {
return request{}, fmt.Errorf("missing required field: op")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return req, nil
}

func handle(req request) (response, error) {
switch req.Op {
case "create_test_repo":
base, err := ensureBaseDir(req.BaseDir)
if err != nil {
return response{}, err
}
if req.Options == nil {
return response{}, fmt.Errorf("missing options")
}
repoPath, err := testutil.CreateTestRepoInDir(base, testutil.RepoOptions{
Name: req.Options.Name,
Dirty: req.Options.Dirty,
Files: req.Options.Files,
Remotes: req.Options.Remotes,
Branches: req.Options.Branches,
InitialCommit: req.Options.InitialCommit,
})
if err != nil {
return response{}, err
}
return response{OK: true, RepoPath: repoPath}, nil

case "create_bare_remote":
base, err := ensureBaseDir(req.BaseDir)
if err != nil {
return response{}, err
}
if req.Options == nil || req.Options.Name == "" {
return response{}, fmt.Errorf("missing options.name")
}
remotePath, err := testutil.CreateBareRemoteInDir(base, req.Options.Name)
if err != nil {
return response{}, err
}
return response{OK: true, RemotePath: remotePath}, nil

case "setup_fake_filesystem":
base, err := ensureBaseDir(req.BaseDir)
if err != nil {
return response{}, err
}
root, err := testutil.SetupFakeFilesystemInDir(base)
if err != nil {
return response{}, err
}
return response{OK: true, FSRoot: root}, nil

case "run_git_cmd":
if req.RepoPath == "" {
return response{}, fmt.Errorf("missing repoPath")
}
output, err := testutil.RunGitCmdE(req.RepoPath, req.Args...)
if err != nil {
return response{}, err
}
return response{OK: true, Output: stringPtr(output)}, nil

case "is_dirty":
if req.RepoPath == "" {
return response{}, fmt.Errorf("missing repoPath")
}
dirty, err := testutil.IsDirtyE(req.RepoPath)
if err != nil {
return response{}, err
}
return response{OK: true, Dirty: &dirty}, nil

case "get_remotes":
if req.RepoPath == "" {
return response{}, fmt.Errorf("missing repoPath")
}
remotes, err := testutil.GetRemotesE(req.RepoPath)
if err != nil {
return response{}, err
}
return response{OK: true, Remotes: remotes}, nil

case "get_current_sha":
if req.RepoPath == "" {
return response{}, fmt.Errorf("missing repoPath")
}
sha, err := testutil.GetCurrentSHAE(req.RepoPath)
if err != nil {
return response{}, err
}
return response{OK: true, SHA: sha}, nil

case "get_branches":
if req.RepoPath == "" {
return response{}, fmt.Errorf("missing repoPath")
}
branches, err := testutil.GetBranchesE(req.RepoPath)
if err != nil {
return response{}, err
}
return response{OK: true, Branches: branches}, nil

case "snapshot_repo":
if req.RepoPath == "" {
return response{}, fmt.Errorf("missing repoPath")
}
snapshot, err := testutil.SnapshotRepoE(req.RepoPath)
if err != nil {
return response{}, err
}
return response{
OK: true,
SnapshotName: snapshot.Name(),
SnapshotSize: intPtr(snapshot.Size()),
}, nil

case "snapshot_save":
if req.RepoPath == "" || req.SnapshotPath == "" {
return response{}, fmt.Errorf("missing repoPath or snapshotPath")
}
snapshot, err := testutil.SnapshotRepoE(req.RepoPath)
if err != nil {
return response{}, err
}
if err := testutil.SaveSnapshotToDiskE(snapshot, req.SnapshotPath); err != nil {
return response{}, err
}
return response{
OK: true,
SnapshotName: snapshot.Name(),
SnapshotSize: intPtr(snapshot.Size()),
}, nil

case "snapshot_load_restore":
if req.SnapshotPath == "" {
return response{}, fmt.Errorf("missing snapshotPath")
}
base, err := ensureBaseDir(req.BaseDir)
if err != nil {
return response{}, err
}
snapshot, err := testutil.LoadSnapshotFromDiskE(req.SnapshotPath)
if err != nil {
return response{}, err
}
restorePath, err := testutil.RestoreSnapshotToDir(snapshot, base)
if err != nil {
return response{}, err
}
return response{
OK: true,
RestorePath: restorePath,
SnapshotName: snapshot.Name(),
SnapshotSize: intPtr(snapshot.Size()),
}, nil

default:
return response{}, fmt.Errorf("unsupported op: %s", req.Op)
}
}

func ensureBaseDir(baseDir string) (string, error) {
if strings.TrimSpace(baseDir) == "" {
return "", fmt.Errorf("missing baseDir")
}
clean := filepath.Clean(baseDir)
if err := os.MkdirAll(clean, 0755); err != nil {
return "", err
}
return clean, nil
}

func writeResponse(res response) {
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
if err := enc.Encode(res); err != nil {
fallback := response{
OK: false,
Error: fmt.Sprintf("failed writing response: %s", err.Error()),
}
stderrEnc := json.NewEncoder(os.Stderr)
stderrEnc.SetEscapeHTML(false)
if encodeErr := stderrEnc.Encode(fallback); encodeErr != nil {
fmt.Fprintf(os.Stderr, "failed writing fallback response: %v\n", encodeErr)
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func intPtr(v int) *int {
return &v
}

func stringPtr(v string) *string {
return &v
}
Loading
Loading