12 KiB
Gitea Release Flow + Windows Service Support
Date: 2026-04-24 Status: Approved (ready for implementation plan)
Goal
Two coupled capabilities for ClaudeMailbox:
- Gitea Release Flow. Tag-driven CI that publishes a self-contained
win-x64single-file binary plus SHA256 checksums to a Gitea release. - Windows Service Support. The binary self-installs as a Windows Service via new
install-service/uninstall-service/start/stopverbs, seeded from amailbox.jsonconfig 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
SelfUpdaterpattern). - 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
pushon tags matchingv*.- Separate
ci.ymlrunsdotnet build+dotnet teston every push tomain(no release).
Workflow Skeleton
.gitea/workflows/release.yml modelled after C:\Private\ClaudeDo\.gitea\workflows\release.yml:
runs-on: ubuntu-latestenv: 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:
- Resolve version — strip
vprefix fromgithub.ref_name→$VERSION - Prepare workspace —
mktemp -d - Checkout tag —
git clone --depth 1 --branch $TAGusingsecrets.GITEA_TOKEN - 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 - Package assets — rename produced exe to
claude-mailbox-${VERSION}-win-x64.exe, writechecksums.txtviasha256sum - Create Gitea Release —
curl POST ${GITEA_API}/repos/${REPO}/releaseswithtag_name,name,draft:false,prerelease:false,target_commitish:main - Upload assets —
curl POST .../releases/${RELEASE_ID}/assets?name=<file>for each asset - Cleanup —
rm -rf $WORKwithif: always()
- Resolve version — strip
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 binarychecksums.txt—sha256sumoutput for the exe
No ZIP (single artifact, simpler discovery).
CI Workflow
.gitea/workflows/ci.yml:
- Trigger: push to
main, PRs tomain - 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:
<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.exeviaProcess.Start(new ProcessStartInfo { ... RedirectStandardOutput = true }), capture exit code, surface stderr on failure.
install-service Flow (concrete)
- Admin check.
Directory.CreateDirectory(@"C:\ProgramData\ClaudeMailbox")(idempotent).- Apply ACL: add
LocalServicewithModifyrights on that directory (DirectorySecurity+FileSystemAccessRule). - If
mailbox.jsonmissing: write seeded JSON with CLI-flag-overridable values:{ "port": 47822, "bind": "127.0.0.1", "dbPath": "C:\\ProgramData\\ClaudeMailbox\\mailbox.db" } - Resolve current exe path via
Environment.ProcessPath. sc.exe create ClaudeMailbox binPath= "\"<exe>\" serve --config \"C:\ProgramData\ClaudeMailbox\mailbox.json\"" start= auto DisplayName= "Claude Mailbox" obj= "NT AUTHORITY\LocalService"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
- Admin check.
sc.exe stop ClaudeMailbox(ignore failure if not running).sc.exe delete ClaudeMailbox.- If
--purge: delete%ProgramData%\ClaudeMailboxrecursively (only if empty of non-ours files, or unconditionally — default to unconditional with explicit--purgeopt-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
explicitPathgiven: must exist, else throw with clear message. - Else: probe
%ProgramData%\ClaudeMailbox\mailbox.json. If absent, return emptyFileConfig. - Parse via
System.Text.Json.JsonSerializerwithPropertyNameCaseInsensitive = true.
Precedence
In Program.cs (before building DaemonConfig):
- CLI flag (
--port,--bind,--db-path) - Config file (explicit via
--config <path>OR default%ProgramData%\ClaudeMailbox\mailbox.jsonif present) - 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:
--portfrom CLI,dbPathfrom file,bindfrom default.
Out of Scope
- Service install/uninstall tests.
sc.exerequires Administrator, is Windows-only, and not runnable onubuntu-latestCI. 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.exeinvocations guarded byOperatingSystem.IsWindows().DirectorySecurity/FileSystemAccessRuleare inSystem.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)
- Download
claude-mailbox-${VERSION}-win-x64.exe→%ProgramFiles%\ClaudeMailbox\claude-mailbox.exe. - Verify SHA256 against
checksums.txt. - Run
claude-mailbox.exe install-service(elevated). This seedsmailbox.json, ACLs ProgramData, creates the service. - Run
claude-mailbox.exe start(orsc.exe start ClaudeMailbox). - Record entry in ClaudeDo's
InstallManifest(version + path). - 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— addMicrosoft.Extensions.Hosting.WindowsServices.src/ClaudeMailbox/ServerHost.cs—UseWindowsService()wiring.src/ClaudeMailbox/Program.cs— dispatch new verbs, integrateFileConfigprecedence.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 (orVersionfallback).README.md— documentinstall-service/uninstall-service, config file, manual smoke test.
Open Questions for Implementation Plan
- Exact Gitea repo slug (confirm
releases/ClaudeMailboxor other). - MinVer package vs.
-p:Version=$VERSION— align with ClaudeDo's current convention. - Whether
statusverb should print parsedRunning | Stopped | NotInstalledor rawsc queryoutput. Recommendation: parsed + exit codes 0/1/2.