Files
mopac-pmo/archive/REPORT-20260829-0900-deploy.md
T

6.0 KiB

REPORT — Multi-account deploy packaging + runbook (Redmine 494)

  • When: 2026-08-29 ~05:45-06:00 CST
  • Repo: projects/meta/MOPAC/harness @ b7799ea (pushed to origin main)
  • Spec: docs/SPEC-20260829-charles-brief.md — account list (9 Linux accounts / 2 hosts), "no root on target accounts" PMO constraint
  • Gates: ./dev.sh check clean (all 11 Go packages ok, untouched); make deploy-test 13/13 ok; make release verified static + end-to-end smoke (install COSWFO in a fake HOME -> once --dry-run --demo exit 0 via the actual release binary)

1. What Charles executes (the whole deployment)

Authority: deploy/runbook.md + deploy/accounts.tsv in the repo.

  1. Build once (workstation, ~2-3 min): make release -> bin/harness-linux-amd64 (digest-pinned docker builder, CGO off, linux/amd64, static + stripped — file says "statically linked").
  2. Stage per host (~1 min each): tar czf /tmp/mopac-deploy.tgz deploy bin/harness-linux-amd64 + one scp per host.
  3. Install per account (~30s each, idempotent): ssh in, then either direct (install-account.sh reachableceo) or via identity switch (sudo -u TSGBOD -H sh -c '... && install-account.sh TSGBOD' — sudo is identity-switch only, nothing system-wide). The installer creates ~/.mopac/{bin,state/loop,state/events,reports,work}, installs the binary, renders harness.toml from the template + TSV row, writes the 0600 ~/.mopac/env secrets template, generates mopac-start/mopac-stop.
  4. Secrets bootstrap (~1 min/account): fill ~/.mopac/env from Bitwarden (Redmine key, LiteLLM vkey, 3 webhook secrets, serve vkey). Configs stay secret-free (env: refs only).
  5. Verify then start (~25s/account): see §3.
  6. Reboot persistence: cron @reboot line (documented verbatim) or manual re-run of the idempotent mopac-start. No systemd — that needs root, which the accounts don't have; both options documented.

2. Concurrency guard (why 9 daemons per host can't collide)

Port scheme from accounts.tsv (index is global 0-8, listed order): events = 4100 + index, serve = 8090 + index.

account host events serve account host events serve
reachableceo streaming 4100 8090 reachableceo-offstage offstage 4105 8095
TSGBOD streaming 4101 8091 COSRCEO-Personal offstage 4106 8096
TSGCOO streaming 4102 8092 COSRCEO-Biz offstage 4107 8097
TSGCTO streaming 4103 8093 COSWFO offstage 4108 8098
TSGCCO streaming 4104 8094

The loop daemon has no port; its state (state/loop, state/events) is per-account under ~/.mopac/. The installer hard-asserts the scheme per row (dies if events != 4100+index), and deploy/tests.sh re-asserts fleet-wide (host,port) uniqueness.

3. Verification steps (per account, runbook step 5+6)

  1. ~/.mopac/bin/harness once --dry-run --demo -config ~/.mopac/harness.toml — no secrets, no LLM call; must exit 0 printing the PLAN with the account's vertical (this exact path is what the release smoke ran).
  2. . ~/.mopac/env && ~/.mopac/bin/harness loop --once --dry-run -config ... — first real Redmine scan of the account's scope (needs the Redmine key): prints what would dispatch, writes nothing.
  3. mopac-start, then healthz on the account's two ports (curl http://127.0.0.1:<events>/healthz, <serve>/healthz) and an authenticated GET /v1/models through the serve vkey; tail ~/.mopac/state/loop.log for scan lines.

4. Rollback (per account, ~30s)

~/.mopac/bin/mopac-stop (SIGTERM, clean) -> rm -rf ~/.mopac (or mv aside to keep evidence) -> drop the cron @reboot line if used. Every trace of an instance lives under ~/.mopac/; Redmine/Gitea data is untouched by removal.

5. Time estimate (the Charles window)

streaming (5 accts) offstage (4 accts)
stage + install ~3 min ~2.5 min
secrets bootstrap ~5 min (unless prefilled) ~4 min
verify + start + healthz ~2 min ~1.5 min
total ~10 min (mechanical only: ~5) ~8 min (mechanical: ~4)

Build + Redmine project bootstrap happen before the window.

6. Tests (deploy/tests.sh, 13 assertions, all green)

TSV: 9 rows, spec-exact account/host sets, unique accounts + (host,port) pairs, events=4100+idx/serve=8090+idx on every row. Installer: render all 9 accounts into fake HOMEs (no leftover @PLACEHOLDER@s, correct vertical/ports/project/absolute paths, env mode 0600, helpers executable with the right ports); idempotent re-run (exit 0, config+env byte-identical); hand-edited harness.toml survives re-runs; unknown account exits non-zero; staged-bundle binary lookup works; rendered TOML actually loads (once --dry-run --demo, exit 0). Plus the live release-binary smoke described above.

7. Assumptions flagged in the runbook (one-line fixes, no redeploy)

  • Redmine project identifiers mopac-<vertical> x9 must exist (or edit the TSV column before staging / the scope_query after install — installer never overwrites an existing config).
  • Quota grouping assumes one z.ai Max plan per host (zai-max-1/zai-max-2); [quota] ships commented with values pre-filled — confirm grouping, then flip enabled = true per account.
  • [redmine.status_map] ships empty (workflow names are per-project; the generated file documents the Released->Done pair to set).
  • Host short names assumed ultix-streaming/ultix-offstage; the installer prints an advisory on mismatch, never blocks.

8. Open items

  • Actual ssh/scp execution is Charles's window (PMO runtime cannot ssh/sudo) — everything is scripted, tested locally, and pushed.
  • Webhook registration in Redmine/Discourse/Gitea per account (URLs + secrets) is post-install config, not packaging; noted in runbook step 0.
  • LiteLLM virtual keys per account assumed to exist on 192.168.3.78:4001.

— PMO worker, MOPAC harness self-host loop