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

on:
push:
tags:
- "v*"

permissions:
contents: write

jobs:
build:
name: Build ${{ matrix.artifact }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- goos: darwin
goarch: amd64
artifact: workmem-darwin-amd64
archive_ext: tar.gz
- goos: darwin
goarch: arm64
artifact: workmem-darwin-arm64
archive_ext: tar.gz
- goos: linux
goarch: amd64
artifact: workmem-linux-amd64
archive_ext: tar.gz
- goos: linux
goarch: arm64
artifact: workmem-linux-arm64
archive_ext: tar.gz
- goos: windows
goarch: amd64
artifact: workmem-windows-amd64
binary_ext: .exe
archive_ext: zip
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build binary with version ldflags
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0
run: |
set -euo pipefail
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
LDFLAGS="-s -w -X main.version=${GITHUB_REF_NAME} -X main.commit=${SHORT_SHA} -X main.buildDate=${BUILD_DATE}"
mkdir -p dist
go build -ldflags "${LDFLAGS}" -o "dist/workmem${{ matrix.binary_ext }}" ./cmd/workmem
- name: Package archive
id: package
env:
REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
# The top-level directory inside the archive includes the version
# suffix so `tar -xzf ...` produces `workmem-<os-arch>-<ver>/`,
# matching the extraction path documented in README Install.
PACKAGE_DIR="${{ matrix.artifact }}-${REF_NAME}"
STAGING_DIR="staging/${PACKAGE_DIR}"
mkdir -p "$STAGING_DIR"
cp "dist/workmem${{ matrix.binary_ext }}" "$STAGING_DIR/"
cp LICENSE README.md "$STAGING_DIR/"
ARCHIVE="${PACKAGE_DIR}.${{ matrix.archive_ext }}"
if [ "${{ matrix.archive_ext }}" = "zip" ]; then
(cd staging && zip -r "../$ARCHIVE" "$PACKAGE_DIR")
else
tar -C staging -czf "$ARCHIVE" "$PACKAGE_DIR"
fi
echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}-archive
path: ${{ steps.package.outputs.archive }}
if-no-files-found: error

