Skip to main content

EW-614 — Wire EverWorksGitProvider into createWork

Status: implemented (2026-05-13). This document is the in-tree companion to the Jira ticket [EW-614]. It describes what the wire-up changes, where, and how to verify.

EW-608 shipped the choice-driven onboarding wizard plus an env-gated EverWorksGitProvider service. The provider was built and unit-tested but never calledwork.storageProvider = 'ever-works-git' was persisted but the runtime repo create still fell through to the user's GitHub OAuth via the existing git facade. EW-614 closes that gap end-to-end.

After this change, picking Ever Works Git in the wizard produces a real private repository under github.com/ever-works-cloud, pushed to with the platform PAT (see EVER_WORKS_CUSTOMERS_GITHUB_PAT), never the user's personal credentials.


1. Scope at a glance

PieceWhereWhat changes
A. Pre-createpackages/agent/src/services/work-lifecycle.service.ts::createWorkIf storageProvider==='ever-works-git' AND everWorksGit.isEnabled(), pre-generate the Work UUID and call EverWorksGitProvider.createRepository(...) BEFORE the DB insert. Override workData.owner/organization/sourceRepository.relatedRepositories.work from the provider response.
B. Token resolverpackages/agent/src/facades/git.facade.ts::resolvePluginAndTokenNew short-circuit at the top of the resolver: when options.workId belongs to an ever-works-git Work AND the env flag is on, return the platform PAT before any user-OAuth lookup. Transparently routes ALL git-facade methods (create, push, pull, status, getRepository, ...).
C. Activity-log actorpackages/agent/src/events/work-created.event.ts + apps/api/src/activity-log/activity-log.listener.tsWorkCreatedEvent gains an optional platformActor payload. The activity-log listener forwards it under metadata.actor / metadata.repository so audit rows distinguish "platform acted on user's behalf" from a regular user-initiated create.
D. Testswork-lifecycle.create-defaults.spec.ts, git.facade.spec.ts, activity-log.listener.spec.ts, work.module.spec.ts11 new unit tests covering the platform-PAT happy path, flag-off fallback, non-ever-works-git fallback, missing-workId fallback, transient DB blip fallback, direct-token-wins precedence, and provider-error → HTTP-exception mapping.
E. Update guardpackages/agent/src/dto/update-work.dto.ts (no change needed)UpdateWorkDto already omits storageProvider, and the global ValidationPipe is whitelist: true. Clients cannot mutate the field after create.

"Piece D — schema-level actorKind column on activity_log" was filed as out-of-scope on the original EW-614 ticket (waiting for ≥2 non-user actor kinds before paying for the schema migration). The metadata-JSONB path is sufficient today.


2. Why pre-create the repo BEFORE the DB insert

Two reasons:

  1. No orphan DB rows. If GitHub rejects the create (rate-limit, org PAT revoked, network blip), the Work never enters our DB, so a retry by the user gets a clean slate.
  2. relatedRepositories.work captures the actual repo name. The provider's collision-suffix path (-{shortId}) means the resolved repo name can differ from the user's slug. Pre-creating lets us persist the full coordinates in sourceRepository on the first DB write, without a follow-up UPDATE.

The Work UUID is pre-generated with node:crypto.randomUUID() and passed to the provider so its collision-suffix derivation (work.id.replace(/-/g, '').slice(0, 7)) is deterministic; the same UUID is then handed to TypeORM as the row's id.

Failure cleanup

The DB insert can still fail AFTER the GitHub repo was created (slug collision on (userId, owner, slug), transient DB blip). When that happens today the ever-works-cloud repo is left orphaned. We didn't add a compensating delete because:

  • The slug-collision branch in workRepository.create was already a user-visible error before EW-614; one orphaned repo every few thousand creates is acceptable.
  • A best-effort EverWorksGitProvider.deleteRepository doesn't exist yet; adding it just for this rollback widens the surface unnecessarily.
  • A cleanup cron over ever-works-cloud repos with no matching Work row is a better long-term fix.

Tracked as a follow-up cleanup task on the EW-614 close-out comment.


3. Why the git-facade hook lives in resolvePluginAndToken

Every git-facade method (createRepository, push, pull, status, getRepository, hasRepositoryAccess, ...) funnels through resolvePluginAndToken(options). Adding the platform-PAT short-circuit at the top of that one method gives us:

  • Single edit point — no need to teach each facade method about storage-provider semantics.
  • No new API surface — the existing GitFacadeOptions.workId already flows through. Callers that don't pass workId (one-off lookups against arbitrary repos) get unchanged behaviour.
  • Test coverage via existing methods — we exercise createRepository in the new tests; the same short-circuit fires for push/pull/etc.

