Skip to content
Closed
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
148 changes: 138 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ jobs:
exit 1
fi

build:
name: Build mem-mcp binaries
build-mcp:
name: Build mem-mcp (${{ matrix.goos }}/${{ matrix.goarch }})
needs: [preflight]
runs-on: ubuntu-24.04
timeout-minutes: 20
Expand Down Expand Up @@ -147,9 +147,88 @@ jobs:
if-no-files-found: error
retention-days: 1

build-server:
name: Build server (${{ matrix.goos }}/${{ matrix.goarch }})
needs: [preflight]
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
steps:
- name: Check out exact tag commit
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.preflight.outputs.commit }}
fetch-depth: 0
persist-credentials: false

- name: Revalidate release source
env:
EXPECTED_COMMIT: ${{ needs.preflight.outputs.commit }}
RELEASE_TAG: ${{ needs.preflight.outputs.tag }}
run: |
set -euo pipefail
git fetch --no-tags origin \
"refs/heads/main:refs/remotes/origin/main" \
"refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
actual_commit="$(./scripts/validate_release_source.sh "${RELEASE_TAG}")"
[[ "${actual_commit}" == "${EXPECTED_COMMIT}" ]]

- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: server/go.mod
cache-dependency-path: server/go.sum

- name: Build server binaries (${{ matrix.goos }}/${{ matrix.goarch }})
working-directory: server
env:
EXPECTED_COMMIT: ${{ needs.preflight.outputs.commit }}
RELEASE_TAG: ${{ needs.preflight.outputs.tag }}
run: |
set -euo pipefail
semver="${RELEASE_TAG#v}"
revision="${EXPECTED_COMMIT}"
contract="durable-context.v1"
ldflags="-s -w -buildvcs=true"
ldflags="${ldflags} -X github.com/PeterGuy326/mem/server/internal/api.Version=${semver}"
ldflags="${ldflags} -X github.com/PeterGuy326/mem/server/internal/api.Revision=${revision}"
ldflags="${ldflags} -X github.com/PeterGuy326/mem/server/internal/api.ContractVersion=${contract}"

mkdir -p "${RUNNER_TEMP}/assets"
suffix="${{ matrix.goos }}-${{ matrix.goarch }}"

for target in memd mem-migrate mem-healthcheck mem; do
output="${RUNNER_TEMP}/assets/${target}-${suffix}"
CGO_ENABLED=0 GOOS="${{ matrix.goos }}" GOARCH="${{ matrix.goarch }}" \
go build -buildvcs=true -trimpath \
-ldflags="${ldflags}" \
-o "${output}" "./cmd/${target}"
go version -m "${output}" | grep -F "vcs.revision=${revision}"
go version -m "${output}" | grep -F 'vcs.modified=false'
done

