Skip to content

feat(onboard): permanent client-onboarding package — templates + onboard command (#127) - #131

Merged
samo-agent merged 4 commits into
NikolayS:mainfrom
samo-agent:feat/client-onboard-package
Jul 7, 2026
Merged

feat(onboard): permanent client-onboarding package — templates + onboard command (#127)#131
samo-agent merged 4 commits into
NikolayS:mainfrom
samo-agent:feat/client-onboard-package

Conversation

@samo-agent

Copy link
Copy Markdown
Collaborator

Summary

Closes #127 (the package itself; rollout checklist in the issue remains open).

Adds the permanent, replicable client-onboarding package so a new client repo inherits prod + previews + CI + stack knowledge with one command — no per-client dance.

What ships

templates/client-repo/ — versioned in samohost; scaffolded into every new client repo:

File Purpose
.github/workflows/ci.yml Generic client CI derived from Tanya301/field-record-1 but with all #117 AppArmor / host-PG workarounds stripped. Uses a postgres:18 service container. Runner tag parameterised as {{RUNNER_TAG}} (repo variable).
.samohost.toml Standard client manifest (name, repo, branch, appDir, buildCmd, migrateCmd, healthUrl, serviceUnit, kind, dbBackend, mainHost) — same schema as docs/examples/.samohost.toml.example.
CLAUDE.md Carries SAMO-STACK:START/END + SAMO-DEV-PRINCIPLES:START/END sync-block markers so client-project Claude agents load the stack docs and dev principles automatically.
staging.env.example Documents the standard env-var shape; never committed with real values.

src/commands/onboard.tssamohost onboard <org/repo> --vm <name>:

  1. Fetches .samohost.toml from the target repo (GitHub API) or reads a local --toml-path.
  2. Scaffolds templates/client-repo/ into the target repo as a samohost-onboarding branch.
  3. Opens a PR (samo-agent identity via GITHUB_TOKEN).
  4. Calls runAppRegister() to register the app in samohost state — equivalent to app register --from-toml.
  5. Verifies trigger coverage: the app appears in appStore.list(), so samohost-trigger.timer will auto-deploy on every push to main.
  6. Idempotent: re-run updates files on the existing branch, surfaces the existing PR URL, upserts the app record without duplication.

src/cli.ts — wires parseArgs onboard … and dispatches to runOnboard().

TDD — RED → GREEN

  • RED commit 09c534c: 20 failing tests (Cannot find module '../src/commands/onboard.ts'). Covers parseArgs, scaffold shape, app registration, trigger coverage, idempotency, template rendering.
  • GREEN commit 605e690: implementation; all 1374 tests pass, typecheck clean.

Root cause / design

The manual onboarding checklist (adopt VM → register app → scaffold CI → open PR) had no code representation. Each new client required a bespoke sequence and produced subtly different CI configurations. This PR collapses that into a single idempotent command. All I/O is injected via OnboardDeps so the flow is fully unit-tested offline.

Deferred (not built — per brief)

Test evidence

bun test — 1374 pass, 0 fail (63 files)
bunx tsc --noEmit — clean

🤖 Generated with Claude Code

samo-agent and others added 4 commits July 7, 2026 19:13
Adds failing tests for:
- parseArgs onboard <org/repo> --vm <name> parsing + error cases
- runOnboard scaffold shape (all TEMPLATE_FILES, CLAUDE.md sync markers,
  staging.env.example, PR URL in report)
- runOnboard app registration and trigger coverage verification
- runOnboard idempotency (re-run → updated, no duplicate PR or app record)
- renderTemplate {{RUNNER_TAG}} substitution
- TEMPLATE_FILES exported constant

All tests fail with "Cannot find module '../src/commands/onboard.ts'" —
the module does not exist yet (this is the RED commit).

Refs NikolayS#127

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the permanent, replicable client-onboarding package:

templates/client-repo/ (versioned in samohost):
- .github/workflows/ci.yml  — generic client CI using a postgres:18 service
  container; parameterised runner tag ({{RUNNER_TAG}}); NO field-record-1 NikolayS#117
  AppArmor/host-PG workarounds
- .samohost.toml             — standard client manifest template with all
  canonical fields (name, repo, branch, appDir, buildCmd, migrateCmd,
  healthUrl, serviceUnit, kind, dbBackend, mainHost)
- CLAUDE.md                  — carries SAMO-STACK:START/END + SAMO-DEV-PRINCIPLES
  sync-block markers so client-project agents load principles + stack docs
- staging.env.example        — standard env var shape documentation

src/commands/onboard.ts:
- runOnboard() — scaffolds templates/client-repo/ into the target repo as
  a branch + PR (samo-agent identity via GITHUB_TOKEN), then calls
  runAppRegister() to register the app in state, verifies trigger coverage
  (app appears in appStore = trigger picks it up). Fully idempotent: re-run
  updates files on existing branch, surfaces existing PR URL, upserts app
  record without duplication.
- renderTemplate() — {{RUNNER_TAG}}/{{APP_NAME}}/{{REPO}}/{{APP_DB_NAME}}
  substitution with sensible defaults
- TEMPLATE_FILES constant — canonical list of scaffolded paths
- defaultOnboardDeps() — live GitHub API deps (requires GITHUB_TOKEN)

src/cli.ts:
- parseArgs onboard <org/repo> --vm <name> [--runner-tag <tag>]
  [--toml-path <path>]
- Dispatch case wired into main()

All 1374 tests pass, typecheck clean.

Deferred (not built now, per brief):
- Mechanising per-repo self-hosted runner registration (docs/runner-admin-handoff.md PAT flow)
- field-record cutover onto the canonical path

Closes NikolayS#127 (the package; rollout checklist in the issue remains open)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pins the expected behaviour for all 5 blockers from the samorev REQUEST_CHANGES:

1. EXIT-CODE HONESTY — test/onboard-exit-code.test.ts: imports
   `computeOnboardExitCode` (does not exist yet) and asserts it returns 1 for
   every partial-failure shape (appRegistered=false, triggerCovered=false,
   status="error") and 0 only on full success.

2. REAL CI.YML TEST — replaces the Array.isArray placeholder at test line 400
   with a real readFileSync of the template and checks for absence of NikolayS#117,
   AppArmor, and host-PG markers.

3. CI.YML COMMENT — asserts the template comment does NOT say "repository
   variable" (it is a scaffold-time substitution, not a repo variable).

4. CI.YML DUPLICATE RUNS-ON — asserts the rendered default (RUNNER_TAG=
   "self-hosted") does not produce [self-hosted, "self-hosted"].

5. RE-ONBOARD CLOBBER GUARD — asserts scaffoldFile is never called for
   .samohost.toml when the repo already has one.

6. findPr ERROR SURFACING — asserts defaultOnboardDeps().findPr throws on
   non-200 HTTP instead of silently returning null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ikolayS#131)

Root-cause fixes (not symptom guards):

1. EXIT-CODE HONESTY — cli.ts: Extract `computeOnboardExitCode(report)` and
   wire it into the onboard case.  Returns 1 when `status==="error"` OR
   `appRegistered===false` OR `triggerCovered===false`.  Previously the
   inline code only checked `status==="error"`, silently returning 0 when
   the PR was created but the app was not registered.

2. REAL CI.YML TEST (test-only) — test/onboard.test.ts: the placeholder
   `Array.isArray(TEMPLATE_FILES)` check at line 400 is replaced with a real
   `readFileSync` of templates/client-repo/.github/workflows/ci.yml asserting
   absence of `NikolayS#117`, `AppArmor`, and `host-PG` markers.

3. CI.YML TEMPLATE — templates/client-repo/.github/workflows/ci.yml:
   (a) Corrected the runs-on comment: RUNNER_TAG is a scaffold-time
       {{RUNNER_TAG}} substitution (performed by `samohost onboard`), NOT a
       GitHub repository variable.
   (b) Changed runs-on from `[self-hosted, "{{RUNNER_TAG}}"]` to
       `"{{RUNNER_TAG}}"`.  The old form rendered `[self-hosted, "self-hosted"]`
       (duplicate label) when the default "self-hosted" was used.

4. RE-ONBOARD CLOBBER GUARD — src/commands/onboard.ts: Track whether
   `.samohost.toml` was fetched from the target repo (vs. generated from the
   template).  If it was, skip `scaffoldFile` for `.samohost.toml` in the
   scaffold loop.  The real manifest is still used for app registration;
   only the overwrite is suppressed.

5. findPr ERROR SURFACING — defaultOnboardDeps().findPr now throws
   `Error("GitHub API error ${status} ...")` on non-200 HTTP instead of
   silently returning null.  A null return incorrectly routed to createPr,
   risking duplicate PRs on transient API failures.

All 1382 tests pass; `bunx tsc --noEmit` clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@samo-agent
samo-agent force-pushed the feat/client-onboard-package branch from 605e690 to 00888e6 Compare July 7, 2026 19:23
@samo-agent
samo-agent merged commit 3a6dcf7 into NikolayS:main Jul 7, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SAMO client package: every client gets prod + dblab previews + auto-deploy

1 participant