From 4cb1cd41c08679628e81c5363675c04af0bc61ba Mon Sep 17 00:00:00 2001 From: marlian <45340800+marlian@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:01:29 +0200 Subject: [PATCH 1/4] feat: tag-triggered release pipeline + workmem version command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ships the release side of Step 3.3: tagging vX.Y.Z on main produces a GitHub release with cross-platform archives + SHA256SUMS, and every binary self-identifies via `workmem version`. Changes: - cmd/workmem: add `version`/`--version`/`-v` command. Build metadata (version, commit, buildDate) lives in package-level vars overridden at link time via `-ldflags -X main.version=...`. Source builds and `go install` report "workmem dev"; tagged release binaries report the real vX.Y.Z + 7-char commit SHA + ISO-8601 build timestamp. - .github/workflows/release.yml: new workflow triggered on `v*` tag push. Matrix builds 5 targets (darwin/linux amd64+arm64, windows amd64), packages each as tar.gz (unix) or zip (windows) with the binary + LICENSE + README inside a top-level directory, uploads to a per-matrix artifact. Second job downloads all archives, generates SHA256SUMS, detects pre-release suffixes (rc/alpha/beta/ pre/dev) and calls `gh release create --generate-notes` to publish. - README.md: rewrite Install section with three paths — Homebrew tap (placeholder until the tap repo exists), direct download with SHA256SUMS verification + Gatekeeper note, build from source incl. `go install`. Fix a duplicated License line. - IMPLEMENTATION.md: Step 3.3 items refreshed — CI cross-builds and release binaries now [x]; Homebrew tap and fresh-machine validation remain [ ] pending the first actual tag + tap repo creation. --- .github/workflows/release.yml | 123 ++++++++++++++++++++++++++++++++++ IMPLEMENTATION.md | 10 +-- README.md | 44 +++++++++++- cmd/workmem/main.go | 28 +++++++- 4 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0cc9f79 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,123 @@ +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 + STAGING_DIR="staging/${{ matrix.artifact }}" + mkdir -p "$STAGING_DIR" + cp "dist/workmem${{ matrix.binary_ext }}" "$STAGING_DIR/" + cp LICENSE README.md "$STAGING_DIR/" + ARCHIVE="${{ matrix.artifact }}-${REF_NAME}.${{ matrix.archive_ext }}" + if [ "${{ matrix.archive_ext }}" = "zip" ]; then + (cd staging && zip -r "../$ARCHIVE" "${{ matrix.artifact }}") + else + tar -C staging -czf "$ARCHIVE" "${{ matrix.artifact }}" + 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 matching v*-rc*, v*-alpha*, v*-beta*, v*-pre*, v*-dev* are + # published as pre-releases. Everything else ships as stable. + if [[ "${REF_NAME}" =~ -(rc|alpha|beta|pre|dev) ]]; 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/* diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 8a634da..3f0da27 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -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/linux/windows × amd64/arm64) +- [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. diff --git a/README.md b/README.md index 016393b..501f557 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,53 @@ 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 before installing: + +```bash +curl -LO "https://github.com/marlian/workmem/releases/download/${VER}/SHA256SUMS" +shasum -a 256 -c SHA256SUMS --ignore-missing +``` + +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, no runtime dependencies — the result is a single static 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 @@ -237,4 +275,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. diff --git a/cmd/workmem/main.go b/cmd/workmem/main.go index 63bc93e..cbbbc36 100644 --- a/cmd/workmem/main.go +++ b/cmd/workmem/main.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "os/signal" + "runtime" "syscall" "workmem/internal/backup" @@ -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 @@ -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 @@ -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 to the SQLite database file\n") fmt.Fprintf(os.Stderr, " -env-file load variables from a .env file (process env takes precedence)\n\n") From 026c9e6c64c7a85c56b3d5dd62a7449706f0f20f Mon Sep 17 00:00:00 2001 From: marlian <45340800+marlian@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:08:30 +0200 Subject: [PATCH 2/4] review: address Copilot round 1 feedback on PR #7 Two real catches: - cmd/workmem: the local variable holding *mcpserver.Runtime in runMCP was named `runtime`, which shadows the stdlib `runtime` package now imported for `runtime.Version()`. Rename to `rt` so the shadowing is gone and future readers aren't confused about which `runtime` is in scope. - .github/workflows/release.yml: the top-level directory inside the tar.gz/zip was `workmem-` but the README install commands extract into `workmem--vX.Y.Z/`. Align the packaging so the dir inside the archive carries the version suffix too. Archive filename was already versioned, only the inner dir was out of sync. --- .github/workflows/release.yml | 12 ++++++++---- cmd/workmem/main.go | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0cc9f79..e745f8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,15 +60,19 @@ jobs: REF_NAME: ${{ github.ref_name }} run: | set -euo pipefail - STAGING_DIR="staging/${{ matrix.artifact }}" + # The top-level directory inside the archive includes the version + # suffix so `tar -xzf ...` produces `workmem--/`, + # 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="${{ matrix.artifact }}-${REF_NAME}.${{ matrix.archive_ext }}" + ARCHIVE="${PACKAGE_DIR}.${{ matrix.archive_ext }}" if [ "${{ matrix.archive_ext }}" = "zip" ]; then - (cd staging && zip -r "../$ARCHIVE" "${{ matrix.artifact }}") + (cd staging && zip -r "../$ARCHIVE" "$PACKAGE_DIR") else - tar -C staging -czf "$ARCHIVE" "${{ matrix.artifact }}" + tar -C staging -czf "$ARCHIVE" "$PACKAGE_DIR" fi echo "archive=$ARCHIVE" >> "$GITHUB_OUTPUT" - uses: actions/upload-artifact@v4 diff --git a/cmd/workmem/main.go b/cmd/workmem/main.go index cbbbc36..d12d5a1 100644 --- a/cmd/workmem/main.go +++ b/cmd/workmem/main.go @@ -131,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, }) @@ -144,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) } From ee93b8c480302d251503b030965a6e81e45582b8 Mon Sep 17 00:00:00 2001 From: marlian <45340800+marlian@users.noreply.github.com> Date: Thu, 16 Apr 2026 06:01:28 +0200 Subject: [PATCH 3/4] review: address Copilot round 2 feedback on PR #7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README: replace "single static binary" with "single self-contained binary, no external runtime dependencies". Go binaries with CGO_ENABLED=0 still link dynamically against libSystem on macOS and kernel32 on Windows; "static" was technically inaccurate. - README: the `shasum --ignore-missing` flag does not exist on macOS shasum (only on GNU sha256sum). Switch the verification recipe to `grep "" SHA256SUMS | shasum -a 256 -c`, which only verifies the archive the user actually downloaded and works identically on macOS and Linux without GNU-specific flags. - IMPLEMENTATION.md: the Step 3.3 CI cross-builds description read "darwin/linux/windows × amd64/arm64" but the matrix has no windows/arm64 target. Replace with the actual 5-target list to keep the doc honest about what ships. --- IMPLEMENTATION.md | 2 +- README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index 3f0da27..c0dc6f7 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -91,7 +91,7 @@ Brief description. **Gate:** the Go binary exposes the parity tool surface over 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 (`.github/workflows/go-ci.yml` matrix over darwin/linux/windows × amd64/arm64) +- [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) diff --git a/README.md b/README.md index 501f557..ab1b73c 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,11 @@ sudo install workmem-darwin-arm64-${VER}/workmem /usr/local/bin/workmem workmem version ``` -For integrity, download `SHA256SUMS` from the release page and verify before installing: +For integrity, download `SHA256SUMS` from the release page and verify only the archive you actually fetched. This works identically on macOS (`shasum`) and Linux (`sha256sum`) without relying on GNU-specific flags: ```bash curl -LO "https://github.com/marlian/workmem/releases/download/${VER}/SHA256SUMS" -shasum -a 256 -c SHA256SUMS --ignore-missing +grep "workmem-darwin-arm64-${VER}" SHA256SUMS | shasum -a 256 -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). @@ -62,7 +62,7 @@ Or let Go fetch and install directly: go install github.com/marlian/workmem/cmd/workmem@latest ``` -Go 1.26+ is required (see `go.mod` for the exact minimum). No CGO, no runtime dependencies — the result is a single static binary. Source builds report `workmem dev` for version; tagged release binaries carry the real `vX.Y.Z` plus commit SHA and build timestamp. +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 From 0b7083b5b6a44d720d27616734b8cfccebb0d933 Mon Sep 17 00:00:00 2001 From: marlian <45340800+marlian@users.noreply.github.com> Date: Thu, 16 Apr 2026 06:15:45 +0200 Subject: [PATCH 4/4] review: address Copilot round 3 feedback on PR #7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README: the single `shasum -a 256 -c` recipe does not work out of the box on Linux — many distros ship `sha256sum` (GNU coreutils) but not `shasum` (Perl-based BSD/macOS). Split the example into two blocks so the macOS verification uses `shasum` and the Linux one uses `sha256sum`, with archive names matching the typical platform for each. - release.yml: the pre-release detection regex `-(rc|alpha|beta|pre| dev)` matched the suffix anywhere in the tag name, which would treat `v1.0.0-preview` as a pre-release because "-pre" appears in the middle. Anchor to end-of-string with `$` and allow optional trailing digits (e.g. `-rc1`, `-beta2`) so only genuinely pre-release suffixes trigger the flag. --- .github/workflows/release.yml | 8 +++++--- README.md | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e745f8f..bf5fa10 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,9 +105,11 @@ jobs: env: REF_NAME: ${{ github.ref_name }} run: | - # Tags matching v*-rc*, v*-alpha*, v*-beta*, v*-pre*, v*-dev* are - # published as pre-releases. Everything else ships as stable. - if [[ "${REF_NAME}" =~ -(rc|alpha|beta|pre|dev) ]]; then + # 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" diff --git a/README.md b/README.md index ab1b73c..778e9af 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,16 @@ 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. This works identically on macOS (`shasum`) and Linux (`sha256sum`) without relying on GNU-specific flags: +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).