release:
name: Publish GitHub release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
pattern: "*-archive"
merge-multiple: true
- name: Generate SHA256SUMS
run: |
set -euo pipefail
cd artifacts
sha256sum * > SHA256SUMS
cat SHA256SUMS
- name: Detect pre-release
id: prerelease
env:
REF_NAME: ${{ github.ref_name }}
run: |
# Tags ending in -rc, -alpha, -beta, -pre, or -dev, optionally
# followed by digits (for example v0.1.0-rc1), are published as
# pre-releases. The $ anchor prevents false positives like
# "v1.0.0-preview" containing "-pre" in the middle of the tag.
if [[ "${REF_NAME}" =~ -(rc|alpha|beta|pre|dev)[0-9]*$ ]]; then
echo "flag=--prerelease" >> "$GITHUB_OUTPUT"
else
echo "flag=" >> "$GITHUB_OUTPUT"
fi
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REF_NAME: ${{ github.ref_name }}
PRERELEASE_FLAG: ${{ steps.prerelease.outputs.flag }}
run: |
set -euo pipefail
gh release create "${REF_NAME}" \
--repo "${{ github.repository }}" \
--title "${REF_NAME}" \
--generate-notes \
${PRERELEASE_FLAG} \
artifacts/*
10 changes: 5 additions & 5 deletions IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ Brief description. **Gate:** the Go binary exposes the parity tool surface over

### Step 3.3: Release pipeline [🔧]

Brief description. **Gate:** release artifacts exist for major OS targets and install flow is simpler than the Node baseline.
Ship workmem as a single binary users can install without `go build`. **Gate:** tagging `vX.Y.Z` on main produces a GitHub release with cross-platform archives + SHA256SUMS, a Homebrew tap formula resolves to those archives, and a fresh-machine walkthrough of each install path succeeds end-to-end.

- [x] Add CI cross-builds
- [ ] Produce release binaries
- [ ] Draft Homebrew strategy
- [ ] Validate install path with fresh-machine assumptions
- [x] Add CI cross-builds (`.github/workflows/go-ci.yml` matrix over darwin amd64+arm64, linux amd64+arm64, windows amd64 — no windows/arm64 yet)
- [x] Produce release binaries (`.github/workflows/release.yml` tag-triggered, 5 platforms, tarball+zip, SHA256SUMS, `-ldflags` inject `version`/`commit`/`buildDate` for `workmem version`)
- [ ] Draft Homebrew strategy (tap repo `marlian/homebrew-tap` with `Formula/workmem.rb` resolving to the release tarball by OS/arch, SHA256 verified)
- [ ] Validate install path with fresh-machine assumptions (`brew install`, `curl | tar | install`, `go install` all succeed on macOS+Linux starting from a clean shell)

**On Step Gate (all items [x]):** trigger release readiness review.

Expand Down
49 changes: 46 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,58 @@ LLMs forget everything between sessions. System prompts can't hold your project'

## Install

Download the binary for your platform from [releases](https://github.com/marlian/workmem/releases), or build from source:
### Homebrew (macOS / Linux)

```bash
brew tap marlian/tap
brew install workmem
```

The tap is published once the first tagged release exists; until then use one of the methods below.

### Direct download

Download the archive for your platform from [releases](https://github.com/marlian/workmem/releases) and extract. Each archive contains the `workmem` binary plus `LICENSE` and `README.md`:

```bash
# pick the archive that matches your OS/arch, e.g. darwin-arm64 / linux-amd64
VER=v0.1.0
curl -LO "https://github.com/marlian/workmem/releases/download/${VER}/workmem-darwin-arm64-${VER}.tar.gz"
tar -xzf workmem-darwin-arm64-${VER}.tar.gz
sudo install workmem-darwin-arm64-${VER}/workmem /usr/local/bin/workmem
workmem version
```

For integrity, download `SHA256SUMS` from the release page and verify only the archive you actually fetched, using the checksum tool that ships with your platform:

```bash
curl -LO "https://github.com/marlian/workmem/releases/download/${VER}/SHA256SUMS"

# macOS
grep "workmem-darwin-arm64-${VER}" SHA256SUMS | shasum -a 256 -c

# Linux
grep "workmem-linux-amd64-${VER}" SHA256SUMS | sha256sum -c
```

On macOS, Gatekeeper will warn on first launch of an unsigned binary downloaded this way. Remove the quarantine attribute with `xattr -d com.apple.quarantine /usr/local/bin/workmem`, or install via Homebrew (which does not trigger the warning).

### Build from source

```bash
git clone https://github.com/marlian/workmem.git
cd workmem
go build -o workmem ./cmd/workmem
./workmem version # prints "workmem dev" without -ldflags
```

Or let Go fetch and install directly:

```bash
go install github.com/marlian/workmem/cmd/workmem@latest
```

No runtime. No dependencies. One file.
Go 1.26+ is required (see `go.mod` for the exact minimum). No CGO and no external runtime dependencies — the result is a single self-contained binary. Source builds report `workmem dev` for version; tagged release binaries carry the real `vX.Y.Z` plus commit SHA and build timestamp.

## Client configuration

Expand Down Expand Up @@ -237,4 +280,4 @@ Only the global memory DB is included. Project-scoped DBs live in their own work

## License

MIT — see [LICENSE](LICENSE) for the full text. — see [LICENSE](LICENSE) for the full text.
MIT — see [LICENSE](LICENSE) for the full text.
32 changes: 29 additions & 3 deletions cmd/workmem/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"os/signal"
"runtime"
"syscall"
Comment thread
marlian marked this conversation as resolved.

"workmem/internal/backup"
Expand All @@ -15,7 +16,24 @@ import (
"workmem/internal/telemetry"
)

// Build metadata overridden at link time via -ldflags "-X main.version=..."
// in the release workflow. Defaults keep `go build` and `go run` usable
// without extra flags and clearly flag unreleased binaries as dev builds.
var (
version = "dev"
commit = "none"
buildDate = "unknown"
)

func main() {
if len(os.Args) >= 2 {
switch os.Args[1] {
case "version", "--version", "-v":
printVersion()
return
}
}

if len(os.Args) < 2 {
runMCP(nil)
return
Expand All @@ -38,6 +56,13 @@ func main() {
}
}

func printVersion() {
fmt.Printf("workmem %s\n", version)
fmt.Printf(" commit: %s\n", commit)
fmt.Printf(" built: %s\n", buildDate)
fmt.Printf(" go: %s\n", runtime.Version())
}

// recipientFlag collects repeatable --age-recipient arguments.
type recipientFlag []string

Expand Down Expand Up @@ -106,7 +131,7 @@ func runMCP(args []string) {
// New returns successfully. If New fails, the DB was already opened by
// FromEnv and must be closed here — otherwise the handle leaks.
tele := telemetry.FromEnv()
runtime, err := mcpserver.New(mcpserver.Config{
rt, err := mcpserver.New(mcpserver.Config{
DBPath: *dbPath,
Telemetry: tele,
})
Expand All @@ -119,7 +144,7 @@ func runMCP(args []string) {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

if err := runtime.RunStdio(ctx); err != nil {
if err := rt.RunStdio(ctx); err != nil {
fmt.Fprintf(os.Stderr, "mcp server failed: %v\n", err)
os.Exit(1)
}
Expand Down Expand Up @@ -169,7 +194,8 @@ func printUsage() {
fmt.Fprintf(os.Stderr, "commands:\n")
fmt.Fprintf(os.Stderr, " serve run the MCP server over stdio (default)\n")
fmt.Fprintf(os.Stderr, " sqlite-canary prove schema init, FTS insert/match/delete, and persistence\n")
fmt.Fprintf(os.Stderr, " backup write an age-encrypted snapshot of memory.db\n\n")
fmt.Fprintf(os.Stderr, " backup write an age-encrypted snapshot of memory.db\n")
fmt.Fprintf(os.Stderr, " version print build metadata (also: --version / -v)\n\n")
fmt.Fprintf(os.Stderr, "flags (serve, sqlite-canary, backup):\n")
fmt.Fprintf(os.Stderr, " -db <path> path to the SQLite database file\n")
fmt.Fprintf(os.Stderr, " -env-file <path> load variables from a .env file (process env takes precedence)\n\n")
Expand Down
Loading