248 lines
12 KiB
Markdown
248 lines
12 KiB
Markdown
# Gitea Release Flow + Windows Service Support
|
|
|
|
**Date:** 2026-04-24
|
|
**Status:** Approved (ready for implementation plan)
|
|
|
|
## Goal
|
|
|
|
Two coupled capabilities for ClaudeMailbox:
|
|
|
|
1. **Gitea Release Flow.** Tag-driven CI that publishes a self-contained `win-x64` single-file binary plus SHA256 checksums to a Gitea release.
|
|
2. **Windows Service Support.** The binary self-installs as a Windows Service via new `install-service` / `uninstall-service` / `start` / `stop` verbs, seeded from a `mailbox.json` config file. The ClaudeDo Installer can then provision ClaudeMailbox directly using this stable verb surface.
|
|
|
|
Both are needed so that a downstream installer (ClaudeDo) can fetch, verify, and register ClaudeMailbox as a long-running service without hand-crafted `sc.exe` calls at the installer side.
|
|
|
|
## Non-Goals
|
|
|
|
- Linux / macOS service integration (`systemd`, `launchd`). Single-platform (`win-x64`) only in v1.
|
|
- Cross-machine / non-loopback deployment.
|
|
- Auto-update of the service binary (ClaudeDo owns update orchestration via its existing `SelfUpdater` pattern).
|
|
- Per-user service installation. Service runs as `NT AUTHORITY\LocalService`; DB lives under `%ProgramData%`.
|
|
|
|
## Architecture Overview
|
|
|
|
```
|
|
tag push ──► .gitea/workflows/release.yml
|
|
│
|
|
├─ dotnet publish (win-x64, self-contained, single-file)
|
|
├─ sha256 → checksums.txt
|
|
└─ POST /api/v1/repos/.../releases
|
|
+ upload assets
|
|
│
|
|
▼
|
|
claude-mailbox-${VERSION}-win-x64.exe
|
|
checksums.txt
|
|
│
|
|
│ (consumed by ClaudeDo Installer)
|
|
▼
|
|
claude-mailbox.exe install-service [--port] [--bind] [--db-path]
|
|
│
|
|
├─ seed %ProgramData%\ClaudeMailbox\mailbox.json
|
|
├─ ACL %ProgramData%\ClaudeMailbox\ for LocalService
|
|
└─ sc.exe create ClaudeMailbox
|
|
binPath= "<exe> serve --config <mailbox.json>"
|
|
start= auto obj= "NT AUTHORITY\LocalService"
|
|
│
|
|
▼
|
|
ClaudeMailbox Windows Service
|
|
(reads mailbox.json, hosts MCP + REST)
|
|
```
|
|
|
|
## Section 1 — Gitea Release Flow
|
|
|
|
### Trigger
|
|
- `push` on tags matching `v*`.
|
|
- Separate `ci.yml` runs `dotnet build` + `dotnet test` on every push to `main` (no release).
|
|
|
|
### Workflow Skeleton
|
|
`.gitea/workflows/release.yml` modelled after `C:\Private\ClaudeDo\.gitea\workflows\release.yml`:
|
|
|
|
- `runs-on: ubuntu-latest`
|
|
- `env: DOTNET_ROOT: /home/mika/.dotnet`, `GITEA_API: https://git.kuns.dev/api/v1`, `REPO: releases/ClaudeMailbox` (exact repo slug to confirm at implementation time)
|
|
- Steps:
|
|
1. **Resolve version** — strip `v` prefix from `github.ref_name` → `$VERSION`
|
|
2. **Prepare workspace** — `mktemp -d`
|
|
3. **Checkout tag** — `git clone --depth 1 --branch $TAG` using `secrets.GITEA_TOKEN`
|
|
4. **Publish** — `dotnet publish src/ClaudeMailbox/ClaudeMailbox.csproj -c Release -r win-x64 --self-contained true /p:MinVerVersionOverride=$VERSION -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o out/app`
|
|
5. **Package assets** — rename produced exe to `claude-mailbox-${VERSION}-win-x64.exe`, write `checksums.txt` via `sha256sum`
|
|
6. **Create Gitea Release** — `curl POST ${GITEA_API}/repos/${REPO}/releases` with `tag_name`, `name`, `draft:false`, `prerelease:false`, `target_commitish:main`
|
|
7. **Upload assets** — `curl POST .../releases/${RELEASE_ID}/assets?name=<file>` for each asset
|
|
8. **Cleanup** — `rm -rf $WORK` with `if: always()`
|
|
|
|
### Versioning
|
|
`Directory.Build.props` gains a MinVer reference so `$VERSION` from the tag is baked into `AssemblyVersion`, `FileVersion`, `InformationalVersion`. Same approach as ClaudeDo (`MinVerVersionOverride` property). Default local dev version: `0.0.0-dev`.
|
|
|
|
Whether to pull in the `MinVer` NuGet package explicitly or use .NET SDK's built-in `Version` property with `-p:Version=$VERSION` is an implementation choice made during the plan phase — ClaudeDo uses `MinVerVersionOverride` which implies the MinVer package is present.
|
|
|
|
### Release Assets (stable contract)
|
|
- `claude-mailbox-${VERSION}-win-x64.exe` — self-contained single-file binary
|
|
- `checksums.txt` — `sha256sum` output for the exe
|
|
|
|
No ZIP (single artifact, simpler discovery).
|
|
|
|
### CI Workflow
|
|
`.gitea/workflows/ci.yml`:
|
|
- Trigger: push to `main`, PRs to `main`
|
|
- Steps: checkout → setup dotnet → `dotnet build` → `dotnet test tests/ClaudeMailbox.Tests/ClaudeMailbox.Tests.csproj`
|
|
- No publish, no release.
|
|
|
|
## Section 2 — Windows Service Support
|
|
|
|
### Project Changes
|
|
`src/ClaudeMailbox/ClaudeMailbox.csproj` adds:
|
|
|
|
```xml
|
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.*" />
|
|
```
|
|
|
|
### Host Wiring
|
|
`ServerHost.CreateBuilder` adds (before `Build()`):
|
|
|
|
```csharp
|
|
builder.Host.UseWindowsService(opt => opt.ServiceName = "ClaudeMailbox");
|
|
```
|
|
|
|
No-op when launched as console app; enables Windows Service lifetime when SCM starts the process.
|
|
|
|
### New CLI Verbs
|
|
Dispatch in `Program.cs` extended. New module `src/ClaudeMailbox/Cli/ServiceCommands.cs`:
|
|
|
|
| Verb | Behavior |
|
|
|---|---|
|
|
| `install-service [--port] [--bind] [--db-path]` | Admin-only. Seed `mailbox.json` (if missing). ACL ProgramData dir. `sc.exe create`. `sc.exe description`. |
|
|
| `uninstall-service [--purge]` | Admin-only. `sc.exe stop` (best-effort), `sc.exe delete`. `--purge` removes `%ProgramData%\ClaudeMailbox`. |
|
|
| `start` | `sc.exe start ClaudeMailbox`. |
|
|
| `stop` | `sc.exe stop ClaudeMailbox`. |
|
|
| `status` | `sc.exe query ClaudeMailbox`, parse `STATE` line, print single-word status. |
|
|
|
|
Service commands:
|
|
- Gate on `OperatingSystem.IsWindows()`. On non-Windows: exit 2, message `"Service commands are Windows-only."`
|
|
- Gate on admin privilege (WindowsIdentity + WindowsPrincipal). On missing: exit 5, message `"install-service requires Administrator."`
|
|
- Shell out to `sc.exe` via `Process.Start(new ProcessStartInfo { ... RedirectStandardOutput = true })`, capture exit code, surface stderr on failure.
|
|
|
|
### install-service Flow (concrete)
|
|
1. Admin check.
|
|
2. `Directory.CreateDirectory(@"C:\ProgramData\ClaudeMailbox")` (idempotent).
|
|
3. Apply ACL: add `LocalService` with `Modify` rights on that directory (`DirectorySecurity` + `FileSystemAccessRule`).
|
|
4. If `mailbox.json` missing: write seeded JSON with CLI-flag-overridable values:
|
|
```json
|
|
{ "port": 47822, "bind": "127.0.0.1", "dbPath": "C:\\ProgramData\\ClaudeMailbox\\mailbox.db" }
|
|
```
|
|
5. Resolve current exe path via `Environment.ProcessPath`.
|
|
6. `sc.exe create ClaudeMailbox binPath= "\"<exe>\" serve --config \"C:\ProgramData\ClaudeMailbox\mailbox.json\"" start= auto DisplayName= "Claude Mailbox" obj= "NT AUTHORITY\LocalService"`
|
|
7. `sc.exe description ClaudeMailbox "MCP mailbox server for parallel Claude sessions"`
|
|
|
|
Service name is fixed (`ClaudeMailbox`) in v1 — no multi-instance support.
|
|
|
|
### uninstall-service Flow
|
|
1. Admin check.
|
|
2. `sc.exe stop ClaudeMailbox` (ignore failure if not running).
|
|
3. `sc.exe delete ClaudeMailbox`.
|
|
4. If `--purge`: delete `%ProgramData%\ClaudeMailbox` recursively (only if empty of non-ours files, or unconditionally — default to unconditional with explicit `--purge` opt-in).
|
|
|
|
## Section 3 — Config File + Precedence
|
|
|
|
### File
|
|
`src/ClaudeMailbox/Config/FileConfig.cs`:
|
|
|
|
```csharp
|
|
public sealed class FileConfig
|
|
{
|
|
public int? Port { get; set; }
|
|
public string? Bind { get; set; }
|
|
public string? DbPath { get; set; }
|
|
}
|
|
```
|
|
|
|
### Loader
|
|
`FileConfig.LoadOrDefault(string? explicitPath)`:
|
|
- If `explicitPath` given: must exist, else throw with clear message.
|
|
- Else: probe `%ProgramData%\ClaudeMailbox\mailbox.json`. If absent, return empty `FileConfig`.
|
|
- Parse via `System.Text.Json.JsonSerializer` with `PropertyNameCaseInsensitive = true`.
|
|
|
|
### Precedence
|
|
In `Program.cs` (before building `DaemonConfig`):
|
|
|
|
1. CLI flag (`--port`, `--bind`, `--db-path`)
|
|
2. Config file (explicit via `--config <path>` OR default `%ProgramData%\ClaudeMailbox\mailbox.json` if present)
|
|
3. Hardcoded defaults in `DaemonConfig`
|
|
|
|
Extract helper `DaemonConfig BuildDaemonConfig(string[] args, FileConfig file)` for testability.
|
|
|
|
### Backward Compatibility
|
|
When `--config` is not passed AND no ProgramData config exists, behavior is unchanged: daemon uses `%USERPROFILE%\.claude-mailbox\mailbox.db` and default port/bind. Existing interactive users are unaffected.
|
|
|
|
### Service CmdLine
|
|
`install-service` always bakes `--config C:\ProgramData\ClaudeMailbox\mailbox.json` into the service's `binPath`, so the service has no dependency on working directory, user profile, or environment.
|
|
|
|
## Section 4 — Tests
|
|
|
|
### New Unit Tests
|
|
- `tests/ClaudeMailbox.Tests/Config/FileConfigTests.cs`
|
|
- Round-trip JSON with all fields.
|
|
- Missing fields deserialize to `null` (optional semantics).
|
|
- Malformed JSON throws with actionable message.
|
|
- `tests/ClaudeMailbox.Tests/Config/ConfigPrecedenceTests.cs`
|
|
- CLI flag wins over file value.
|
|
- File value wins over default.
|
|
- Default used when neither file nor CLI provides it.
|
|
- Mixed: `--port` from CLI, `dbPath` from file, `bind` from default.
|
|
|
|
### Out of Scope
|
|
- Service install/uninstall tests. `sc.exe` requires Administrator, is Windows-only, and not runnable on `ubuntu-latest` CI. A manual smoke-test protocol is documented in README:
|
|
```
|
|
claude-mailbox.exe install-service
|
|
sc query ClaudeMailbox
|
|
Invoke-WebRequest http://127.0.0.1:47822/health
|
|
claude-mailbox.exe uninstall-service --purge
|
|
```
|
|
|
|
### Cross-Platform Build
|
|
Service-related code must compile on Linux (CI runner). Use:
|
|
- `Microsoft.Extensions.Hosting.WindowsServices` — NuGet package is cross-platform; `UseWindowsService()` is a no-op on non-Windows.
|
|
- `sc.exe` invocations guarded by `OperatingSystem.IsWindows()`.
|
|
- `DirectorySecurity` / `FileSystemAccessRule` are in `System.Security.AccessControl` — available on non-Windows but throw on use. Guard all calls.
|
|
|
|
## Section 5 — ClaudeDo Installer Contract
|
|
|
|
This section is **informational** — the work is on the ClaudeDo side, not in this repo. Listed here to make the contract explicit.
|
|
|
|
### Discovery
|
|
`GET https://git.kuns.dev/api/v1/repos/releases/ClaudeMailbox/releases/latest` returns `assets[].browser_download_url`. Reuse ClaudeDo's `ReleaseClient` + `ChecksumVerifier` + `VersionComparer`.
|
|
|
|
### Installer Steps (new in ClaudeDo)
|
|
1. Download `claude-mailbox-${VERSION}-win-x64.exe` → `%ProgramFiles%\ClaudeMailbox\claude-mailbox.exe`.
|
|
2. Verify SHA256 against `checksums.txt`.
|
|
3. Run `claude-mailbox.exe install-service` (elevated). This seeds `mailbox.json`, ACLs ProgramData, creates the service.
|
|
4. Run `claude-mailbox.exe start` (or `sc.exe start ClaudeMailbox`).
|
|
5. Record entry in ClaudeDo's `InstallManifest` (version + path).
|
|
6. On uninstall: `claude-mailbox.exe uninstall-service --purge` → delete `%ProgramFiles%\ClaudeMailbox\`.
|
|
|
|
### Stable Contract
|
|
- Verbs: `install-service`, `uninstall-service`, `start`, `stop`, `status`.
|
|
- Flags on `install-service`: `--port`, `--bind`, `--db-path` (all optional).
|
|
- Service name: `ClaudeMailbox`.
|
|
- Config path: `%ProgramData%\ClaudeMailbox\mailbox.json`.
|
|
|
|
Any changes to the above require coordinated updates in the ClaudeDo installer.
|
|
|
|
## Files Touched
|
|
|
|
- `src/ClaudeMailbox/ClaudeMailbox.csproj` — add `Microsoft.Extensions.Hosting.WindowsServices`.
|
|
- `src/ClaudeMailbox/ServerHost.cs` — `UseWindowsService()` wiring.
|
|
- `src/ClaudeMailbox/Program.cs` — dispatch new verbs, integrate `FileConfig` precedence.
|
|
- `src/ClaudeMailbox/Cli/ServiceCommands.cs` — **new**. Verb handlers.
|
|
- `src/ClaudeMailbox/Config/FileConfig.cs` — **new**. Config file model + loader.
|
|
- `tests/ClaudeMailbox.Tests/Config/FileConfigTests.cs` — **new**.
|
|
- `tests/ClaudeMailbox.Tests/Config/ConfigPrecedenceTests.cs` — **new**.
|
|
- `.gitea/workflows/release.yml` — **new**. Tag-triggered release.
|
|
- `.gitea/workflows/ci.yml` — **new**. Build + test on main.
|
|
- `Directory.Build.props` — MinVer integration (or `Version` fallback).
|
|
- `README.md` — document `install-service` / `uninstall-service`, config file, manual smoke test.
|
|
|
|
## Open Questions for Implementation Plan
|
|
|
|
- Exact Gitea repo slug (confirm `releases/ClaudeMailbox` or other).
|
|
- MinVer package vs. `-p:Version=$VERSION` — align with ClaudeDo's current convention.
|
|
- Whether `status` verb should print parsed `Running | Stopped | NotInstalled` or raw `sc query` output. Recommendation: parsed + exit codes 0/1/2.
|