.github/9
ISSUE_TEMPLATE/3bug_report.md config.yml feature_request.md
workflows/3ci.yml clean-clone.yml release.yml
CODEOWNERS dependabot.yml pull_request_template.md
apps/630
api/250
src/243
common/30
__tests__/3authorize.test.ts bigint-serialization.interceptor.test.ts files-limits.test.ts
filters/1all-exceptions.filter.ts
guards/3auth.guard.ts internal-service.guard.ts rate-limit.guard.ts
interceptors/2actor-context.interceptor.ts bigint-serialization.interceptor.ts
middleware/2idempotency.test.ts idempotency.ts
pipes/1zod-validation.pipe.ts
actor-context.ts audit.service.ts cache.test.ts cache.ts cookies.ts core.module.ts deprecation.ts email.service.ts errors.ts event-bus.ts log-scrubber.test.ts log-scrubber.ts logger.ts permission-cache-bridge.ts prisma.service.ts redis.service.ts trace-metadata.ts types.ts
config/1env.ts
lib/11
__tests__/5crypto.test.ts filter-builder.test.ts jwt.test.ts pagination.test.ts positions.test.ts
authorize.ts crypto.ts filter-builder.ts jwt.ts pagination.ts positions.ts
modules/197
ai/28
__tests__/4ai-writing.test.ts database-ai.test.ts generate-executor.test.ts prompts.test.ts
generator/4executor.ts generate.controller.ts generate.dto.ts generate.service.ts
ai-chat.service.ts ai-rate-limit.ts ai-writing.controller.ts ai-writing.dto.ts ai-writing.service.ts ai.controller.ts ai.module.ts ai.service.ts budget-guard.service.ts database-ai.controller.ts database-ai.dto.ts database-ai.service.ts db-prompts.ts dto.ts gateway-provider.ts ledger.service.ts prompts.ts rag.controller.ts rag.dto.ts rag.service.ts
api-keys/3api-keys.controller.ts api-keys.module.ts api-keys.service.ts
audit/2audit-log.controller.ts audit.module.ts
auth/4auth.controller.ts auth.module.ts auth.service.ts dto.ts
automations/5automations.controller.ts automations.module.ts automations.service.ts dto.ts webhook-incoming.controller.ts
blocks/5
__tests__/1batch-version-cas.test.ts
block.service.ts blocks.controller.ts blocks.module.ts dto.ts
comments/4comments.controller.ts comments.module.ts comments.service.ts dto.ts
databases/9
__tests__/3normalize-select-options.test.ts option-id-canonicalization.test.ts record-open-query-shape.test.ts
database.service.ts databases.controller.ts databases.module.ts dto.ts property-cache-schema.ts query.service.ts
docs/2docs.controller.ts docs.module.ts
files/10
__tests__/1quota.test.ts
dto.ts files.controller.ts files.module.ts files.service.ts quota.ts record.ts s3.ts uploads.controller.ts uploads.service.ts
forms/18debug.controller.ts form.dto.ts forms.controller.ts forms.module.ts forms.service.ts markdown.test.ts markdown.ts optional-auth.guard.ts public-forms.controller.ts public-forms.service.ts question.dto.ts questions.service.ts rate-limit.ts recaptcha.ts slug.ts submissions.controller.ts submissions.service.ts submit-schema.ts
health/3health.controller.ts health.module.ts health.service.ts
integrations/3integrations.controller.ts integrations.module.ts integrations.service.ts
internal/12
__tests__/2flush-query-shape.test.ts internal.endpoints.test.ts
ai-test-seam.controller.ts ai-test-seam.dto.ts ai-test-seam.service.ts internal.controller.ts internal.dto.ts internal.module.ts internal.service.ts test-seam.controller.ts test-seam.dto.ts test-seam.service.ts
notifications/3notifications.controller.ts notifications.module.ts notifications.service.ts
oauth/6
__tests__/1oauth.scopes.test.ts
oauth.controller.ts oauth.module.ts oauth.service.ts scopes.ts token-keys.ts
org/15audit.service.ts embed-preview.controller.ts guards.ts members.service.ts org-admin.controller.ts org-policy.service.ts org.module.ts overview.service.ts policy-effective.controller.ts policy.schema.ts policy.test.ts retention.service.ts totp.test.ts totp.ts two-factor.service.ts
orgs/5dto.ts orgs.controller.ts orgs.module.ts orgs.service.ts util.ts
pages/4dto.ts page.service.ts pages.controller.ts pages.module.ts
scim/15
__tests__/3email-mapping.test.ts etag.test.ts filters.test.ts
etag.ts filters.ts groups.service.ts schemas.ts scim-admin.controller.ts scim-admin.service.ts scim-auth.guard.ts scim-auth.service.ts scim-errors.ts scim.controller.ts scim.module.ts users.service.ts
search/4
__tests__/1search.service.test.ts
search.controller.ts search.module.ts search.service.ts
sessions/3sessions.controller.ts sessions.module.ts sessions.service.ts
share/3share.controller.ts share.module.ts share.service.ts
sso/16
__tests__/4enforcement.test.ts oidc.verify.test.ts provisioning.test.ts saml.validate.test.ts
enforcement.ts oidc.controller.ts oidc.service.ts oidc.ts provisioning.service.ts saml.controller.ts saml.ts schemas.ts sso-admin.controller.ts sso-admin.service.ts sso-secrets.ts sso.module.ts
teams/4dto.ts teams.controller.ts teams.module.ts teams.service.ts
users/4dto.ts users.controller.ts users.module.ts users.service.ts
webhooks/3webhooks.controller.ts webhooks.module.ts webhooks.service.ts
workspaces/4dto.ts workspaces.controller.ts workspaces.module.ts workspaces.service.ts
types/1xmldom.d.ts
app.module.ts constants.ts main.ts
_probe_rawbody.cjs .env.example Dockerfile package.json README.md tsconfig.json vitest.config.ts
realtime/33
src/17auth.ts comments.ts config.ts event-source.ts http-store.ts index.ts metrics.ts notifications.ts page-updated-source.ts presence.ts prisma-store.ts protocol.ts rate-limit.ts room-key.ts rooms.ts server.ts update-log.ts
test/11auth.test.ts config.test.ts event-source.test.ts http-store.test.ts metrics-merge.test.ts page-updated-source.test.ts presence.test.ts protocol.test.ts rate-limit.test.ts rooms.test.ts task-0094-builders.test.ts
.env.example Dockerfile package.json README.md tsconfig.json
web/255
src/244
app/45
__tests__/1theme-tokens.test.ts
(app)/26
w/25
[workspace]/25
ask/1page.tsx
databases/1
[id]/1page.tsx
favorites/1page.tsx
files/1page.tsx
notifications/1page.tsx
page/4
[id]/4
[block]/1page.tsx
share/1page.tsx
layout.tsx page.tsx
search/1page.tsx
settings/10
appearance/1page.tsx
automations/3
[id]/1page.tsx
new/1page.tsx
page.tsx
billing/1page.tsx
developers/1page.tsx
members/1page.tsx
permissions/1page.tsx
teams/1page.tsx
page.tsx
shared/1page.tsx
templates/1page.tsx
trash/1page.tsx
layout.tsx page.tsx
layout.tsx
(auth)/4
login/1page.tsx
register/1page.tsx
reset-password/1page.tsx
layout.tsx
api/2
metrics/1route.ts
v1/1
health/1route.ts
f/1
[slug]/1page.tsx
health/1
ready/1route.ts
org/2
[orgId]/2
admin/2
integrations/1page.tsx
page.tsx
share/1
[id]/1page.tsx
error.tsx global-error.tsx globals.css layout.tsx not-found.tsx page.tsx providers.tsx
components/118
admin/10
org-admin/8ApiKeysTab.tsx AuditTab.tsx MembersTab.tsx OverviewTab.tsx PoliciesTab.tsx RetentionTab.tsx WebhooksTab.tsx WorkspacePicker.tsx
ScimCard.tsx SsoTabs.tsx
ai/6ai-panel.tsx ask-ai-submenu.tsx db-ai-dialog.tsx generate-page-dialog.tsx writing-actions-menu.tsx writing-diff.tsx
automations/5action-editor.tsx condition-editor.tsx lib.ts run-history.tsx trigger-editor.tsx
collaboration/6CursorLayer.tsx OfflineBanner.tsx PresenceBar.tsx README.md ReviewFailedDialog.tsx StatusChip.tsx
comments/3comment-panel.tsx comment-thread.tsx mention-input.tsx
database/27
__tests__/2cell-editor.test.tsx cell-renderer.test.tsx
AiExtractDialog.tsx AiFormulaDialog.tsx AiGenerateDialog.tsx board-view.tsx buttons-panel.tsx calendar-view.tsx cell-editor.tsx cell-renderer.tsx chart-view.tsx database-page-view.tsx database-toolbar.tsx database-view-renderer.tsx db-embedded.tsx filter-builder.tsx form-builder-view.tsx formula-editor.tsx gallery-view.tsx group-builder.tsx list-view.tsx property-editor.tsx record-header.tsx record-panel.tsx sort-builder.tsx table-view.tsx timeline-view.tsx
dialogs/7
__tests__/1move-to-dialog.test.tsx
create-dialog.tsx dialog.tsx file-permissions-dialog.tsx invite-dialog.tsx move-to-dialog.tsx share-dialog.tsx
editor/19
__tests__/3code-block-text.test.ts code-block.test.tsx expandable.test.tsx
block-card.tsx block-renderer.tsx block-save-queue.ts block-toolbar.tsx context-menu.tsx expandable.tsx history-popover.tsx inline-editor.tsx insert-dialog.tsx page-canvas.tsx page-spinner.tsx page-view.tsx selection-toolbar.tsx slash-menu.tsx template-dialog.tsx type-menu.tsx
files/4FileBlock.tsx FileBrowser.tsx FilePicker.tsx PreviewModal.tsx
form/4form-field.tsx form-renderer.tsx form-responses-view.tsx form-view.tsx
layout/8
__tests__/1page-tree-icon.test.tsx
command-palette.tsx factory-logo.tsx page-tree-node.tsx sidebar.tsx theme-provider.tsx topbar.tsx workspace-shell.tsx
mentions/3
__tests__/1mention-model.test.ts
mention-model.ts MentionPopup.tsx
search/3ask-ai-search.tsx results-list.tsx search-dialog.tsx
settings/10
api-keys/4ApiKeyCreateDialog.tsx ApiKeyRevealDialog.tsx ApiKeyTable.tsx ScopesBadge.tsx
webhooks/5CurlExamples.tsx DeliveryInspector.tsx EventExplorer.tsx OverviewTab.tsx WebhooksTab.tsx
settings-shell.tsx
ui/3confirm-dialog.tsx form.tsx toaster.tsx
hooks/21
__tests__/4realtime-frame-handler.test.ts use-blocks.test.ts use-database-options.test.ts use-workspace.test.tsx
realtime-frame-handler.ts use-ai-stream.ts use-auth.ts use-blocks.ts use-database.ts use-db-ai-stream.ts use-forms.ts use-members.ts use-mention-sources.ts use-notifications.ts use-offline-status.ts use-pages.ts use-presence-announcer.ts use-presence-toasts.ts use-presence.ts use-realtime.ts use-workspace.ts
lib/50
__tests__/21ai-database.test.ts ai-generate.test.ts ai-sse.test.ts ai-writing.test.ts api-keys.test.ts api-members.test.ts auth-refresh.test.ts auth-store.test.ts batch-queue-drop.test.ts block-content.test.ts clipboard-import.test.ts code-highlight.test.ts offline-status.test.ts presence-announcer.test.ts presence-color.test.ts presence-cursor.test.ts presence-emitter.test.ts realtime-url.test.ts search.test.ts utils.test.ts writing-diff.test.ts
files/7
__tests__/2picker.test.ts upload.test.ts
api.ts picker.ts upload-store.ts upload.ts use-file-uploads.ts
ai-database.ts ai.ts api-keys.ts api.ts auth-store.ts batch-queue.ts block-content.ts clipboard-import.ts code-highlight.ts constants.ts offline-status.ts org-admin.ts presence-announcer.ts presence-color.ts presence-cursor.ts presence-editor-views.ts presence-emitter.ts query-client.ts realtime.ts search.ts types.ts utils.ts
stores/8
__tests__/1block-patch-store.test.ts
block-patch-store.ts comment-pill-store.ts database-store.ts editor-store.ts offlineQueue.ts presence-store.ts ui-store.ts
instrumentation.ts middleware.ts
.env.example Dockerfile next-env.d.ts next.config.mjs package.json postcss.config.mjs README.md tailwind.config.ts tsconfig.json tsconfig.tsbuildinfo vitest.config.ts
worker/92
src/87
__tests__/13ai-embedding.test.ts ai-request.test.ts ai-usage-budget.test.ts audit-export.test.ts email-decision.test.ts encrypt.test.ts health-search-index-lag.test.ts mime.test.ts retention.test.ts s3-sign.test.ts search-index.test.ts smtp.test.ts webhook-sign.test.ts
health/1server.ts
lib/12
__tests__/1clamav.test.ts
clamav.ts encrypt.ts job-otel.ts mime.ts notify.ts prisma.ts redis-stream.ts redis.ts s3.ts sharp.ts smtp.ts
processors/56
automations/28
__tests__/3condition-evaluator.spec.ts executors.spec.ts run-pipeline.spec.ts
executors/17assign-user.ts call-integration.ts change-status.ts classify.ts create-notification.ts create-page.ts email-cap.ts helpers.ts integration-registry.ts permission.ts record-write.ts registry.ts run-ai-action.ts send-email.ts send-webhook.ts types.ts update-record.ts
condition-evaluator.ts dead-letter.ts log.ts matcher.ts pipeline.ts run-state.ts schedule.ts watchdog.ts
files/15
__tests__/4errors.test.ts scan-gate.test.ts status.test.ts temp.test.ts
steps/4finalize.ts metadata.ts scan.ts thumbnails.ts
cleanup.ts errors.ts generateThumbs.ts processor.ts status.ts temp.ts types.ts
search-index/4builders.ts grants.ts index.ts processor.ts
ai-embedding.ts ai.processor.ts audit-export.ts automations.processor.ts email-decision.processor.ts email.processor.ts maintenance.processor.ts retention.ts webhooks.processor.ts
config.ts events.ts index.ts logger.ts queues.ts
.env.example Dockerfile package.json README.md tsconfig.json
docs/73
api/17ai.md authentication.md automations.md blocks.md comments.md databases.md files.md forms.md openapi.yaml org-admin.md pages.md scim.md search.md sso.md versioning.md webhooks.md workspaces.md
architecture/10ai-architecture.md automation.md backend-architecture.md database-architecture.md frontend-architecture.md realtime-architecture.md search-architecture.md security-architecture.md storage.md system-architecture.md
database/2database-architecture.md redis-keys.md
development/8admin-guide.md coding-standards.md deployment.md development-guide.md files-pipeline.md perf-report.md performance.md testing.md
product/4functional-requirements.md non-functional-requirements.md product-requirements.md ux-requirements.md
requirements/1requirements-matrix.md
security/5audit-report.md files.md forms.md license-allowlist.md security-architecture.md
tasks/16phase-00-foundation.md phase-01-authentication.md phase-02-workspace.md phase-03-pages.md phase-04-editor.md phase-05-databases.md phase-06-relations.md phase-07-collaboration.md phase-08-search.md phase-09-files.md phase-10-forms.md phase-11-automation.md phase-12-ai.md phase-13-api.md phase-14-enterprise.md phase-15-production.md
ui/9ai.md automations.md design-system.md developers.md files.md forms.md org-admin.md scim.md sso.md
support-matrix.md
infrastructure/71
caddy/1Caddyfile
docker/6
init/101-extensions.sql
opensearch/1init-indices.json
.env.docker docker-compose.yml opensearch-init.sh README.md
kubernetes/40
observability/20
alertmanager/1alertmanager.yaml
grafana/8
dashboards/6ai.json api-overview.json infra.json realtime.json search.json workers-queues.json
provisioning/2
dashboards/1dashboards.yaml
datasources/1datasource.yaml
prometheus/3alerts.yaml recording-rules.yaml scrape.yaml
scripts/1sync-sources.mjs
alertmanager.yaml grafana.yaml kustomization.yaml namespace.yaml node-exporter.yaml otel-collector.yaml prometheus.yaml
postgres/7configmap.yaml cronjob-backup.yaml cronjob-opensearch-snapshot.yaml pgbouncer.yaml replica.yaml statefulset.yaml walg-iam-policy.yaml
api.yaml configmap.yaml ingress.yaml kustomization.yaml namespace.yaml networkpolicy.yaml pdb.yaml README.md realtime.yaml redis.yaml secret.example.yaml web.yaml worker.yaml
nginx/2nginx.conf README.md
observability/14
alertmanager/1alertmanager.yaml
grafana/8
dashboards/6ai.json api-overview.json infra.json realtime.json search.json workers-queues.json
provisioning/2
dashboards/1dashboards.yaml
datasources/1datasource.yaml
prometheus/3alerts.yaml recording-rules.yaml scrape.yaml
scripts/1dashboard-completeness.mjs
README.md
terraform/8compute.tf data.tf main.tf network.tf outputs.tf README.md security.tf variables.tf
packages/289
ai/24
src/20
providers/7anthropic.ts google.ts http.ts index.ts openai-compatible.ts rag.test.ts writing.test.ts
schemas/2database.ts generate.ts
adapters.test.ts config.test.ts config.ts gateway.test.ts gateway.ts index.ts tools.test.ts tools.ts types.ts usage.test.ts usage.ts
package.json README.md tsconfig.json vitest.config.ts
auth/11
src/7index.ts passwords.test.ts passwords.ts sessions.test.ts sessions.ts tokens.test.ts tokens.ts
package.json README.md tsconfig.json vitest.config.ts
collaboration/17
src/13awareness.ts codec.test.ts codec.ts index.ts lww.test.ts lww.ts mapping.ts offline.ts outbox.test.ts outbox.ts provider.test.ts provider.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
config/7
src/4eslint.base.js index.ts prettier.base.json tsconfig.base.json
package.json README.md tsconfig.json
database-client/5
src/2index.test.ts index.ts
package.json README.md tsconfig.json
database-engine/33
src/29
filter/2filter.test.ts index.ts
formula/8evaluator.ts formula.test.ts functions.ts index.ts lexer.ts parser.ts types.ts validate.ts
query/6group.ts index.ts paginate.ts pivot.ts query.test.ts sort.ts
relations/2index.ts relations.test.ts
rollups/2index.ts rollups.test.ts
values/5dates.ts index.ts registry.ts types.ts values.test.ts
views/3buttons.ts index.ts views.test.ts
index.ts
package.json README.md tsconfig.json vitest.config.ts
editor/25
src/21
marks/1highlight.ts
nodes/2equation.ts mention.ts
autosave.test.ts autosave.ts clipboard.test.ts clipboard.ts codec.test.ts codec.ts context-menu.test.ts context-menu.ts diff.test.ts diff.ts factory.ts history.test.ts history.ts index.ts positions.ts slash.test.ts slash.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
observability/20
src/16
__tests__/8db-redis-metrics.test.ts metrics.test.ts otel.test.ts pino-otel.test.ts prometheus.test.ts resource.test.ts scrubber.test.ts traceparent.test.ts
index.ts metrics.ts otel.ts pino-otel.ts prometheus.ts resource.ts scrubber.ts traceparent.ts
package.json README.md tsconfig.json vitest.config.ts
permissions/11
src/7index.ts levels.test.ts levels.ts resolve.test.ts resolve.ts rules.test.ts rules.ts
package.json README.md tsconfig.json vitest.config.ts
search/22
src/18chunking.test.ts chunking.ts client.test.ts client.ts embeddings.test.ts embeddings.ts hybrid.test.ts hybrid.ts index.ts indexer.test.ts indexer.ts mapping.test.ts mapping.ts queries.test.ts queries.ts rrf.test.ts rrf.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
shared/61
src/57
dto/17ai.dto.ts auth.dto.test.ts auth.dto.ts block.dto.ts browser-safety.test.ts comment.dto.ts database.dto.test.ts database.dto.ts file.dto.ts formula-ast.ts index.ts notification.dto.ts page.dto.ts search.dto.ts share.dto.ts webhook.dto.ts workspace.dto.ts
events/8audit-catalog.test.ts audit-catalog.ts audit-csv.test.ts audit-csv.ts audit-scope.test.ts audit-scope.ts catalog.test.ts catalog.ts
realtime/6comment-events.ts frames.test.ts frames.ts notification-policy.ts notification-prefs.ts task-0094.test.ts
automation.test.ts automation.ts constants.ts crypto.test.ts crypto.ts env.ts errors.ts events.test.ts events.ts forms.test.ts forms.ts index.ts markdown.test.ts markdown.ts mentions.test.ts mentions.ts net.test.ts net.ts positions.test.ts positions.ts presence.test.ts presence.ts security-headers.ts webhook-delivery.test.ts webhook-delivery.ts webhook-events.ts
package.json README.md tsconfig.json vitest.config.ts
storage/16
src/12client.test.ts client.ts index.ts keys.test.ts keys.ts mime.test.ts mime.ts presign.test.ts presign.ts thumbnails.test.ts thumbnails.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
ui/37
src/33
components/19avatar.tsx badge.tsx button.tsx checkbox.tsx command.tsx context-menu.tsx dialog.tsx dropdown-menu.tsx empty-state.tsx index.ts popover.tsx separator.tsx skeleton.tsx slider.tsx spinner.tsx switch.tsx tabs.tsx toast.tsx tooltip.tsx
color.ts command-filter.test.ts command-filter.ts focus.test.ts focus.ts formatters.test.ts formatters.ts index.ts theme.test.ts theme.ts toast-store.test.ts toast-store.ts tokens.test.ts tokens.ts
package.json README.md tsconfig.json vitest.config.ts
prisma/16
migrations/13
20260927000000_init/1migration.sql
20260928000000_search_documents_content_hash/1migration.sql
20260929000000_workspace_usage_storage_bytes/1migration.sql
20260930000000_form_submitted_notification_type/1migration.sql
20261001000000_ai_phase12/1migration.sql
20261002000000_webhooks_secret_encrypted_dlq/1migration.sql
20261002111436_webhook_delivery_latency/1migration.sql
20261002120000_org_admin_2fa_retention_unique/1migration.sql
20261002215124_audit_export/1migration.sql
20261003210000_perf_indexes_hot_paths/1migration.sql
20261004004500_perf_indexes_hot_paths_2/1migration.sql
migration_lock.toml README.md
schema.prisma seed.ts verify.ts
scripts/7backup-health.ts docs-completeness.mjs generate-openapi.mjs minio-init.ts opensearch-reindex.ts storage-orphans.ts verify-clean-clone.sh
tests/169
e2e/63
.auth/1user.json
fixtures/7
files/6clip.mp4 clip.wav clip.webm hero-1400.png note.docx note.txt
realtime.ts
mocks/2oidc-issuer.ts saml-idp.ts
playwright-report/3
data/2a6557bae9c2714a658968d4eb8534195c08ebda8.png e3092d07d00a16063e46385c02a559a9054fa847.webm
index.html
reports/1files-security.json
specs/34ai-database.spec.ts ai-generate.spec.ts ai-rag.spec.ts ai-security.spec.ts ai-writing.spec.ts ai.spec.ts api-keys.spec.ts appearance-theme.spec.ts auth.spec.ts automation.spec.ts collab-two-tab.spec.ts comments.spec.ts databases.spec.ts default-workspace-alias.spec.ts developers.spec.ts editor.spec.ts files.spec.ts final-flow.spec.ts forms.spec.ts oauth.spec.ts org-admin.spec.ts pages.spec.ts performance.spec.ts presence-cursors.spec.ts qa-paste-toggle.spec.ts realtime.spec.ts scim.spec.ts search-index-lag.spec.ts search.spec.ts sharing.spec.ts sso.spec.ts task-0094-comments-notifications.spec.ts webhooks.spec.ts workspace.spec.ts
test-results-final-flow/3
final-flow-Release-v1-0-fi-6c06b-w-state-carries-sequential--chromium-desktop/2test-failed-1.png trace.zip
.last-run.json
utils/4api.ts dnd.ts search-lag.ts wait.ts
.gitignore fixtures.ts global-setup.ts package.json playwright.config.ts playwright.final-flow.config.ts README.md tsconfig.json
fixtures/1
files/1make-sparse.ts
integration/60
ai-capability/2ai-app.capability.test.ts gateway.capability.test.ts
drill/2seed-drill.mjs verify-restore.mjs
fixtures/5
files/5clip.mp4 clip.wav doc.pdf hero-1400.png note.txt
helpers/3api.ts containers.ts seed.ts
realtime/11control-channel.test.ts convergence.test.ts flush-rebase-http.test.ts handshake.test.ts heartbeat-limits.test.ts lifecycle.test.ts presence.test.ts rebase.test.ts resume.test.ts rt-helpers.ts task-0094-fanout.test.ts
ai-chat.integration.test.ts ai-database.integration.test.ts ai-generate.integration.test.ts ai-rag.integration.test.ts ai-writing.integration.test.ts api-keys.integration.test.ts audit-completeness.integration.test.ts audit-export-s3.integration.test.ts auth.integration.test.ts automation-pipeline.integration.test.ts automations.integration.test.ts blocks.integration.test.ts cache-invalidation.integration.test.ts comments.integration.test.ts databases.integration.test.ts files-lifecycle.integration.test.ts files-pipeline.integration.test.ts files-security.integration.test.ts files.integration.test.ts forms-responses.integration.test.ts forms.integration.test.ts oauth.integration.test.ts openapi.contract.test.ts org-admin.integration.test.ts package.json pages.integration.test.ts permissions.integration.test.ts README.md replica-routing.integration.test.ts retention-engine.integration.test.ts scim.integration.test.ts search.integration.test.ts sso.integration.test.ts tsconfig.json vitest.config.ts webhooks.integration.test.ts workspace.integration.test.ts
performance/28
fixtures/1search-100k.ts
helpers/2seed-100k.mjs seed-1k-load.mjs
reports/7api-load-smoke.json database-load-smoke.json editor-local.json realtime-load.json search-full.json search-last.json search-reduced.json
api-load.js api.js archive.tar database-load.js database.js editor-local.mjs extract-100k-env.mjs package.json README.md realtime-1k.js realtime-load.js realtime.js run-realtime-load.sh search-search.k6.js search-summary.json seed-100k.mjs threshold-breach-demo.js verify-zero-op-loss.mjs
security/16
helpers/1app.ts
audit-report.test.ts auth-paths.test.ts deps.test.ts formula-fuzz.test.ts gitleaks-config.test.ts headers.test.ts images.test.ts oracle.ts package.json permission-fuzz.test.ts rate-limits.test.ts README.md tsconfig.json upload-abuse.test.ts vitest.config.ts
README.md
.dockerignore .env .env.bak-task0125 .env.example .eslintrc.json .gitignore .npmrc .prettierignore .prettierrc.json AGENTS.md ARCHITECTURE.md CHANGELOG.md CONTRIBUTING.md gitleaks.toml LICENSE package.json pnpm-lock.yaml pnpm-workspace.yaml PRODUCT-SPEC.md README.md ROADMAP.md SECURITY.md tsconfig.json turbo.json
release.yml.github/workflows/release.ymlraw← workflows42,041 B← Back to the app
1# =============================================================================
2# AI Software Factory — Release v1.0 (TASK-0135, Phase 15)
3#
4# 12-stage release pipeline. The TAG IS THE LAST ACT: nothing in this file is
5# triggered by a tag (the old `push: tags:` trigger is REMOVED — the tag is
6# created BY stage 12, so a tag push can never re-enter this workflow).
7#
8#   Trigger ......... workflow_dispatch only:
9#                       from_stage      1–12 (default 1) — re-runnable per stage
10#                       release_version semver (default 1.0.0)
11#   Stage gate ...... a single `gate` job outputs `from` (the start stage);
12#                     stage N runs iff `needs.gate.outputs.from == N`. Every
13#                     stage also `needs` every predecessor, so a stage can
14#                     never start on a red predecessor (spec rule).
15#   Re-runnability .. `from_stage: K` re-runs stages K..12 only. Stage 12 is
16#                     idempotent: it re-pushes the same image tags/digests and
17#                     never moves an existing tag.
18#
19#   Stages:
20#     1  install+lint+typecheck   7  smoke            (Phase 0 smoke vs staging)
21#     2  unit                     8  performance      (4 k6/local suites, HARD)
22#     3  integration              9  backup-health    (26h/10min/26h, HARD gate)
23#     4  security                 10 final-e2e        (tests/e2e/final-flow.spec.ts)
24#     5  build                    11 docs-completeness (scripts/docs-completeness.mjs)
25#     6  deploy-staging           12 push+tag         (GHCR × 4, annotated tag,
26#                                                      release notes — LAST)
27#
28#   Permissions: contents: write (tag + release), packages: write (GHCR push),
29#   actions: read (run link for the release notes).
30# =============================================================================
31name: release
32 
33on:
34  workflow_dispatch:
35    inputs:
36      from_stage:
37        description: "Stage to start at (1-12; re-runnable per stage)"
38        required: false
39        default: "1"
40        type: string
41      release_version:
42        description: "Release version (semver, no leading v)"
43        required: false
44        default: "1.0.0"
45        type: string
46 
47# A release should never be canceled by a newer dispatch mid-publish.
48concurrency:
49  group: release-${{ github.workflow }}-${{ github.ref }}
50  cancel-in-progress: false
51 
52permissions:
53  contents: write
54  packages: write
55  actions: read
56 
57env:
58  REGISTRY: ghcr.io
59  IMAGE_OWNER: ${{ github.repository }}
60  PNPM_VERSION: "9.15.0" # keep in sync with "packageManager" in package.json
61 
62jobs:
63  # ---------------------------------------------------------------------------
64  # gate — stage orchestrator (validates inputs, computes the start stage).
65  # ---------------------------------------------------------------------------
66  gate:
67    name: "Gate: resolve version + start stage"
68    runs-on: ubuntu-24.04
69    timeout-minutes: 5
70    outputs:
71      version: ${{ steps.resolve.outputs.version }}
72      is_patch: ${{ steps.resolve.outputs.is_patch }}
73      from: ${{ steps.resolve.outputs.from }}
74    steps:
75      - name: Resolve version + start stage
76        id: resolve
77        env:
78          RAW_VERSION: ${{ github.event.inputs.release_version }}
79          RAW_FROM: ${{ github.event.inputs.from_stage }}
80        run: |
81          set -euo pipefail
82          V="${RAW_VERSION:-1.0.0}"
83          V="${V#v}"
84          case "$V" in
85            *[!0-9.]*|*..*|.*|*.*) echo "invalid semver version: ${RAW_VERSION:-1.0.0}" >&2; exit 1 ;;
86          esac
87          MAJOR="$(cut -d. -f1 <<<"$V")"
88          MINOR="$(cut -d. -f2 <<<"$V")"
89          PATCH="$(cut -d. -f3 <<<"$V")"
90          if ! [[ "$MAJOR$MINOR$PATCH" =~ ^[0-9]+$ ]]; then
91            echo "version components must be non-negative integers: $V" >&2; exit 1
92          fi
93          # "Patch bump" = a release on the vX.0.0 line (minor=0 AND patch=0)
94          # → those releases also publish the `latest` tag.
95          if [ "$MINOR" = "0" ] && [ "$PATCH" = "0" ]; then IS_PATCH=true; else IS_PATCH=false; fi
96          # from_stage: clamp into 1..12 (empty/invalid → 1 = full pipeline).
97          F="${RAW_FROM:-1}"
98          if ! [[ "$F" =~ ^[0-9]+$ ]] || [ "$F" -lt 1 ] || [ "$F" -gt 12 ]; then
99            echo "from_stage must be 1..12 (got '${RAW_FROM:-1}') — using 1" >&2
100            F=1
101          fi
102          echo "version=$V" >> "$GITHUB_OUTPUT"
103          echo "is_patch=$IS_PATCH" >> "$GITHUB_OUTPUT"
104          echo "from=$F" >> "$GITHUB_OUTPUT"
105          echo "::notice::release=$V (latest-tag: $IS_PATCH) — running stages ${F}..12"
106 
107  # ---------------------------------------------------------------------------
108  # Stage 1 — install+lint+typecheck (hard).
109  # ---------------------------------------------------------------------------
110  stage-1-lint-typecheck:
111    name: "[1/12] install + lint + typecheck"
112    runs-on: ubuntu-24.04
113    timeout-minutes: 30
114    needs: [gate]
115    if: needs.gate.outputs.from == 1
116    steps:
117      - name: Checkout
118        uses: actions/checkout@v4
119      - name: Setup pnpm
120        uses: pnpm/action-setup@v4
121        with:
122          version: ${{ env.PNPM_VERSION }}
123      - name: Setup Node.js
124        uses: actions/setup-node@v4
125        with:
126          node-version: 20
127          cache: pnpm
128      - name: Install (frozen lockfile)
129        run: pnpm install --frozen-lockfile
130      - name: ESLint
131        run: pnpm lint
132      - name: Typecheck (tsc strict via turbo)
133        run: pnpm typecheck
134 
135  # ---------------------------------------------------------------------------
136  # Stage 2 — unit (hard): Vitest (packages) + Jest (api).
137  # ---------------------------------------------------------------------------
138  stage-2-unit:
139    name: "[2/12] unit tests"
140    runs-on: ubuntu-24.04
141    timeout-minutes: 30
142    needs: [gate, stage-1-lint-typecheck]
143    if: needs.gate.outputs.from == 2
144    steps:
145      - name: Checkout
146        uses: actions/checkout@v4
147      - name: Setup pnpm
148        uses: pnpm/action-setup@v4
149        with:
150          version: ${{ env.PNPM_VERSION }}
151      - name: Setup Node.js
152        uses: actions/setup-node@v4
153        with:
154          node-version: 20
155          cache: pnpm
156      - name: Install (frozen lockfile)
157        run: pnpm install --frozen-lockfile
158      - name: "Unit tests (turbo: vitest packages + jest api)"
159        run: pnpm test:unit
160 
161  # ---------------------------------------------------------------------------
162  # Stage 3 — integration (hard): tests/integration against live services
163  # (same service block + env as the CI integration job).
164  # ---------------------------------------------------------------------------
165  stage-3-integration:
166    name: "[3/12] integration tests"
167    runs-on: ubuntu-24.04
168    timeout-minutes: 45
169    needs: [gate, stage-1-lint-typecheck, stage-2-unit]
170    if: needs.gate.outputs.from == 3
171    services:
172      postgres:
173        image: postgres:16-alpine
174        env:
175          POSTGRES_USER: postgres
176          POSTGRES_PASSWORD: postgres
177          POSTGRES_DB: factory
178        ports:
179          - 5432:5432
180        options: >-
181          --health-cmd "pg_isready -U postgres -d factory"
182          --health-interval 5s
183          --health-timeout 5s
184          --health-retries 12
185      redis:
186        image: redis:7-alpine
187        ports:
188          - 6379:6379
189        options: >-
190          --health-cmd "redis-cli ping"
191          --health-interval 5s
192          --health-timeout 3s
193          --health-retries 12
194      minio:
195        image: minio/minio
196        command: server /data --console-address ":9001"
197        env:
198          MINIO_ROOT_USER: minioadmin
199          MINIO_ROOT_PASSWORD: minioadmin
200        ports:
201          - 9000:9000
202        options: >-
203          --health-cmd "curl -fsS http://localhost:9000/minio/health/live"
204          --health-interval 5s
205          --health-timeout 5s
206          --health-retries 12
207      opensearch:
208        image: opensearchproject/opensearch:2
209        env:
210          discovery.type: single-node
211          plugins.security.disabled: "true"
212          OPENSEARCH_JAVA_OPTS: "-Xms512m -Xmx512m"
213        ports:
214          - 9200:9200
215        # OpenSearch is SLOW on GitHub runners — wait explicitly (below).
216    env:
217      DATABASE_URL: postgresql://postgres:postgres@localhost:5432/factory?schema=public
218      REDIS_URL: redis://localhost:6379
219      SEARCH_URL: http://localhost:9200
220      SEARCH_USERNAME: ""
221      SEARCH_PASSWORD: ""
222      SEARCH_INDEX_PREFIX: factory
223      S3_ENDPOINT: http://localhost:9000
224      S3_ACCESS_KEY: minioadmin
225      S3_SECRET_KEY: minioadmin
226      S3_BUCKET: factory-uploads
227      S3_REGION: us-east-1
228      S3_FORCE_PATH_STYLE: "true"
229      # Test-only secrets (never real values).
230      JWT_SECRET: release-test-jwt-secret
231      REFRESH_TOKEN_SECRET: release-test-refresh-secret
232      SESSION_SECRET: release-test-session-secret
233      MASTER_ENCRYPTION_KEY: "cmVsZWFzZS10ZXN0LW1hc3Rlci1rZXktMzJieXRlcyEh" # "release-test-master-key-32bytes!!"
234    steps:
235      - name: Checkout
236        uses: actions/checkout@v4
237      - name: Setup pnpm
238        uses: pnpm/action-setup@v4
239        with:
240          version: ${{ env.PNPM_VERSION }}
241      - name: Setup Node.js
242        uses: actions/setup-node@v4
243        with:
244          node-version: 20
245          cache: pnpm
246      - name: Install (frozen lockfile)
247        run: pnpm install --frozen-lockfile
248      - name: Wait for OpenSearch
249        run: |
250          for i in $(seq 1 60); do
251            if curl -sf http://localhost:9200/_cluster/health >/dev/null 2>&1; then
252              echo "opensearch is up after ${i} attempts"; exit 0
253            fi
254            sleep 5
255          done
256          echo "opensearch did not start in 300s" >&2; exit 1
257      - name: Create MinIO bucket + OpenSearch index
258        run: |
259          aws s3api create-bucket \
260            --endpoint-url http://localhost:9000 \
261            --bucket factory-uploads \
262            --region us-east-1 || true
263          curl -sf -X PUT "http://localhost:9200/factory-docs" \
264            -H 'Content-Type: application/json' -d '{}' || true
265          curl -sf -X POST "http://localhost:9200/factory-docs/_refresh" >/dev/null || true
266      - name: Prisma generate
267        run: pnpm db:generate
268      - name: Apply migrations
269        run: pnpm db:deploy
270      - name: Seed
271        run: pnpm db:seed
272      - name: Integration tests (turbo)
273        run: pnpm test:integration
274 
275  # ---------------------------------------------------------------------------
276  # Stage 4 — security (hard): TASK-0134's 10 suites + pnpm audit + gitleaks.
277  # Mirrors the CI security job (live dev-mode stack: the suites assert dev
278  # behavior — NODE_ENV must NOT be production).
279  # ---------------------------------------------------------------------------
280  stage-4-security:
281    name: "[4/12] security scan"
282    runs-on: ubuntu-24.04
283    timeout-minutes: 60
284    needs: [gate, stage-3-integration]
285    if: needs.gate.outputs.from == 4
286    services:
287      postgres:
288        image: postgres:16-alpine
289        env:
290          POSTGRES_USER: postgres
291          POSTGRES_PASSWORD: postgres
292          POSTGRES_DB: factory
293        ports:
294          - 5432:5432
295        options: >-
296          --health-cmd "pg_isready -U postgres -d factory"
297          --health-interval 5s
298          --health-timeout 5s
299          --health-retries 12
300      redis:
301        image: redis:7-alpine
302        ports:
303          - 6379:6379
304        options: >-
305          --health-cmd "redis-cli ping"
306          --health-interval 5s
307          --health-timeout 3s
308          --health-retries 12
309      minio:
310        image: minio/minio
311        command: server /data --console-address ":9001"
312        env:
313          MINIO_ROOT_USER: minioadmin
314          MINIO_ROOT_PASSWORD: minioadmin
315        ports:
316          - 9000:9000
317        options: >-
318          --health-cmd "curl -fsS http://localhost:9000/minio/health/live"
319          --health-interval 5s
320          --health-timeout 5s
321          --health-retries 12
322      opensearch:
323        image: opensearchproject/opensearch:2
324        env:
325          discovery.type: single-node
326          plugins.security.disabled: "true"
327          OPENSEARCH_JAVA_OPTS: "-Xms512m -Xmx512m"
328        ports:
329          - 9200:9200
330    env:
331      # Dev-mode stack (the live security suites assert dev behavior: no
332      # HSTS, no Secure cookie on plain-HTTP :4000) — NODE_ENV stays dev.
333      DATABASE_URL: postgresql://postgres:postgres@localhost:5432/factory?schema=public
334      REDIS_URL: redis://localhost:6379
335      SEARCH_URL: http://localhost:9200
336      SEARCH_USERNAME: ""
337      SEARCH_PASSWORD: ""
338      SEARCH_INDEX_PREFIX: factory
339      S3_ENDPOINT: http://localhost:9000
340      S3_ACCESS_KEY: minioadmin
341      S3_SECRET_KEY: minioadmin
342      S3_BUCKET: factory-uploads
343      S3_REGION: us-east-1
344      S3_FORCE_PATH_STYLE: "true"
345      JWT_SECRET: release-sec-jwt-secret
346      REFRESH_TOKEN_SECRET: release-sec-refresh-secret
347      SESSION_SECRET: release-sec-session-secret
348      MASTER_ENCRYPTION_KEY: "cmVsZWFzZS1zZWMtbWFzdGVyLWtleS0zMmJ5dGVzISE="
349      INTERNAL_SERVICE_TOKEN: release-sec-internal-service-token
350      INTERNAL_TEST_SEAM: "1"
351      AI_PROVIDER: mock
352      SEARCH_VECTOR_DIMENSION: "8"
353      AI_MOCK_DELTA_DELAY_MS: "60"
354      # Live-app security suites: a down stack FAILS the build (not skips).
355      SEC_REQUIRE_LIVE: "1"
356      # Global rate-limit buckets raised (as in CI) so the other suites don't
357      # trip them; the two fixed buckets (webhook 60/min, form 5/min) are
358      # env-independent and stay live-testable.
359      RATE_LIMIT_API: "100000"
360      RATE_LIMIT_AUTH: "10000"
361    steps:
362      - name: Checkout
363        uses: actions/checkout@v4
364      - name: Setup pnpm
365        uses: pnpm/action-setup@v4
366        with:
367          version: ${{ env.PNPM_VERSION }}
368      - name: Setup Node.js
369        uses: actions/setup-node@v4
370        with:
371          node-version: 20
372          cache: pnpm
373      - name: Install (frozen lockfile)
374        run: pnpm install --frozen-lockfile
375      - name: Wait for OpenSearch
376        run: |
377          for i in $(seq 1 60); do
378            if curl -sf http://localhost:9200/_cluster/health >/dev/null 2>&1; then
379              echo "opensearch is up after ${i} attempts"; exit 0
380            fi
381            sleep 5
382          done
383          echo "opensearch did not start in 300s" >&2; exit 1
384      - name: Create MinIO bucket + OpenSearch index
385        run: |
386          aws s3api create-bucket \
387            --endpoint-url http://localhost:9000 \
388            --bucket factory-uploads \
389            --region us-east-1 || true
390          curl -sf -X PUT "http://localhost:9200/factory-docs" \
391            -H 'Content-Type: application/json' -d '{}' || true
392      - name: Prisma generate + migrate + seed
393        run: |
394          pnpm db:generate
395          pnpm db:deploy
396          pnpm db:seed
397      - name: Start dev stack (background)
398        run: |
399          set -o pipefail
400          nohup pnpm dev > /tmp/factory-dev.log 2>&1 &
401          echo $! > /tmp/factory-dev.pid
402          wait_for_port() {
403            local port="$1" name="$2"
404            for i in $(seq 1 120); do
405              if (exec 3<>"/dev/tcp/127.0.0.1/${port}") 2>/dev/null; then
406                exec 3>&- 3<&- || true
407                echo "${name} is up on :${port} after ${i}s"; return 0
408              fi
409              sleep 1
410            done
411            echo "${name} did not come up on :${port} in 120s" >&2
412            tail -n 100 /tmp/factory-dev.log >&2 || true
413            return 1
414          }
415          wait_for_port 4000 api
416          wait_for_port 3000 web
417          wait_for_port 4001 realtime
418      - name: pnpm audit (high+ vulnerabilities fail the build)
419        run: pnpm audit --audit-level=high
420      - name: Security suites (10 suites, live stack required)
421        run: pnpm test:security
422      - name: Stop dev stack
423        if: always()
424        run: |
425          if [ -f /tmp/factory-dev.pid ]; then
426            kill "$(cat /tmp/factory-dev.pid)" 2>/dev/null || true
427            pkill -f "turbo" 2>/dev/null || true
428            pkill -f "next dev" 2>/dev/null || true
429          fi
430          tail -n 50 /tmp/factory-dev.log || true
431      - name: Install gitleaks (pinned 8.18.4)
432        run: |
433          curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v8.18.4/gitleaks_8.18.4_linux_x64.tar.gz" | tar -xz -C /usr/local/bin gitleaks
434          gitleaks version
435      - name: Gitleaks — full git history (new secret fails the build)
436        # protect = full history. --baseline-path suppresses exactly the
437        # pre-existing findings documented in gitleaks-baseline.json; ANY
438        # other finding exits non-zero.
439        run: |
440          gitleaks protect \
441            --source=. \
442            --config gitleaks.toml \
443            --baseline-path gitleaks-baseline.json \
444            --report-format json \
445            --report-path gitleaks-report.json \
446            --exit-code 1
447          echo "full-history scan clean (baseline applied)"
448 
449  # ---------------------------------------------------------------------------
450  # Stage 5 — build (hard): pnpm build (turbo) + the four Docker images built
451  # (NOT yet pushed — stage 12 pushes). Build outputs artifact for stages
452  # 4-artifact consumers and the release notes.
453  # ---------------------------------------------------------------------------
454  stage-5-build:
455    name: "[5/12] build (turbo + docker images)"
456    runs-on: ubuntu-24.04
457    timeout-minutes: 60
458    needs: [gate, stage-4-security]
459    if: needs.gate.outputs.from == 5
460    strategy:
461      fail-fast: false
462      matrix:
463        # Keep in sync with the apps/ directories and the kustomization
464        # images list (infrastructure/kubernetes/kustomization.yaml).
465        app: [api, web, worker, realtime]
466    steps:
467      - name: Checkout
468        uses: actions/checkout@v4
469      - name: Setup pnpm
470        uses: pnpm/action-setup@v4
471        with:
472          version: ${{ env.PNPM_VERSION }}
473      - name: Setup Node.js
474        uses: actions/setup-node@v4
475        with:
476          node-version: 20
477          cache: pnpm
478      - name: Install (frozen lockfile)
479        run: pnpm install --frozen-lockfile
480      - name: pnpm build (turbo — all apps + packages)
481        run: pnpm build
482        if: matrix.app == 'api' # once per workflow run (matrix fans the docker part)
483      - name: Set up Docker Buildx
484        uses: docker/setup-buildx-action@v3
485      - name: Build image (not pushed)
486        uses: docker/build-push-action@v6
487        with:
488          context: .
489          file: apps/${{ matrix.app }}/Dockerfile
490          push: false
491          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ matrix.app }}:${{ needs.gate.outputs.version }}
492          cache-from: type=gha,scope=${{ matrix.app }}
493          cache-to: type=gha,scope=${{ matrix.app }}
494          labels: |
495            org.opencontainers.image.source=${{ github.repositoryUrl }}
496            org.opencontainers.image.revision=${{ github.sha }}
497            org.opencontainers.image.version=${{ needs.gate.outputs.version }}
498      - name: Image build digest
499        run: echo "${{ env.REGISTRY }}/${{ env.IMAGE_OWNER }}/${{ matrix.app }}:${{ needs.gate.outputs.version }} built (push happens in stage 12)"
500      - name: Upload build outputs
501        if: matrix.app == 'api'
502        uses: actions/upload-artifact@v4
503        with:
504          name: build-outputs
505          path: |
506            apps/api/dist
507            apps/worker/dist
508            apps/realtime/dist
509          if-no-files-found: warn
510 
511  # ---------------------------------------------------------------------------
512  # Stage 6 — deploy-staging (hard: apply + readiness).
513  #
514  # PLACEHOLDER — wire to your cluster before a real release. To activate:
515  # set the repo variable DEPLOY_STAGING=true and a KUBECONFIG (or ArgoCD
516  # token) on the "staging" environment. Until then the step is an explicit
517  # no-op (and fails closed if DEPLOY_STAGING=true but no credentials exist).
518  # ---------------------------------------------------------------------------
519  stage-6-deploy-staging:
520    name: "[6/12] deploy staging (placeholder)"
521    runs-on: ubuntu-24.04
522    timeout-minutes: 15
523    needs: [gate, stage-5-build]
524    if: needs.gate.outputs.from == 6
525    environment:
526      name: staging
527    env:
528      DEPLOY_STAGING: ${{ vars.DEPLOY_STAGING }}
529    steps:
530      - name: Checkout
531        uses: actions/checkout@v4
532      - name: Check deployment wiring
533        run: |
534          if [ "$DEPLOY_STAGING" != "true" ]; then
535            echo "DEPLOY_STAGING is not 'true' — skipping deployment (placeholder)."
536            echo "To enable: set the repo variable DEPLOY_STAGING=true, add a KUBECONFIG"
537            echo "(or ArgoCD token) to the 'staging' environment, and wire the deploy step."
538            exit 0
539          fi
540          if [ -z "${KUBECONFIG:-}" ] && [ -z "${ARGOCD_TOKEN:-}" ]; then
541            echo "DEPLOY_STAGING=true but no KUBECONFIG/ARGOCD_TOKEN secret found." >&2
542            echo "Refusing to guess: add cluster credentials to the staging environment." >&2
543            exit 1
544          fi
545          # ── Wire your deployment here ─────────────────────────────────
546          # Example (kustomize): bump the tag and apply:
547          #   kustomize edit set image ghcr.io/<org>/api=${{ needs.gate.outputs.version }} \
548          #     infrastructure/kubernetes/
549          #   kubectl apply -k infrastructure/kubernetes/
550          # Example (Argo CD):
551          #   argocd app sync factory-staging --force
552          # ─────────────────────────────────────────────────────────────
553          echo "Deployment step not yet wired — see release.yml header. (no-op)"
554      - name: Wait for rollout (10 min budget)
555        # Runs only once deployment is wired: stages 7/8/10 read staging
556        # AFTER the rollout is Ready (spec rule).
557        if: env.DEPLOY_STAGING == 'true'
558        run: |
559          for i in $(seq 1 60); do
560            if kubectl -n factory get deploy api web worker realtime \
561                 -o jsonpath='{range .items[*]}{.status.readyReplicas}/{.status.replicas}{"\n"}{end}' \
562                 | awk -F/ '$1 != $2' | grep -q . ; then
563              sleep 10
564            else
565              echo "all deployments ready after ${i}0s"; exit 0
566            fi
567          done
568          echo "rollout not ready in 600s" >&2; exit 1
569 
570  # ---------------------------------------------------------------------------
571  # Stage 7 — smoke (hard): the Phase 0 smoke suite against staging —
572  # register → login → create page → search finds it. Budget: < 2 min.
573  # ---------------------------------------------------------------------------
574  stage-7-smoke:
575    name: "[7/12] smoke (staging)"
576    runs-on: ubuntu-24.04
577    timeout-minutes: 10
578    needs: [gate, stage-6-deploy-staging]
579    if: needs.gate.outputs.from == 7
580    environment:
581      name: staging
582    env:
583      # Staging base URLs (the smoke hits the deployed stack, not CI services).
584      E2E_BASE_URL: ${{ vars.STAGING_WEB_URL }}
585      E2E_API_URL: ${{ vars.STAGING_API_URL }}
586    steps:
587      - name: Checkout
588        uses: actions/checkout@v4
589      - name: Setup pnpm
590        uses: pnpm/action-setup@v4
591        with:
592          version: ${{ env.PNPM_VERSION }}
593      - name: Setup Node.js
594        uses: actions/setup-node@v4
595        with:
596          node-version: 20
597          cache: pnpm
598      - name: Install (frozen lockfile)
599        run: pnpm install --frozen-lockfile
600      - name: "Smoke: login + page create + search (< 2 min)"
601        env:
602          DEPLOY_STAGING: ${{ vars.DEPLOY_STAGING }}
603        run: |
604          set -euo pipefail
605          API="${E2E_API_URL:-http://staging.example.com/api}"
606          echo "smoke target: api=$API"
607          if [ "${DEPLOY_STAGING:-}" != "true" ]; then
608            echo "SKIPPED (loud): staging is not wired (DEPLOY_STAGING != true) — smoke cannot run."
609            exit 1
610          fi
611          START=$(date +%s)
612          RUN_ID="$(date +%s)-$RANDOM"
613          EMAIL="smoke-${RUN_ID}@factory.local"
614          PASSW0RD='Smoke!2345'
615          jqget() { node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const j=JSON.parse(d);const v=eval('j$1');console.log(v??'')}catch{console.log('')}})"; }
616          # 1 — register (201: user + personal workspace + tokens; httpOnly refresh cookie)
617          REG=$(curl -fsS -c /tmp/smoke.cookies -X POST "${API}/v1/auth/register" \
618            -H 'content-type: application/json' \
619            -d "{\"name\":\"Smoke ${RUN_ID}\",\"email\":\"${EMAIL}\",\"password\":\"${PASSW0RD}\"}")
620          WS_ID="$(printf '%s' "$REG" | jqget '.personalWorkspace.id')"
621          [ -n "$WS_ID" ] || { echo "register response missing personalWorkspace.id: ${REG}" >&2; exit 1; }
622          # 2 — login (fresh session + fresh cookie)
623          LOG=$(curl -fsS -c /tmp/smoke.cookies -X POST "${API}/v1/auth/login" \
624            -H 'content-type: application/json' \
625            -d "{\"email\":\"${EMAIL}\",\"password\":\"${PASSW0RD}\"}")
626          printf '%s' "$LOG" | grep -q 'accessToken' || { echo "login did not return a token" >&2; exit 1; }
627          # 3 — create a page in the auto-created personal workspace
628          PAGE=$(curl -fsS -b /tmp/smoke.cookies -X POST "${API}/v1/workspaces/${WS_ID}/pages" \
629            -H 'content-type: application/json' -d "{\"title\":\"Smoke page ${RUN_ID}\"}")
630          PAGE_ID="$(printf '%s' "$PAGE" | jqget '.id')"
631          [ -n "$PAGE_ID" ] || { echo "page create failed: ${PAGE}" >&2; exit 1; }
632          # 4 — search finds the page (GET /search/query = the command-palette
633          #    page search, a bare array of {id,type,title}; allow indexing
634          #    lag: 3 retries × 5 s)
635          FOUND=""
636          for i in 1 2 3; do
637            sleep 5
638            FOUND=$(curl -fsS -b /tmp/smoke.cookies "${API}/v1/search/query?q=Smoke+page+${RUN_ID}" || true)
639            printf '%s' "$FOUND" | grep -q "Smoke page ${RUN_ID}" && break
640          done
641          printf '%s' "$FOUND" | grep -q "Smoke page ${RUN_ID}" || { echo "search did not find the page: ${FOUND}" >&2; exit 1; }
642          END=$(date +%s)
643          ELAPSED=$((END - START))
644          echo "smoke GREEN in ${ELAPSED}s (budget 120s)"
645          [ "$ELAPSED" -lt 120 ] || { echo "smoke exceeded the 2-minute budget (${ELAPSED}s)" >&2; exit 1; }
646 
647  # ---------------------------------------------------------------------------
648  # Stage 8 — performance (hard thresholds): the 4 TASK-0135 suites
649  # (api-load, database-load, realtime-load, editor-local) + the 100k seed.
650  # The dev/PR run of the same suites is report-only; HERE thresholds are
651  # hard (K6_STRICT=1) — a breach blocks the release.
652  # ---------------------------------------------------------------------------
653  stage-8-performance:
654    name: "[8/12] performance (hard)"
655    runs-on: ubuntu-24.04
656    timeout-minutes: 180
657    needs: [gate, stage-7-smoke]
658    if: needs.gate.outputs.from == 8
659    environment:
660      name: staging
661    env:
662      DEPLOY_STAGING: ${{ vars.DEPLOY_STAGING }}
663      # Staging load target (the perf suites run against the deployed stack).
664      K6_API_URL: ${{ vars.STAGING_API_URL }}
665      K6_REALTIME_URL: ${{ vars.STAGING_WS_URL }}
666      K6_EMAIL: demo@factory.local
667      K6_PASSWORD: Factory!2345
668      # Hard thresholds: a breach FAILS the run (dev/PR is report-only).
669      K6_STRICT: "1"
670      # Seed fixture connects to staging Postgres (least-priv seed identity).
671      DATABASE_URL: ${{ secrets.STAGING_DATABASE_URL }}
672    steps:
673      - name: Checkout
674        uses: actions/checkout@v4
675      - name: Setup pnpm
676        uses: pnpm/action-setup@v4
677        with:
678          version: ${{ env.PNPM_VERSION }}
679      - name: Setup Node.js
680        uses: actions/setup-node@v4
681        with:
682          node-version: 20
683          cache: pnpm
684      - name: Install (frozen lockfile)
685        run: pnpm install --frozen-lockfile
686      - name: Install k6 (pinned)
687        run: |
688          K6_VERSION="0.50.0"
689          curl -sSfL "https://github.com/grafana/k6/releases/download/v${K6_VERSION}/k6-v${K6_VERSION}-linux-amd64.tar.gz" \
690            | tar -xz -C /usr/local/bin k6
691          k6 version
692      - name: Resolve perf fixtures (workspace / page / db ids)
693        if: env.DEPLOY_STAGING == 'true'
694        run: |
695          echo "Perf fixture ids come from the staging seed (demo user's workspace,"
696          echo "the seeded 100k database, and the 200 smoke pages). Exported:"
697          echo "  K6_WORKSPACE_ID=${{ vars.STAGING_PERF_WORKSPACE_ID }}"
698          echo "  K6_DB_ID=${{ vars.STAGING_PERF_DB_ID }}"
699          echo "  K6_PAGE_IDS=${{ vars.STAGING_PERF_PAGE_IDS }}"
700      - name: Seed the 100k fixture (idempotent — marker table test_seeds)
701        if: env.DEPLOY_STAGING == 'true'
702        run: pnpm --dir tests/performance seed:100k
703      - name: api-load (1k rps, p95 < 300 ms, errors < 0.1%)
704        if: env.DEPLOY_STAGING == 'true'
705        run: pnpm --dir tests/performance api-load
706      - name: database-load (100k records, table p95 < 500 ms, 100 rps creates)
707        if: env.DEPLOY_STAGING == 'true'
708        run: pnpm --dir tests/performance database-load
709      - name: realtime-load (10k conns / 200 msg/s, update p95 < 200 ms)
710        if: env.DEPLOY_STAGING == 'true'
711        run: pnpm --dir tests/performance realtime-load
712      - name: editor-local (interaction p95 < 50 ms, block move < 16 ms)
713        if: env.DEPLOY_STAGING == 'true'
714        run: |
715          WEB_BASE_URL="${{ vars.STAGING_WEB_URL }}" \
716          API_BASE_URL="${{ vars.STAGING_API_URL }}" \
717          pnpm --dir tests/performance editor-local
718      - name: Upload perf reports
719        if: always()
720        uses: actions/upload-artifact@v4
721        with:
722          name: perf-reports
723          path: tests/performance/reports
724          if-no-files-found: warn
725      - name: Placeholder verdict
726        if: env.DEPLOY_STAGING != 'true'
727        run: |
728          echo "SKIPPED (loud): staging is not wired — performance cannot run against"
729          echo "CI services (the suites target the deployed stack). The DEV/PR pipeline"
730          echo "runs the same suites report-only; this stage is hard-once-staging-exists."
731          exit 1
732 
733  # ---------------------------------------------------------------------------
734  # Stage 9 — backup-health (hard): the TASK-0135 backup/DR gate (runbook §8.7).
735  # Reads the REAL backup artifacts (no synthetic markers):
736  #   - newest object LastModified under pg/basebackups_005/  < 26 h
737  #   - newest object LastModified under pg/wal_005/          < 10 min
738  #   - newest SUCCESS snapshot start_time in repo factory-s3 < 26 h
739  # Semantics (scripts/backup-health.ts): reachable-but-stale or auth errors
740  # ALWAYS fail; an unreachable/empty endpoint is a loud report-only SKIP
741  # (exit 0) in dev, and a hard FAIL when DEPLOY_STAGING=true /
742  # BACKUP_HEALTH_STRICT=1 — so in staging the gate bites where the backup
743  # stack actually exists. A release whose rollback backup is stale must not
744  # ship.
745  # ---------------------------------------------------------------------------
746  stage-9-backup-health:
747    name: "[9/12] backup health (PITR/OS gate)"
748    runs-on: ubuntu-24.04
749    timeout-minutes: 10
750    needs: [gate, stage-8-performance]
751    if: needs.gate.outputs.from == 9
752    environment:
753      name: staging
754    env:
755      CI: true
756      # Endpoints of the BACKUP stack (the S3 the CronJobs write to + the
757      # OpenSearch the snapshot CronJob targets). Defaults match local dev;
758      # set them in the staging environment to the real values.
759      S3_ENDPOINT: ${{ vars.BACKUP_S3_ENDPOINT }}
760      BACKUP_S3_BUCKET: ${{ vars.BACKUP_S3_BUCKET }}
761      OS_URL: ${{ vars.BACKUP_OS_URL }}
762      # Hard-gate in staging (unreachable = FAIL); report-only in dev.
763      DEPLOY_STAGING: ${{ vars.DEPLOY_STAGING }}
764    steps:
765      - name: Checkout
766        uses: actions/checkout@v4
767      - name: Setup pnpm
768        uses: pnpm/action-setup@v4
769        with:
770          version: ${{ env.PNPM_VERSION }}
771      - name: Setup Node.js
772        uses: actions/setup-node@v4
773        with:
774          node-version: 20
775          cache: pnpm
776      - name: Install dependencies
777        run: pnpm install --frozen-lockfile
778      - name: Assert backup freshness (26 h / 10 min / 26 h)
779        env:
780          # Credentials for the backup identity (least-priv, see
781          # infrastructure/kubernetes/postgres/walg-iam-policy.yaml).
782          S3_ACCESS_KEY: ${{ secrets.BACKUP_S3_ACCESS_KEY }}
783          S3_SECRET_KEY: ${{ secrets.BACKUP_S3_SECRET_KEY }}
784          S3_REGION: ${{ vars.BACKUP_S3_REGION }}
785          BACKUP_S3_PG_PREFIX: pg
786          OS_SNAPSHOT_REPO: factory-s3
787        run: pnpm backup:health
788 
789  # ---------------------------------------------------------------------------
790  # Stage 10 — final-e2e (hard): tests/e2e/final-flow.spec.ts — the 13-step
791  # sequential final flow, against the staging URL. All green = the release
792  # is authorized (the tag step, 12, may now run).
793  # ---------------------------------------------------------------------------
794  stage-10-final-e2e:
795    name: "[10/12] final E2E (13-step flow)"
796    runs-on: ubuntu-24.04
797    timeout-minutes: 45
798    needs: [gate, stage-9-backup-health]
799    if: needs.gate.outputs.from == 10
800    environment:
801      name: staging
802    env:
803      E2E_BASE_URL: ${{ vars.STAGING_WEB_URL }}
804      E2E_API_URL: ${{ vars.STAGING_API_URL }}
805      E2E_INTERNAL_SERVICE_TOKEN: ${{ secrets.STAGING_INTERNAL_SERVICE_TOKEN }}
806      INTERNAL_TEST_SEAM: "1"
807      AI_PROVIDER: mock
808      AI_MOCK_DELTA_DELAY_MS: "60"
809    steps:
810      - name: Checkout
811        uses: actions/checkout@v4
812      - name: Setup pnpm
813        uses: pnpm/action-setup@v4
814        with:
815          version: ${{ env.PNPM_VERSION }}
816      - name: Setup Node.js
817        uses: actions/setup-node@v4
818        with:
819          node-version: 20
820          cache: pnpm
821      - name: Install (frozen lockfile)
822        run: pnpm install --frozen-lockfile
823      - name: Install Playwright browsers
824        run: pnpm --dir tests/e2e exec playwright install --with-deps chromium
825      - name: Run the 13-step final flow (dedicated config)
826        run: pnpm --dir tests/e2e exec playwright test --config playwright.final-flow.config.ts
827      - name: Upload E2E report
828        if: always()
829        uses: actions/upload-artifact@v4
830        with:
831          name: final-e2e-report
832          path: tests/e2e/playwright-report
833          if-no-files-found: warn
834 
835  # ---------------------------------------------------------------------------
836  # Stage 11 — docs-completeness (hard): scripts/docs-completeness.mjs
837  # (READMEs per package, OpenAPI + docs/api coverage, ROADMAP Phase 15
838  # boxes). Release mode: --require-phase-15-ticked — the boxes are ticked
839  # last (by the coordinator, after this release is green), so this stage is
840  # re-run (from_stage: 11) once they land.
841  # ---------------------------------------------------------------------------
842  stage-11-docs-completeness:
843    name: "[11/12] docs completeness"
844    runs-on: ubuntu-24.04
845    timeout-minutes: 10
846    needs: [gate, stage-10-final-e2e]
847    if: needs.gate.outputs.from == 11
848    steps:
849      - name: Checkout
850        uses: actions/checkout@v4
851      - name: Setup Node.js
852        uses: actions/setup-node@v4
853        with:
854          node-version: 20
855      - name: Run docs-completeness (release mode — Phase 15 boxes ticked)
856        run: node scripts/docs-completeness.mjs --require-phase-15-ticked
857 
858  # ---------------------------------------------------------------------------
859  # Stage 12 — push+tag (hard; THE LAST ACT).
860  #   1. Push the four GHCR images: tag = <version>, plus `latest` for
861  #      vX.0.0 releases (is_patch), plus the commit sha. Idempotent —
862  #      re-pushing the same tags/digests is a no-op.
863  #   2. Create the ANNOTATED tag v<version> (idempotent: never moves an
864  #      existing tag).
865  #   3. Publish the GitHub release (notes: CHANGELOG section + digests +
866  #      run links) as a DRAFT (the human publishes — v1.0.0 is deliberate).
867  # ---------------------------------------------------------------------------
868  stage-12-push-tag:
869    name: "[12/12] push + tag + release"
870    runs-on: ubuntu-24.04
871    timeout-minutes: 45
872    needs: [gate, stage-11-docs-completeness]
873    if: needs.gate.outputs.from == 12
874    steps:
875      - name: Checkout
876        uses: actions/checkout@v4
877        with:
878          fetch-depth: 0 # full history → the annotated tag can live anywhere
879      - name: Log in to GitHub Container Registry
880        uses: docker/login-action@v3
881        with:
882          registry: ${{ env.REGISTRY }}
883          username: ${{ github.actor }}
884          password: ${{ secrets.GITHUB_TOKEN }}
885      - name: Set up Docker Buildx
886        uses: docker/setup-buildx-action@v3
887      - name: Push the four images (version + [latest] + sha)
888        env:
889          VERSION: ${{ needs.gate.outputs.version }}
890          IS_PATCH: ${{ needs.gate.outputs.is_patch }}
891        run: |
892          set -euo pipefail
893          : "${VERSION:?}"
894          for app in api web worker realtime; do
895            IMAGE="${REGISTRY}/${IMAGE_OWNER}/${app}"
896            TAGS="${IMAGE}:${VERSION}"
897            if [ "$IS_PATCH" = "true" ]; then
898              TAGS="${TAGS},${IMAGE}:latest"
899            fi
900            echo "=== ${IMAGE}:${VERSION} ${IS_PATCH:+"(+ latest)"} ==="
901            docker buildx build \
902              --file "apps/${app}/Dockerfile" \
903              --tag "$TAGS" \
904              --push \
905              --cache-from type=gha,scope="${app}" \
906              --cache-to type=gha,scope="${app}" \
907              --label "org.opencontainers.image.source=${GITHUB_REPOSITORY_URL}" \
908              --label "org.opencontainers.image.revision=${GITHUB_SHA}" \
909              --label "org.opencontainers.image.version=${VERSION}" \
910              .
911          done
912          echo "all four images pushed"
913      - name: Registry check (tag + latest exist)
914        env:
915          VERSION: ${{ needs.gate.outputs.version }}
916          IS_PATCH: ${{ needs.gate.outputs.is_patch }}
917        run: |
918          set -euo pipefail
919          for app in api web worker realtime; do
920            IMAGE="${REGISTRY}/${IMAGE_OWNER}/${app}"
921            # `docker manifest inspect` proves the tag is pullable from GHCR.
922            docker manifest inspect "${IMAGE}:${VERSION}" >/dev/null
923            [ "$IS_PATCH" = "true" ] && docker manifest inspect "${IMAGE}:latest" >/dev/null
924            echo "${IMAGE}:${VERSION} present"
925          done
926          echo "registry check: OK"
927      - name: Digests to job summary
928        env:
929          VERSION: ${{ needs.gate.outputs.version }}
930          IS_PATCH: ${{ needs.gate.outputs.is_patch }}
931          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
932        run: |
933          set -euo pipefail
934          echo "### Published images — v${VERSION}" >> "$GITHUB_STEP_SUMMARY"
935          echo "" >> "$GITHUB_STEP_SUMMARY"
936          for app in api web worker realtime; do
937            IMAGE="${REGISTRY}/${IMAGE_OWNER}/${app}"
938            DIGEST="$(docker buildx imagetools inspect "${IMAGE}:${VERSION}" 2>/dev/null \
939              | awk -F': ' '/^  digest:/{print $2}' | head -1)"
940            echo "- \`${IMAGE}:${VERSION}\` — \`${DIGEST:-see build log}\`" >> "$GITHUB_STEP_SUMMARY"
941            [ "$IS_PATCH" = "true" ] && echo "- \`${IMAGE}:latest\` → v${VERSION}" >> "$GITHUB_STEP_SUMMARY"
942          done
943          echo "" >> "$GITHUB_STEP_SUMMARY"
944          echo "commit: \`${GITHUB_SHA}\`" >> "$GITHUB_STEP_SUMMARY"
945      - name: Create annotated tag (idempotent — never moves an existing tag)
946        env:
947          VERSION: ${{ needs.gate.outputs.version }}
948        run: |
949          set -euo pipefail
950          TAG="v${VERSION}"
951          if git ls-remote --tags origin "refs/tags/${TAG}" | grep -q .; then
952            echo "tag ${TAG} already exists — leaving it exactly where it is (idempotent re-run)"
953            exit 0
954          fi
955          # The tag is created BY this workflow on the tested commit
956          # (this dispatch's head — the code all 11 preceding stages ran).
957          # The release notes carry the run link, so provenance is explicit.
958          git tag -a "${TAG}" -m "Release v${VERSION}" "${GITHUB_SHA}"
959          git push origin "refs/tags/${TAG}"
960          echo "tagged ${GITHUB_SHA} as ${TAG}"
961      - name: Publish GitHub release (draft)
962        env:
963          VERSION: ${{ needs.gate.outputs.version }}
964          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
965          GH_REPO: ${{ github.repository }}
966        run: |
967          set -euo pipefail
968          TAG="v${VERSION}"
969          RUN_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
970          # Notes: the CHANGELOG section for this version (if present) + the
971          # run link + the runbook links.
972          NOTES=""
973          if [ -f CHANGELOG.md ]; then
974            SECTION="$(awk -v v="## [${VERSION}]" 'index($0,v)==1{f=1;next} f&&/^## \[/{exit} f' CHANGELOG.md || true)"
975            NOTES="${SECTION:-_CHANGELOG.md has no [${VERSION}] section — add it._}"
976          fi
977          NOTES="${NOTES}
978 
979          ---
980          - Release run: ${RUN_URL}
981          - Runbooks: [deployment](docs/development/deployment.md) · [admin guide](docs/development/admin-guide.md) · [support matrix](docs/support-matrix.md)
982          "
983          if gh release view "${TAG}" >/dev/null 2>&1; then
984            echo "release ${TAG} exists — leaving it untouched (idempotent re-run)"
985          else
986            gh release create "${TAG}" --title "v${VERSION}" --notes "${NOTES}" --draft
987            echo "draft release ${TAG} created"
988          fi
989