- name: Upload server assets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: server-${{ matrix.goos }}-${{ matrix.goarch }}
path: ${{ runner.temp }}/assets/*
if-no-files-found: error
retention-days: 1

release:
name: Create GitHub Release
needs: [preflight, build]
needs: [preflight, build-mcp, build-server]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
Expand Down Expand Up @@ -181,7 +260,6 @@ jobs:
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
path: /tmp/release-assets
pattern: mem-mcp-*
merge-multiple: true

- name: Validate exact assets and generate checksums
Expand Down Expand Up @@ -209,13 +287,29 @@ jobs:
fi

assets=(
/tmp/release-assets/memd-darwin-amd64
/tmp/release-assets/memd-darwin-arm64
/tmp/release-assets/memd-linux-amd64
/tmp/release-assets/memd-linux-arm64
/tmp/release-assets/mem-migrate-darwin-amd64
/tmp/release-assets/mem-migrate-darwin-arm64
/tmp/release-assets/mem-migrate-linux-amd64
/tmp/release-assets/mem-migrate-linux-arm64
/tmp/release-assets/mem-healthcheck-darwin-amd64
/tmp/release-assets/mem-healthcheck-darwin-arm64
/tmp/release-assets/mem-healthcheck-linux-amd64
/tmp/release-assets/mem-healthcheck-linux-arm64
/tmp/release-assets/mem-darwin-amd64
/tmp/release-assets/mem-darwin-arm64
/tmp/release-assets/mem-linux-amd64
/tmp/release-assets/mem-linux-arm64
/tmp/release-assets/mem-mcp-darwin-amd64
/tmp/release-assets/mem-mcp-darwin-arm64
/tmp/release-assets/mem-mcp-linux-amd64
/tmp/release-assets/mem-mcp-linux-arm64
/tmp/release-assets/mem-mcp-windows-amd64.exe
/tmp/release-assets/mem-mcp-windows-arm64.exe
/tmp/release-assets/mem-mcp-checksums.txt
/tmp/release-assets/mem-checksums.txt
)
release_flags=()
if [[ "${RELEASE_TAG}" == *-rc.* ]]; then
Expand Down Expand Up @@ -244,21 +338,38 @@ jobs:
jq -e --arg tag "${RELEASE_TAG}" '
.tagName == $tag and
.isDraft == true and
(.assets | length == 7) and
(.assets | length == 23) and
all(.assets[]; ((.size | type) == "number") and (.size > 0))
' <<< "${release_json}" >/dev/null

expected_assets="$(printf '%s\n' \
mem-checksums.txt \
mem-darwin-amd64 \
mem-darwin-arm64 \
mem-linux-amd64 \
mem-linux-arm64 \
mem-healthcheck-darwin-amd64 \
mem-healthcheck-darwin-arm64 \
mem-healthcheck-linux-amd64 \
mem-healthcheck-linux-arm64 \
mem-mcp-checksums.txt \
mem-mcp-darwin-amd64 \
mem-mcp-darwin-arm64 \
mem-mcp-linux-amd64 \
mem-mcp-linux-arm64 \
mem-mcp-windows-amd64.exe \
mem-mcp-windows-arm64.exe | LC_ALL=C sort)"
mem-mcp-windows-arm64.exe \
mem-migrate-darwin-amd64 \
mem-migrate-darwin-arm64 \
mem-migrate-linux-amd64 \
mem-migrate-linux-arm64 \
memd-darwin-amd64 \
memd-darwin-arm64 \
memd-linux-amd64 \
memd-linux-arm64 | LC_ALL=C sort)"
actual_assets="$(jq -r '.assets[].name' <<< "${release_json}" | LC_ALL=C sort)"
if [[ "${actual_assets}" != "${expected_assets}" ]]; then
echo "draft Release asset inventory does not match the expected seven files" >&2
echo "draft Release asset inventory does not match the expected files" >&2
printf 'expected:\n%s\nactual:\n%s\n' "${expected_assets}" "${actual_assets}" >&2
exit 1
fi
Expand All @@ -283,17 +394,34 @@ jobs:
jq -e --arg tag "${RELEASE_TAG}" '
.tagName == $tag and
.isDraft == true and
(.assets | length == 7) and
(.assets | length == 23) and
all(.assets[]; ((.size | type) == "number") and (.size > 0))
' <<< "${release_json}" >/dev/null
expected_assets="$(printf '%s\n' \
mem-checksums.txt \
mem-darwin-amd64 \
mem-darwin-arm64 \
mem-linux-amd64 \
mem-linux-arm64 \
mem-healthcheck-darwin-amd64 \
mem-healthcheck-darwin-arm64 \
mem-healthcheck-linux-amd64 \
mem-healthcheck-linux-arm64 \
mem-mcp-checksums.txt \
mem-mcp-darwin-amd64 \
mem-mcp-darwin-arm64 \
mem-mcp-linux-amd64 \
mem-mcp-linux-arm64 \
mem-mcp-windows-amd64.exe \
mem-mcp-windows-arm64.exe | LC_ALL=C sort)"
mem-mcp-windows-arm64.exe \
mem-migrate-darwin-amd64 \
mem-migrate-darwin-arm64 \
mem-migrate-linux-amd64 \
mem-migrate-linux-arm64 \
memd-darwin-amd64 \
memd-darwin-arm64 \
memd-linux-amd64 \
memd-linux-arm64 | LC_ALL=C sort)"
actual_assets="$(jq -r '.assets[].name' <<< "${release_json}" | LC_ALL=C sort)"
[[ "${actual_assets}" == "${expected_assets}" ]]

Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@ The project publishes 0.x prerelease versions; a stable release line is not yet

## [Unreleased]

### Added

- Publish installable server binaries (`memd`, `mem-migrate`, `mem-healthcheck`,
`mem`) alongside `mem-mcp` in the release workflow for `darwin/arm64`,
`darwin/amd64`, `linux/arm64` and `linux/amd64`, with per-group checksum
manifests.
- `/v1/version` now exposes `version` (semver), `revision` (40-hex git commit)
and `contract` (durable-context wire contract) as distinct fields, so clients
can pin a revision or accept a version range without a third mechanism. Both
the release workflow and the Docker image inject all three at build time.
- Documented first-run path in `docs/DEPLOYMENT.md` that yields a reachable
endpoint, a workspace, a token with write and recall scopes, and the
corresponding durable-context grant.

### Changed

- Migrate GitHub repository, Release, issue, badge, and raw-content coordinates
Expand Down
113 changes: 113 additions & 0 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ model-free Worker; optional heavy extras must be explicitly selected.

```bash
export MEM_VERSION=0.1.1
export MEM_REVISION="$(git rev-parse HEAD)"
export MEM_REGISTRY=registry.example.internal/mem

docker build \
--build-arg VERSION="$MEM_VERSION" \
--build-arg REVISION="$MEM_REVISION" \
-t "$MEM_REGISTRY/server:$MEM_VERSION" server
docker build \
-t "$MEM_REGISTRY/worker:$MEM_VERSION" worker
Expand Down Expand Up @@ -96,6 +98,117 @@ MEM_VALIDATE_BUILD_IMAGES=1 make test-deploy
The first command validates Compose and Helm. The second also builds all three
images from the current checkout.

## Version coordinate and client preflight

Every release publishes a single version grammar that clients pin against. The
`/v1/version` endpoint returns three distinct fields:

| Field | Example | Meaning |
| --- | --- | --- |
| `version` | `"0.1.1"` | Semver release tag (without the `v` prefix) |
| `revision` | `"10d4bf7a48fd5ab0ce6fc67caa407a717f81830e"` | 40-hex git commit the binary was built from |
| `contract` | `"durable-context.v1"` | Durable-context wire contract the server speaks |

Both build paths — the release workflow and the Docker image — inject all three
fields at build time via `-ldflags`. A binary produced by either path answers
`/v1/version` with the same shape. A client may pin either the `revision` (exact
commit) or accept a `version` range; the `contract` field is informational and
changes only when the durable-context wire format breaks compatibility.

The release workflow verifies that every published binary embeds the exact
release commit via `go version -m`. The Dockerfile accepts `VERSION`, `REVISION`
and `CONTRACT_VERSION` build args; the Compose and Helm deployment paths pass
the release tag as `VERSION` and the tag commit as `REVISION`.

No step in the documented deployment path requires hand-editing a build flag to
become compatible with a pinned client.

## First-run path

After starting memd from a release artifact (Compose, Docker image or bare
binary), complete these steps to reach a working endpoint with a workspace, a
token and the scopes needed for both write and recall operations.

### 1. Verify the server is reachable

```bash
curl --fail "$(mem config get server)/healthz"
curl --fail "$(mem config get server)/v1/version"
```

The `/v1/version` response must contain `version`, `revision` and `contract`
fields. If the endpoint is unreachable, the server is not running or the
configured URL is wrong. Run `mem doctor` to diagnose common preconditions.

### 2. Register the first user

With `MEM_REGISTRATION_MODE=first_user` (the Compose default), the first
registration creates the owner account and disables further registration
automatically:

```bash
mem auth login
```

Follow the interactive prompt. The CLI saves the session token to
`~/.mem/config.yaml`. Verify with:

```bash
mem auth status
```

### 3. Create an API token with write and recall scopes

The durable-context recall endpoint requires a token whose scopes cover both
`write` and `read`. Create one:

```bash
mem auth token create \
--name "digital-employee" \
--scope "read,write"
```

Store the returned token. It is shown exactly once.

### 4. Create a durable-context grant

Recall operations require an admin-created grant that binds a principal to an
approved set of memories. From the admin session:

```bash
curl -X POST "$(mem config get server)/v1/durable-context/grants" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"contract":"durable-context.v1","principal":"digital-employee","memory_ids":["<id>"]}'
```

The grant ties the principal name to the specific memories the recall scope is
allowed to read. Without this grant, recall returns `scope_denied` even with a
valid token.

### 5. Verify end-to-end

With the token and grant in place, a client adapter can complete one write and
one recall:

```bash
# Write a memory
curl -X POST "$(mem config get server)/v1/memories" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"content":"test memory","tags":["smoke-test"]}'

# Recall durable context
curl -X POST "$(mem config get server)/v1/durable-context/recall" \
-H "Authorization: Bearer ${API_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"contract":"durable-context.v1","principal":"digital-employee"}'
```

If any step fails with a named precondition error (`scope_denied`,
`contract_unsupported`, `registration_disabled`), the error message identifies
the missing configuration rather than a generic connection failure.

## Single-node Compose

### Host and network
Expand Down
Loading