feat(git-sync): GitSyncModule orchestrator + config + listener (Phase A.4b/B)

Control plane wiring (plan §5-§11):
- PageService create/update/movePage now honor provenance actor 'git-sync'
  (stamp lastUpdatedSource='git-sync'), closing the A.4a gap.
- EnvironmentService: GIT_SYNC_ENABLED / DATA_DIR / REMOTE_TEMPLATE /
  POLL_INTERVAL_MS / DEBOUNCE_MS / SERVICE_USER_ID (required-if-enabled) /
  SSH_KEY_PATH + validation.
- VaultRegistryService: per-space vault path + cached VaultGit.
- GitSyncOrchestrator: per-space Redis leader-lock (SET NX PX + CAS-Lua release,
  randomUUID instanceId) + in-process mutex; runOnce drives the vendored engine
  PULL (readExisting->computePullActions->applyPullActions) then PUSH (runPush)
  with the bound native GitSyncClient + VaultGit; @Interval poll-safety gated on
  GIT_SYNC_ENABLED; imports plain ScheduleModule (TelemetryModule owns forRoot).
- PageChangeListener: @OnEvent PAGE_* -> per-space debounce -> runOnce, with a
  best-effort lastUpdatedSource==='git-sync' loop-guard.
- GitSyncController: admin POST /api/git-sync/trigger + GET /status (ops/e2e).
- GitSyncModule registered in app.module. Enabled-space enumeration uses
  settings.gitSync.enabled, falling back to all live spaces until Phase C writes
  the flag (master gate = GIT_SYNC_ENABLED).

tsc clean; 713 tests/71 suites pass; dev server hot-reloaded the module (route
live, DI graph boots). Live pull/push round-trip verified next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-21 15:04:31 +03:00
parent faa989ddbf
commit da612c600e
10 changed files with 805 additions and 3 deletions

View File

@@ -320,4 +320,58 @@ export class EnvironmentService {
.map((o) => o.trim())
.filter(Boolean);
}
// --- git-sync (plan §7.2) -------------------------------------------------
/** Global master switch for the git-sync control plane (default false). */
isGitSyncEnabled(): boolean {
return (
this.configService.get<string>('GIT_SYNC_ENABLED', 'false').toLowerCase() ===
'true'
);
}
/**
* Root directory holding the per-space vault repos. Defaults to
* `<DATA_DIR or ./data>/git-sync`. `DATA_DIR` is read directly (no dedicated
* getter exists in this codebase) so the vault root tracks the data volume.
*/
getGitSyncDataDir(): string {
const explicit = this.configService.get<string>('GIT_SYNC_DATA_DIR');
if (explicit) return explicit;
const dataDir = this.configService.get<string>('DATA_DIR') || './data';
return `${dataDir.replace(/\/+$/, '')}/git-sync`;
}
/** Optional remote template, e.g. `git@host:vault-{spaceId}.git`. */
getGitSyncRemoteTemplate(): string | undefined {
return this.configService.get<string>('GIT_SYNC_REMOTE_TEMPLATE');
}
/** Poll-safety interval in ms (default 15000). */
getGitSyncPollIntervalMs(): number {
return parseInt(
this.configService.get<string>('GIT_SYNC_POLL_INTERVAL_MS', '15000'),
);
}
/** Event debounce window in ms (default 2000). */
getGitSyncDebounceMs(): number {
return parseInt(
this.configService.get<string>('GIT_SYNC_DEBOUNCE_MS', '2000'),
);
}
/**
* The service user id git-sync writes are attributed to. Required when sync is
* enabled (validated in environment.validation.ts); optional otherwise.
*/
getGitSyncServiceUserId(): string | undefined {
return this.configService.get<string>('GIT_SYNC_SERVICE_USER_ID');
}
/** Optional path to the SSH key used for git remote access. */
getGitSyncSshKeyPath(): string | undefined {
return this.configService.get<string>('GIT_SYNC_SSH_KEY_PATH');
}
}