The resolver returns null (falls through to existing OAuth/PAT logic) when ANY of these is true:

  • options.workId is unset
  • STORAGE_EVER_WORKS_GIT_ENABLED is off
  • The Work doesn't exist or has a different storageProvider
  • EVER_WORKS_CUSTOMERS_GITHUB_PAT is empty (misconfig safety)
  • workRepository.findById throws (transient DB blip)

The last point — fall through on DB error — matches "what the user sees when the feature is off" and avoids crashing every git call when the Work-table read blips.


4. Activity-log payload

When the platform creates the repo, the WorkCreatedEvent is emitted with a platformActor payload:

new WorkCreatedEvent(work, {
actorKind: 'platform',
actor: 'ever-works-cloud',
repoFullName: 'ever-works-cloud/evereq-my-work',
htmlUrl: 'https://github.com/ever-works-cloud/evereq-my-work'
});

The listener records it under metadata:

{
"actor": { "kind": "platform", "id": "ever-works-cloud" },
"repository": {
"fullName": "ever-works-cloud/evereq-my-work",
"htmlUrl": "https://github.com/ever-works-cloud/evereq-my-work"
}
}

A regular user-initiated create (any non-Ever-Works-Git storage) still fires WorkCreatedEvent — we now emit it unconditionally, fixing a gap where the event was wired but never raised — and the activity row's metadata is undefined. Down-stream consumers (activity-log.listener, work-created.listener for user-research learning) handle both shapes.


5. Error mapping

EverWorksGitProvider throws typed errors. WorkLifecycleService maps them to HTTP-shaped NestJS exceptions so controllers don't have to:

Provider errorHTTP exceptionWhen it fires
EverWorksGitDisabledErrorBadRequestException (400)Feature flag was on at resolveProviderDefaults time but off by the time the provider executed — should be impossible, but mapped for safety
EverWorksGitMisconfiguredErrorServiceUnavailableException (503)PAT empty or org empty mid-flight (env was misconfigured between API startup and now)
EverWorksGitRequestErrorServiceUnavailableException (503)GitHub returned a non-2xx (rate-limit, 5xx, both {user}-{slug} AND {user}-{slug}-{shortId} already taken in the org)

The Work is NOT persisted in any of these cases (the provider call comes before workRepository.create).


6. Configuration touchpoints (unchanged from EW-608)

All env values are inherited from the EW-608 wiring (PR #721) — nothing new in .env.example, the k8s manifests, or the deploy workflows.

STORAGE_EVER_WORKS_GIT_ENABLED=true # gate
EVER_WORKS_CUSTOMERS_GITHUB_ORG=ever-works-cloud
EVER_WORKS_CUSTOMERS_GITHUB_PAT=github_pat_… # secret (GH Actions)
EVER_WORKS_CUSTOMERS_GITHUB_VISIBILITY=private

See docs/specs/features/onboarding-wizard-v2/deployment.md §1 for the rotation runbook.


7. Smoke-test recipe (post-cascade)

On any env (dev/stage/prod) with the flag enabled:

  1. Register a fresh user via POST /api/auth/register.
  2. Drive the wizard with default storage (Ever Works Git) OR call POST /api/works { slug, name, description, storageProvider: 'ever-works-git' } directly.
  3. Verify the response: work.owner === 'ever-works-cloud', work.organization === true, work.sourceRepository.relatedRepositories.work set.
  4. GET https://github.com/orgs/ever-works-cloud/repositories → the new private repo appears with name {user-slug}-{work-slug}.
  5. GET /api/activity-log?workId={id} → the WORK_CREATED row's metadata matches §4.
  6. Trigger any downstream git op (e.g. generate items, push README) — confirm the operation lands in ever-works-cloud and NOT the user's personal GitHub.
  7. Clean up: delete the repo via the API (uses the platform PAT transparently) + delete the test user.

8. Out-of-scope

  • Switching storageProvider after Work creation. UI doesn't expose it and UpdateWorkDto doesn't accept it. Future ticket if needed.
  • The "Ever Works Deploy" equivalent — blocked on k8s-works cluster + SSL termination, which is owner-provisioned separately.
  • Moving the platform PAT to a GitHub App installation token (cleaner long-term, eliminates manual PAT rotation). Separate ticket.
  • Schema-level actor_kind column on activity_log. Metadata-JSONB is enough until we have ≥2 non-user actor kinds.
  • Orphan-repo cleanup cron (see §2 "Failure cleanup").