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
4 changes: 4 additions & 0 deletions .github/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
changelog:
exclude:
labels:
- tagpr
56 changes: 56 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Release

on:
push:
branches: [main]
workflow_dispatch:

jobs:
tagpr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: read
outputs:
tag: ${{ steps.tagpr.outputs.tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- id: tagpr
uses: Songmu/tagpr@d1b8138b7a31075141b6cd64103de9485ced7ac9 # v1.20.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

goreleaser:
needs: tagpr
if: needs.tagpr.outputs.tag != ''
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0

- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod

# tagpr creates the release pull request with GITHUB_TOKEN, which by
# design does not trigger workflows, so CI never runs on it. Test the
# tagged commit here instead of publishing an unverified build.
- run: go test ./...

- uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
distribution: goreleaser
# The action is SHA-pinned but the CLI it downloads is not; keep it
# on the v2 line so a future major cannot change release behaviour.
version: "~> v2"
args: --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/eda
/dist
45 changes: 45 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
version: 2

builds:
- main: .
binary: eda
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
# No -X injection: the version lives in main.go and is maintained by
# tagpr, so a released binary reports the same value as `go install`.
ldflags:
- -s -w

archives:
- formats:
- tar.gz
name_template: >-
{{ .ProjectName }}_
{{- .Version }}_
{{- .Os }}_
{{- .Arch }}

checksum:
name_template: "checksums.txt"

# tagpr owns the release notes. It writes them into the draft release from
# the same pull request titles that go into CHANGELOG.md; goreleaser only
# attaches the artifacts to that draft and publishes it.
changelog:
disable: true

release:
github:
owner: masawada
name: eda
use_existing_draft: true
mode: keep-existing
# A rerun after a partially uploaded release must not fail on assets that
# are already there.
replace_existing_artifacts: true
5 changes: 5 additions & 0 deletions .tagpr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[tagpr]
vPrefix = true
releaseBranch = main
release = draft
versionFile = main.go
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ $ eda tree # show where the worktrees diverged from each other
$ eda remove <branch>... # remove worktrees and their branches as pairs
$ eda root # move back to the primary checkout
$ eda status # print current repository, worktree, and branch
$ eda version # print the version of eda
```

`eda switch` resolves in order: an existing worktree, an existing local branch, a remote branch on `origin` (eda creates a tracking branch), and finally a new branch based on the HEAD of the directory you run it in. Switching from inside a worktree therefore stacks the new branch on top of it, like `git switch -c`.
Expand Down
22 changes: 22 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,15 @@ commands:
init - <shell> print the shell integration script (zsh, bash)
hook worktree-create Claude Code WorktreeCreate hook entrypoint (stdin JSON)
hook worktree-remove Claude Code WorktreeRemove hook entrypoint (stdin JSON)
version print the version of eda
`

// version is the current release version, maintained by tagpr. It is the
// single source of truth: tagpr bumps it in the release pull request and
// tags what it reads back, so plain `go build`, `go install` and the
// released binaries all report the same value.
var version = "0.0.0"

func main() {
cwd, err := os.Getwd()
if err != nil {
Expand Down Expand Up @@ -63,6 +70,8 @@ func run(stdin io.Reader, stdout, stderr io.Writer, args []string, cwd string) i
err = cmdInit(stdout, rest)
case "hook":
err = cmdHook(stdin, stdout, stderr, rest, cwd)
case "version":
err = cmdVersion(stdout, rest)
default:
_, _ = fmt.Fprintf(stderr, "eda: unknown command %q\n%s", cmd, usage)
return 2
Expand Down Expand Up @@ -238,6 +247,19 @@ func cmdStatus(stdout io.Writer, args []string, cwd string) error {
return err
}

// cmdVersion reports the version. It deliberately does not load a repository:
// asking a binary what it is must work from anywhere, including outside a
// git checkout.
func cmdVersion(stdout io.Writer, args []string) error {
if len(args) != 0 {
return fmt.Errorf("usage: eda version")
}
if _, err := fmt.Fprintf(stdout, "eda version %s\n", version); err != nil {
return fmt.Errorf("write version: %w", err)
}
return nil
}

// hookInput is the JSON Claude Code writes to worktree hooks. The cwd field
// is the session's current directory. WorktreeCreate sends name (the slug of
// the new worktree) and uses cwd as the base of the new branch, so a subagent
Expand Down
43 changes: 43 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
Expand Down Expand Up @@ -578,6 +579,41 @@ func TestRunHookWorktreeRemoveWithoutCwd(t *testing.T) {
assertRemoved(t, repo, wt, "agent-abc")
}

func TestRunVersion(t *testing.T) {
code, stdout, stderr := runEda(t, t.TempDir(), "", "version")
if code != 0 {
t.Fatalf("exit = %d, want 0; stderr = %q", code, stderr)
}
if want := "eda version " + version + "\n"; stdout != want {
t.Errorf("stdout = %q, want %q", stdout, want)
}
}

func TestRunVersionRejectsArguments(t *testing.T) {
code, stdout, stderr := runEda(t, t.TempDir(), "", "version", "extra")
if code == 0 {
t.Error("version with an argument must fail")
}
if stdout != "" {
t.Errorf("stdout must stay empty on error, got %q", stdout)
}
if !strings.Contains(stderr, "usage") {
t.Errorf("stderr must include usage, got %q", stderr)
}
}

// tagpr rewrites the version constant in the release pull request and reads
// it back at merge time to decide the tag. It writes the bare x.y.z form, so
// a leading "v" would desynchronise file and tag. Leading zeros do the same
// more quietly: tagpr normalises "01.2.3" to the tag v1.2.3 while the binary
// keeps reporting 01.2.3.
func TestVersionIsBareSemver(t *testing.T) {
bareSemver := regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$`)
if !bareSemver.MatchString(version) {
t.Errorf("version = %q, want bare semver without a leading v or leading zeros", version)
}
}

func TestRunUnknownCommand(t *testing.T) {
repo := newTestRepo(t)
code, _, stderr := runEda(t, repo, "", "bogus")
Expand Down Expand Up @@ -634,6 +670,13 @@ func TestRunInitFailsOnBrokenStdout(t *testing.T) {
}
}

func TestRunVersionFailsOnBrokenStdout(t *testing.T) {
var stderr strings.Builder
if code := run(strings.NewReader(""), failWriter{}, &stderr, []string{"version"}, t.TempDir()); code == 0 {
t.Error("a failed version write must fail the command, not report success")
}
}

func TestRunHookWorktreeRemoveKeptReportFails(t *testing.T) {
repo := newTestRepo(t)
ctx, _ := loadRepoWithRoot(t, repo)
Expand Down