Files
ClaudeMailbox/docs/superpowers/specs/2026-04-24-gitea-release-and-windows-service-design.md
mika kuns 0586d67a41 docs(spec): gitea release flow and windows service support
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 18:53:13 +02:00

12 KiB

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 workspacemktemp -d
    3. Checkout taggit clone --depth 1 --branch $TAG using secrets.GITEA_TOKEN
    4. Publishdotnet 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 Releasecurl POST ${GITEA_API}/repos/${REPO}/releases with tag_name, name, draft:false, prerelease:false, target_commitish:main
    7. Upload assetscurl POST .../releases/${RELEASE_ID}/assets?name=<file> for each asset
    8. Cleanuprm -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.txtsha256sum 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 builddotnet test tests/ClaudeMailbox.Tests/ClaudeMailbox.Tests.csproj
  • No publish, no release.

Section 2 — Windows Service Support

Project Changes

src/ClaudeMailbox/ClaudeMailbox.csproj adds:

<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.*" />

Host Wiring

ServerHost.CreateBuilder adds (before Build()):

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:
    { "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:

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.csUseWindowsService() wiring.
  • src/ClaudeMailbox/Program.cs — dispatch new verbs, integrate FileConfig precedence.
  • src/ClaudeMailbox/Cli/ServiceCommands.csnew. Verb handlers.
  • src/ClaudeMailbox/Config/FileConfig.csnew. Config file model + loader.
  • tests/ClaudeMailbox.Tests/Config/FileConfigTests.csnew.
  • tests/ClaudeMailbox.Tests/Config/ConfigPrecedenceTests.csnew.
  • .gitea/workflows/release.ymlnew. Tag-triggered release.
  • .gitea/workflows/ci.ymlnew. 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.