devpush 0.0.0-stage → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/README.md +22 -2
  2. package/agent-release.json +37 -0
  3. package/dist/package/LICENSE.md +21 -0
  4. package/dist/package/migrate.mjs +182 -0
  5. package/dist/package/plugins.mjs +125 -0
  6. package/dist/package/vite.mjs +439 -0
  7. package/package.json +168 -3
  8. package/public/apple-touch-icon.png +0 -0
  9. package/public/favicon.svg +1 -0
  10. package/public/logo-email.png +0 -0
  11. package/public/robots.txt +3 -0
  12. package/public/social.png +0 -0
  13. package/src/cloudflare-runtime.d.ts +78 -0
  14. package/src/components/about-dialog.tsx +70 -0
  15. package/src/components/copy-button.tsx +74 -0
  16. package/src/components/logo-mark.tsx +20 -0
  17. package/src/components/pending-collection.tsx +103 -0
  18. package/src/components/resource-avatar.tsx +65 -0
  19. package/src/components/route-error-state.tsx +95 -0
  20. package/src/components/section-navigation.tsx +39 -0
  21. package/src/components/settings-layout.tsx +111 -0
  22. package/src/components/storage-capabilities.tsx +11 -0
  23. package/src/components/table-pager.tsx +101 -0
  24. package/src/components/ui/alert-dialog.tsx +180 -0
  25. package/src/components/ui/alert.tsx +78 -0
  26. package/src/components/ui/avatar.tsx +58 -0
  27. package/src/components/ui/badge.tsx +49 -0
  28. package/src/components/ui/button-group.tsx +38 -0
  29. package/src/components/ui/button.tsx +67 -0
  30. package/src/components/ui/card.tsx +82 -0
  31. package/src/components/ui/chart.tsx +307 -0
  32. package/src/components/ui/checkbox.tsx +30 -0
  33. package/src/components/ui/combobox.tsx +256 -0
  34. package/src/components/ui/dialog.tsx +163 -0
  35. package/src/components/ui/dropdown-menu.tsx +110 -0
  36. package/src/components/ui/empty.tsx +65 -0
  37. package/src/components/ui/field.tsx +192 -0
  38. package/src/components/ui/input-group.tsx +118 -0
  39. package/src/components/ui/input-otp.tsx +70 -0
  40. package/src/components/ui/input.tsx +19 -0
  41. package/src/components/ui/label.tsx +24 -0
  42. package/src/components/ui/pagination.tsx +34 -0
  43. package/src/components/ui/popover.tsx +86 -0
  44. package/src/components/ui/select.tsx +145 -0
  45. package/src/components/ui/skeleton.tsx +19 -0
  46. package/src/components/ui/sonner.tsx +47 -0
  47. package/src/components/ui/switch.tsx +31 -0
  48. package/src/components/ui/table.tsx +81 -0
  49. package/src/components/ui/textarea.tsx +18 -0
  50. package/src/components/ui/tooltip.tsx +55 -0
  51. package/src/features/admin/admin-settings.tsx +1048 -0
  52. package/src/features/auth/current-user-context-query.ts +11 -0
  53. package/src/features/auth/login-form.tsx +442 -0
  54. package/src/features/auth/sign-out-menu-item.tsx +42 -0
  55. package/src/features/deployments/deployment-actions.tsx +336 -0
  56. package/src/features/deployments/deployment-alias-badge.tsx +35 -0
  57. package/src/features/deployments/deployment-detail.tsx +596 -0
  58. package/src/features/deployments/deployment-filters.ts +66 -0
  59. package/src/features/deployments/deployment-list.tsx +465 -0
  60. package/src/features/deployments/deployment-visit-links.tsx +104 -0
  61. package/src/features/deployments/deployments-page.tsx +376 -0
  62. package/src/features/logs/log-scroll-area.tsx +69 -0
  63. package/src/features/notifications/email-preferences.tsx +232 -0
  64. package/src/features/onboarding/profile-onboarding.tsx +162 -0
  65. package/src/features/plugins/contributions.tsx +118 -0
  66. package/src/features/projects/backup-job-logs.tsx +143 -0
  67. package/src/features/projects/backup-policy-delete-dialog.tsx +82 -0
  68. package/src/features/projects/backup-restore-actions.tsx +573 -0
  69. package/src/features/projects/backup-status-icon.tsx +33 -0
  70. package/src/features/projects/backup-target-list.tsx +222 -0
  71. package/src/features/projects/backup-timestamp.tsx +32 -0
  72. package/src/features/projects/backup-volume-picker.tsx +158 -0
  73. package/src/features/projects/compose-service-input.tsx +64 -0
  74. package/src/features/projects/deploy-readiness.ts +28 -0
  75. package/src/features/projects/environment-dot.tsx +91 -0
  76. package/src/features/projects/log-keyword-filter.tsx +48 -0
  77. package/src/features/projects/manual-deploy-dialog.tsx +563 -0
  78. package/src/features/projects/pending-section.ts +26 -0
  79. package/src/features/projects/project-backup-details-pending.tsx +305 -0
  80. package/src/features/projects/project-backup-details.tsx +936 -0
  81. package/src/features/projects/project-backup-policy.tsx +644 -0
  82. package/src/features/projects/project-backup-utils.ts +190 -0
  83. package/src/features/projects/project-backups-pending.tsx +136 -0
  84. package/src/features/projects/project-backups.tsx +876 -0
  85. package/src/features/projects/project-environment-settings.tsx +1128 -0
  86. package/src/features/projects/project-heading.tsx +68 -0
  87. package/src/features/projects/project-layout.tsx +188 -0
  88. package/src/features/projects/project-logs.tsx +461 -0
  89. package/src/features/projects/project-overview.tsx +280 -0
  90. package/src/features/projects/project-route-pending.tsx +1064 -0
  91. package/src/features/projects/project-routing-settings.tsx +663 -0
  92. package/src/features/projects/project-settings-section.tsx +64 -0
  93. package/src/features/projects/project-settings-utils.ts +101 -0
  94. package/src/features/projects/project-settings.tsx +1472 -0
  95. package/src/features/projects/repository-picker-dialog.tsx +404 -0
  96. package/src/features/repositories/new-project-dialog-launcher.tsx +44 -0
  97. package/src/features/repositories/new-project-dialog.tsx +1204 -0
  98. package/src/features/repositories/new-project-pending.tsx +28 -0
  99. package/src/features/repositories/repository-list-pending.tsx +59 -0
  100. package/src/features/servers/server-availability-alert.tsx +44 -0
  101. package/src/features/servers/server-availability.ts +29 -0
  102. package/src/features/servers/server-avatar.tsx +52 -0
  103. package/src/features/servers/server-detail-pending.tsx +315 -0
  104. package/src/features/servers/server-detail.tsx +855 -0
  105. package/src/features/servers/server-metric-chart-pending.tsx +38 -0
  106. package/src/features/servers/server-metric-chart.tsx +182 -0
  107. package/src/features/servers/server-metric-format.ts +50 -0
  108. package/src/features/servers/server-metrics-pending.tsx +75 -0
  109. package/src/features/servers/server-metrics.tsx +136 -0
  110. package/src/features/servers/team-servers.tsx +432 -0
  111. package/src/features/settings/settings-route-pending.tsx +406 -0
  112. package/src/features/teams/backup-storage-settings.tsx +483 -0
  113. package/src/features/teams/navigation.ts +51 -0
  114. package/src/features/teams/new-team-dialog.tsx +132 -0
  115. package/src/features/teams/pending-section.ts +19 -0
  116. package/src/features/teams/team-header.tsx +1286 -0
  117. package/src/features/teams/team-heading.tsx +57 -0
  118. package/src/features/teams/team-home.tsx +391 -0
  119. package/src/features/teams/team-invite.tsx +155 -0
  120. package/src/features/teams/team-route-pending.tsx +505 -0
  121. package/src/features/teams/team-settings.tsx +1555 -0
  122. package/src/features/teams/use-workspace-updates.ts +126 -0
  123. package/src/features/users/api-keys-settings.tsx +544 -0
  124. package/src/features/users/api-scope-selection.tsx +269 -0
  125. package/src/features/users/connected-apps-settings.tsx +449 -0
  126. package/src/features/users/mcp-consent.tsx +220 -0
  127. package/src/features/users/profile-picture.tsx +171 -0
  128. package/src/features/users/user-settings.tsx +1064 -0
  129. package/src/lib/account-settings-schema.ts +78 -0
  130. package/src/lib/action-feedback.ts +81 -0
  131. package/src/lib/admin-search-schema.ts +8 -0
  132. package/src/lib/agent-release.d.mts +21 -0
  133. package/src/lib/agent-release.mjs +90 -0
  134. package/src/lib/allowlist-schema.ts +79 -0
  135. package/src/lib/api-key-schema.ts +176 -0
  136. package/src/lib/auth/client.ts +20 -0
  137. package/src/lib/avatar-schema.ts +11 -0
  138. package/src/lib/backup-cron.ts +118 -0
  139. package/src/lib/backup-database.ts +41 -0
  140. package/src/lib/backup-feature.ts +2 -0
  141. package/src/lib/backup-paths.ts +7 -0
  142. package/src/lib/backup-policy-schema.ts +85 -0
  143. package/src/lib/backup-resource-schema.ts +12 -0
  144. package/src/lib/backup-restore-schema.ts +15 -0
  145. package/src/lib/backup-storage-schema.ts +55 -0
  146. package/src/lib/deferred-module.ts +38 -0
  147. package/src/lib/deployment-logs-query.ts +63 -0
  148. package/src/lib/display-name.ts +7 -0
  149. package/src/lib/error-message.ts +119 -0
  150. package/src/lib/form-errors.ts +10 -0
  151. package/src/lib/internal-redirect.ts +25 -0
  152. package/src/lib/manual-deploy-options-query.ts +21 -0
  153. package/src/lib/notification-preferences.ts +43 -0
  154. package/src/lib/onboarding.ts +11 -0
  155. package/src/lib/operation-logs-query.ts +51 -0
  156. package/src/lib/page-head.ts +14 -0
  157. package/src/lib/project-context-query.ts +257 -0
  158. package/src/lib/project-creation-schema.ts +64 -0
  159. package/src/lib/project-env-schema.ts +23 -0
  160. package/src/lib/project-environments-schema.ts +56 -0
  161. package/src/lib/project-routing-schema.ts +94 -0
  162. package/src/lib/project-runtime-config.ts +131 -0
  163. package/src/lib/project-settings-schema.ts +75 -0
  164. package/src/lib/public-api-schema.ts +128 -0
  165. package/src/lib/public-error.ts +337 -0
  166. package/src/lib/query.ts +13 -0
  167. package/src/lib/repository-selection-query.ts +11 -0
  168. package/src/lib/resource-cache.ts +30 -0
  169. package/src/lib/runtime-id.ts +19 -0
  170. package/src/lib/search.ts +14 -0
  171. package/src/lib/server-context-query.ts +43 -0
  172. package/src/lib/server-name-schema.ts +11 -0
  173. package/src/lib/settings-conflict.ts +45 -0
  174. package/src/lib/settings-version.ts +7 -0
  175. package/src/lib/team-context-query.ts +126 -0
  176. package/src/lib/team-context-schema.ts +8 -0
  177. package/src/lib/team-forms-schema.ts +45 -0
  178. package/src/lib/team-role.ts +13 -0
  179. package/src/lib/team-settings-schema.ts +48 -0
  180. package/src/lib/urls.ts +3 -0
  181. package/src/lib/use-avatar-input.ts +61 -0
  182. package/src/lib/use-refresh-error.ts +18 -0
  183. package/src/lib/use-settings-version.ts +23 -0
  184. package/src/lib/utils.ts +6 -0
  185. package/src/lib/validation.ts +5 -0
  186. package/src/lib/workspace-updates.ts +53 -0
  187. package/src/package/config.ts +10 -0
  188. package/src/package/env.mjs +54 -0
  189. package/src/package/migrate-api.d.ts +5 -0
  190. package/src/package/migrate.ts +56 -0
  191. package/src/package/plugins.ts +141 -0
  192. package/src/package/sqlite.mjs +2 -0
  193. package/src/package/vite-api.d.ts +5 -0
  194. package/src/package/vite.ts +324 -0
  195. package/src/package/watch-guard.cjs +26 -0
  196. package/src/plugins/client.ts +57 -0
  197. package/src/plugins/contract.ts +188 -0
  198. package/src/plugins/default.server.ts +4 -0
  199. package/src/plugins/default.ui.ts +4 -0
  200. package/src/plugins/operations.ts +45 -0
  201. package/src/plugins/server.ts +197 -0
  202. package/src/plugins/version.ts +1 -0
  203. package/src/routeTree.gen.ts +1275 -0
  204. package/src/router.tsx +46 -0
  205. package/src/routes/$teamSlug.index.tsx +53 -0
  206. package/src/routes/$teamSlug.new-project.tsx +13 -0
  207. package/src/routes/$teamSlug.projects.$projectName.backups.$backupId.tsx +40 -0
  208. package/src/routes/$teamSlug.projects.$projectName.backups.tsx +82 -0
  209. package/src/routes/$teamSlug.projects.$projectName.deployments.$deploymentId.tsx +52 -0
  210. package/src/routes/$teamSlug.projects.$projectName.deployments.tsx +86 -0
  211. package/src/routes/$teamSlug.projects.$projectName.logs.tsx +55 -0
  212. package/src/routes/$teamSlug.projects.$projectName.settings.tsx +51 -0
  213. package/src/routes/$teamSlug.projects.$projectName.tsx +106 -0
  214. package/src/routes/$teamSlug.projects.tsx +40 -0
  215. package/src/routes/$teamSlug.servers.$serverId.tsx +31 -0
  216. package/src/routes/$teamSlug.servers.index.tsx +59 -0
  217. package/src/routes/$teamSlug.servers.tsx +7 -0
  218. package/src/routes/$teamSlug.settings.tsx +56 -0
  219. package/src/routes/$teamSlug.tsx +86 -0
  220. package/src/routes/[.]well-known.oauth-authorization-server.api.auth.ts +13 -0
  221. package/src/routes/[.]well-known.oauth-protected-resource.mcp.ts +6 -0
  222. package/src/routes/__root.tsx +155 -0
  223. package/src/routes/admin.tsx +55 -0
  224. package/src/routes/api/agents/certificates.ts +21 -0
  225. package/src/routes/api/agents/credentials/rotate.ts +22 -0
  226. package/src/routes/api/agents/heartbeat.ts +42 -0
  227. package/src/routes/api/agents/job-logs.ts +22 -0
  228. package/src/routes/api/agents/jobs/complete.ts +22 -0
  229. package/src/routes/api/agents/jobs/next.ts +29 -0
  230. package/src/routes/api/agents/jobs/progress.ts +32 -0
  231. package/src/routes/api/agents/jobs/status.ts +22 -0
  232. package/src/routes/api/agents/register.ts +45 -0
  233. package/src/routes/api/agents/runtime-logs.ts +22 -0
  234. package/src/routes/api/agents/switch.ts +25 -0
  235. package/src/routes/api/auth/$.ts +38 -0
  236. package/src/routes/api/cron/backups.ts +33 -0
  237. package/src/routes/api/cron/maintenance.ts +30 -0
  238. package/src/routes/api/github/install.callback.ts +51 -0
  239. package/src/routes/api/github/install.ts +41 -0
  240. package/src/routes/api/github/webhook.ts +11 -0
  241. package/src/routes/api/health.ts +19 -0
  242. package/src/routes/api/install/agent.$token.ts +50 -0
  243. package/src/routes/api/install/agent.binary.ts +83 -0
  244. package/src/routes/api/install/agent.checksum.ts +45 -0
  245. package/src/routes/api/install/agent.ts +26 -0
  246. package/src/routes/api/plugins/$.ts +11 -0
  247. package/src/routes/api/v1/$.ts +15 -0
  248. package/src/routes/api/workspaces/updates.ts +8 -0
  249. package/src/routes/auth.login.tsx +50 -0
  250. package/src/routes/deployments.$deploymentId.unavailable.tsx +21 -0
  251. package/src/routes/index.tsx +23 -0
  252. package/src/routes/invites.$token.tsx +35 -0
  253. package/src/routes/mcp.ts +13 -0
  254. package/src/routes/oauth.consent.tsx +54 -0
  255. package/src/routes/onboarding.tsx +37 -0
  256. package/src/routes/onboarding.workspace.tsx +44 -0
  257. package/src/routes/settings.tsx +30 -0
  258. package/src/routes/uploads/avatars/$fileName.ts +44 -0
  259. package/src/server/admin/access.ts +67 -0
  260. package/src/server/admin/functions.ts +122 -0
  261. package/src/server/admin/service.ts +260 -0
  262. package/src/server/agents/backup-state.ts +210 -0
  263. package/src/server/agents/certificates.ts +96 -0
  264. package/src/server/agents/claims.ts +116 -0
  265. package/src/server/agents/cleanup.ts +52 -0
  266. package/src/server/agents/compatibility.ts +116 -0
  267. package/src/server/agents/completion.ts +83 -0
  268. package/src/server/agents/container-cleanup-state.ts +51 -0
  269. package/src/server/agents/deployment-state.ts +111 -0
  270. package/src/server/agents/job-payloads.ts +159 -0
  271. package/src/server/agents/jobs.ts +1293 -0
  272. package/src/server/agents/protocol.ts +400 -0
  273. package/src/server/agents/reconciliation.ts +43 -0
  274. package/src/server/agents/runtime-logs.ts +337 -0
  275. package/src/server/agents/runtime-state.ts +125 -0
  276. package/src/server/api/admission.ts +12 -0
  277. package/src/server/api/authorization.ts +115 -0
  278. package/src/server/api/credentials.ts +569 -0
  279. package/src/server/api/cursors.ts +72 -0
  280. package/src/server/api/errors.ts +24 -0
  281. package/src/server/api/functions.ts +60 -0
  282. package/src/server/api/handler.ts +274 -0
  283. package/src/server/api/mutation-operations.ts +913 -0
  284. package/src/server/api/operation-context.ts +139 -0
  285. package/src/server/api/operation-contract.ts +1099 -0
  286. package/src/server/api/operations.ts +64 -0
  287. package/src/server/api/read-operations.ts +891 -0
  288. package/src/server/api/retention.ts +33 -0
  289. package/src/server/auth/access.ts +109 -0
  290. package/src/server/auth/auth-methods.ts +28 -0
  291. package/src/server/auth/better-auth.ts +518 -0
  292. package/src/server/auth/context.ts +28 -0
  293. package/src/server/auth/email-change.ts +47 -0
  294. package/src/server/auth/email-delivery.ts +51 -0
  295. package/src/server/auth/provider-identity.ts +19 -0
  296. package/src/server/auth/provider-tokens.ts +27 -0
  297. package/src/server/auth/session.server.ts +19 -0
  298. package/src/server/backups/database-commands.ts +40 -0
  299. package/src/server/backups/destination.ts +12 -0
  300. package/src/server/backups/feature.ts +6 -0
  301. package/src/server/backups/policy-validation.ts +51 -0
  302. package/src/server/backups/queue-state.ts +137 -0
  303. package/src/server/backups/service.ts +918 -0
  304. package/src/server/backups/storage-functions.ts +42 -0
  305. package/src/server/backups/storage-state.ts +67 -0
  306. package/src/server/backups/storage.ts +237 -0
  307. package/src/server/config.ts +274 -0
  308. package/src/server/db/atomic.ts +16 -0
  309. package/src/server/db/d1-connection.ts +83 -0
  310. package/src/server/db/errors.ts +17 -0
  311. package/src/server/db/index.cloudflare.ts +15 -0
  312. package/src/server/db/index.ts +35 -0
  313. package/src/server/db/libsql-connection.ts +72 -0
  314. package/src/server/db/log-schema.ts +102 -0
  315. package/src/server/db/migrate-libsql.ts +29 -0
  316. package/src/server/db/migrate-sqlite.ts +47 -0
  317. package/src/server/db/migration-history.ts +12 -0
  318. package/src/server/db/migrations-logs/0000_initial.sql +48 -0
  319. package/src/server/db/migrations-logs/0001_runtime_log_order.sql +2 -0
  320. package/src/server/db/migrations-logs/meta/0000_snapshot.json +327 -0
  321. package/src/server/db/migrations-logs/meta/0001_snapshot.json +329 -0
  322. package/src/server/db/migrations-logs/meta/_journal.json +20 -0
  323. package/src/server/db/migrations-sqlite/0000_initial.sql +894 -0
  324. package/src/server/db/migrations-sqlite/0001_agent_indexes.sql +5 -0
  325. package/src/server/db/migrations-sqlite/meta/0000_snapshot.json +5816 -0
  326. package/src/server/db/migrations-sqlite/meta/0001_snapshot.json +5841 -0
  327. package/src/server/db/migrations-sqlite/meta/_journal.json +20 -0
  328. package/src/server/db/oauth-schema.ts +216 -0
  329. package/src/server/db/remote-config.ts +47 -0
  330. package/src/server/db/schema.ts +1218 -0
  331. package/src/server/db/sqlite-connection.ts +94 -0
  332. package/src/server/db/sqlite-files.ts +27 -0
  333. package/src/server/db/sqlite-params.ts +19 -0
  334. package/src/server/db/worker-connection.ts +47 -0
  335. package/src/server/deleted-resources.ts +133 -0
  336. package/src/server/deletion-recovery.ts +272 -0
  337. package/src/server/email/layout.tsx +85 -0
  338. package/src/server/email/notification-template.tsx +62 -0
  339. package/src/server/email/otp-template.tsx +48 -0
  340. package/src/server/email/service.ts +251 -0
  341. package/src/server/email/team-invite-template.tsx +59 -0
  342. package/src/server/email/theme.ts +10 -0
  343. package/src/server/github/http.ts +14 -0
  344. package/src/server/github/install-state.ts +48 -0
  345. package/src/server/github/service.ts +831 -0
  346. package/src/server/github/webhook-handler.ts +198 -0
  347. package/src/server/github/webhooks.ts +205 -0
  348. package/src/server/http/auth.ts +11 -0
  349. package/src/server/http/error-diagnostics.ts +66 -0
  350. package/src/server/http/plugin-request.ts +15 -0
  351. package/src/server/http/proxy-request.ts +53 -0
  352. package/src/server/http/public-errors.ts +48 -0
  353. package/src/server/http/rate-limit.ts +59 -0
  354. package/src/server/http/request-body.ts +54 -0
  355. package/src/server/http/request-ip.ts +54 -0
  356. package/src/server/http/security-headers.ts +36 -0
  357. package/src/server/install/agent-binary.ts +230 -0
  358. package/src/server/install/agent-script.ts +565 -0
  359. package/src/server/install/release-binary.ts +65 -0
  360. package/src/server/logs/budget.ts +72 -0
  361. package/src/server/logs/cleanup.ts +64 -0
  362. package/src/server/logs/deployments.ts +98 -0
  363. package/src/server/logs/operations.ts +145 -0
  364. package/src/server/logs/retention.ts +39 -0
  365. package/src/server/maintenance.ts +61 -0
  366. package/src/server/mcp/connections.ts +358 -0
  367. package/src/server/mcp/functions.ts +63 -0
  368. package/src/server/mcp/handler.ts +295 -0
  369. package/src/server/mcp/oauth-discovery.cloudflare.ts +6 -0
  370. package/src/server/mcp/oauth-discovery.ts +8 -0
  371. package/src/server/mcp/oauth-provider.ts +220 -0
  372. package/src/server/mcp/urls.ts +20 -0
  373. package/src/server/notifications/functions.ts +51 -0
  374. package/src/server/notifications/observe.ts +303 -0
  375. package/src/server/notifications/outbox.ts +335 -0
  376. package/src/server/notifications/preferences.ts +131 -0
  377. package/src/server/notifications/query.ts +16 -0
  378. package/src/server/plugins/scope.ts +17 -0
  379. package/src/server/plugins/server-context.ts +157 -0
  380. package/src/server/plugins/services.server.ts +10 -0
  381. package/src/server/plugins/workspace-access.ts +74 -0
  382. package/src/server/projects/backup-policy-state.ts +68 -0
  383. package/src/server/projects/backups.ts +219 -0
  384. package/src/server/projects/compose-metadata.ts +174 -0
  385. package/src/server/projects/configuration-state.ts +90 -0
  386. package/src/server/projects/context.ts +909 -0
  387. package/src/server/projects/creation-state.ts +46 -0
  388. package/src/server/projects/deletion-state.ts +69 -0
  389. package/src/server/projects/deployment-config.ts +139 -0
  390. package/src/server/projects/deployment-logs.ts +97 -0
  391. package/src/server/projects/deployments.ts +1123 -0
  392. package/src/server/projects/domain-state.ts +71 -0
  393. package/src/server/projects/environment-branches.ts +75 -0
  394. package/src/server/projects/environment-state.ts +158 -0
  395. package/src/server/projects/environments.ts +424 -0
  396. package/src/server/projects/functions.ts +748 -0
  397. package/src/server/projects/generated-hostname.ts +77 -0
  398. package/src/server/projects/hostnames.ts +244 -0
  399. package/src/server/projects/mappers.ts +457 -0
  400. package/src/server/projects/names.ts +41 -0
  401. package/src/server/projects/project-env-vars.ts +28 -0
  402. package/src/server/projects/queue-state.ts +118 -0
  403. package/src/server/projects/repository.ts +368 -0
  404. package/src/server/projects/route-state.ts +37 -0
  405. package/src/server/projects/runtime-logs.ts +97 -0
  406. package/src/server/projects/settings-context.ts +58 -0
  407. package/src/server/projects/settings.ts +738 -0
  408. package/src/server/projects/types.ts +355 -0
  409. package/src/server/realtime/agent-events.ts +32 -0
  410. package/src/server/realtime/node-broadcaster.ts +71 -0
  411. package/src/server/realtime/node-handler.ts +55 -0
  412. package/src/server/realtime/service.ts +125 -0
  413. package/src/server/realtime/workspace-object.cloudflare.ts +62 -0
  414. package/src/server/realtime/workspace-object.ts +3 -0
  415. package/src/server/resources/color.ts +13 -0
  416. package/src/server/runtime/background.ts +21 -0
  417. package/src/server/runtime/node-asset-cache-plugin.ts +12 -0
  418. package/src/server/runtime/node-scheduler-plugin.ts +17 -0
  419. package/src/server/runtime/node-scheduler.ts +51 -0
  420. package/src/server/runtime/platform-env.cloudflare.ts +17 -0
  421. package/src/server/runtime/platform-env.ts +19 -0
  422. package/src/server/runtime/scheduled-task-error.ts +9 -0
  423. package/src/server/scheduled-tasks.ts +28 -0
  424. package/src/server/secrets/crypto.ts +56 -0
  425. package/src/server/servers/functions.ts +233 -0
  426. package/src/server/servers/lifecycle.ts +122 -0
  427. package/src/server/servers/metric-history-schema.ts +24 -0
  428. package/src/server/servers/metric-history.ts +55 -0
  429. package/src/server/servers/monitoring.ts +592 -0
  430. package/src/server/servers/service.ts +1211 -0
  431. package/src/server/servers/status.ts +32 -0
  432. package/src/server/servers/switch.ts +96 -0
  433. package/src/server/settings-version.ts +43 -0
  434. package/src/server/storage/capabilities.ts +6 -0
  435. package/src/server/storage/local-object-store.cloudflare.ts +7 -0
  436. package/src/server/storage/local-object-store.ts +50 -0
  437. package/src/server/storage/object-store.ts +160 -0
  438. package/src/server/teams/creation.ts +51 -0
  439. package/src/server/teams/delete-policy.ts +13 -0
  440. package/src/server/teams/deletion-state.ts +80 -0
  441. package/src/server/teams/functions.ts +324 -0
  442. package/src/server/teams/invitations.ts +38 -0
  443. package/src/server/teams/membership.ts +86 -0
  444. package/src/server/teams/queries.ts +261 -0
  445. package/src/server/teams/service.ts +1127 -0
  446. package/src/server/teams/types.ts +101 -0
  447. package/src/server/uploads/avatar.ts +133 -0
  448. package/src/server/users/deletion-state.ts +93 -0
  449. package/src/server/users/functions.ts +160 -0
  450. package/src/server/users/onboarding.ts +17 -0
  451. package/src/server/users/service.ts +644 -0
  452. package/src/server.ts +55 -0
  453. package/src/styles/shadcn.css +653 -0
  454. package/src/styles/theme.css +76 -0
  455. package/src/styles.css +177 -0
  456. package/wrangler.template.jsonc +37 -0
@@ -0,0 +1,1123 @@
1
+ import {
2
+ withWorkspaceAccess,
3
+ requireWorkspaceAdmission,
4
+ } from '#devpush/server/plugins/workspace-access'
5
+ import type { WorkspaceAdmission } from '#devpush/server/plugins/workspace-access'
6
+ import { resolveGeneratedHostname } from './generated-hostname'
7
+ import type { ApiMutationAdmission } from '#devpush/server/api/admission'
8
+ import { PublicError, getPublicErrorMessage } from '#devpush/lib/public-error'
9
+ import { safeErrorDiagnostics } from '#devpush/server/http/error-diagnostics'
10
+ import { notifyProject } from '#devpush/server/realtime/service'
11
+ import { enqueueDeployment } from './queue-state'
12
+ import { and, asc, eq, sql } from 'drizzle-orm'
13
+ import { db } from '#devpush/server/db'
14
+ import { canAgentRunDeployments } from '#devpush/server/agents/compatibility'
15
+ import {
16
+ aliases,
17
+ deployments,
18
+ domains,
19
+ environments,
20
+ projects,
21
+ servers,
22
+ } from '#devpush/server/db/schema'
23
+ import {
24
+ createInstallationAccessToken,
25
+ getBranchHeadCommit,
26
+ listBranchCommits,
27
+ listRepositoryBranches,
28
+ } from '#devpush/server/github/service'
29
+ import {
30
+ encryptDeploymentEnvVars,
31
+ normalizeDeploymentCommitMeta,
32
+ parseDeploymentConfig,
33
+ toDeploymentCommitMeta,
34
+ } from './deployment-config'
35
+ import {
36
+ getEnvironmentForBranch,
37
+ groupBranchesByEnvironment,
38
+ } from './environment-branches'
39
+ import { resolveAvailableAliasHostname } from './hostnames'
40
+ import type {
41
+ DeploymentCommitMeta,
42
+ DeploymentConfig,
43
+ DeploymentEnvVar,
44
+ } from './deployment-config'
45
+ import { getEffectiveServerStatus } from '#devpush/server/servers/status'
46
+ import { requireProjectRuntimeConfig } from '#devpush/lib/project-runtime-config'
47
+ import { getProjectContext } from './context'
48
+ import {
49
+ expandPublicRoutesFromHostnames,
50
+ getEffectiveProjectRoutes,
51
+ getEnvVarsForEnvironment,
52
+ mapEnvironmentConfig,
53
+ normalizeProjectRoutes,
54
+ toRouteHostnames,
55
+ } from './mappers'
56
+ import type { RouteHostname } from './mappers'
57
+ import { readProjectEnvVars } from './project-env-vars'
58
+
59
+ export async function createManualDeployment({
60
+ environmentId,
61
+ gitBranch,
62
+ gitSha,
63
+ userId,
64
+ teamSlug,
65
+ projectName,
66
+ admission,
67
+ }: {
68
+ environmentId: string
69
+ gitBranch: string
70
+ gitSha?: string | null
71
+ userId: string
72
+ teamSlug: string
73
+ projectName: string
74
+ admission?: ApiMutationAdmission
75
+ }) {
76
+ const context = await getProjectContext({
77
+ userId,
78
+ teamSlug,
79
+ projectName,
80
+ scope: 'shell',
81
+ })
82
+ return withWorkspaceAccess(
83
+ context.team.id,
84
+ 'deployment.create',
85
+ async (workspaceAdmission) => {
86
+ const environmentConfig = context.environments.find(
87
+ (environment) => environment.id === environmentId,
88
+ )
89
+
90
+ if (!environmentConfig) {
91
+ throw new PublicError('DEPLOYMENT_ENVIRONMENT_NOT_FOUND')
92
+ }
93
+
94
+ const matchedEnvironment = getEnvironmentForBranch(
95
+ gitBranch,
96
+ context.environments,
97
+ )
98
+
99
+ if (matchedEnvironment?.id !== environmentConfig.id) {
100
+ throw new PublicError('DEPLOYMENT_BRANCH_MISMATCH')
101
+ }
102
+
103
+ const environmentRows = await db
104
+ .select({
105
+ environment: environments,
106
+ project: projects,
107
+ server: servers,
108
+ })
109
+ .from(environments)
110
+ .innerJoin(projects, eq(projects.id, environments.projectId))
111
+ .leftJoin(
112
+ servers,
113
+ eq(
114
+ servers.id,
115
+ sql`coalesce(${environments.serverId}, ${projects.defaultServerId})`,
116
+ ),
117
+ )
118
+ .where(
119
+ and(
120
+ eq(environments.id, environmentConfig.id),
121
+ eq(environments.projectId, context.project.id),
122
+ ),
123
+ )
124
+ .limit(1)
125
+ const environmentRow = environmentRows.at(0)
126
+
127
+ if (!environmentRow?.server) {
128
+ throw new PublicError('DEPLOYMENT_SERVER_REQUIRED')
129
+ }
130
+
131
+ if (
132
+ getEffectiveServerStatus({
133
+ status: environmentRow.server.status,
134
+ lastSeenAt: environmentRow.server.lastSeenAt,
135
+ }) !== 'connected'
136
+ ) {
137
+ throw new PublicError('DEPLOYMENT_SERVER_OFFLINE')
138
+ }
139
+
140
+ if (
141
+ !canAgentRunDeployments({
142
+ protocolVersion: environmentRow.server.agentProtocolVersion,
143
+ capabilities: environmentRow.server.agentCapabilities,
144
+ })
145
+ ) {
146
+ throw new PublicError('DEPLOYMENT_AGENT_UPDATE_REQUIRED')
147
+ }
148
+
149
+ if (
150
+ !context.project.githubInstallationId ||
151
+ !context.project.githubRepoId
152
+ ) {
153
+ throw new PublicError('DEPLOYMENT_GITHUB_INSTALLATION_REQUIRED')
154
+ }
155
+
156
+ const installationToken = await createInstallationAccessToken({
157
+ installationId: context.project.githubInstallationId,
158
+ repositoryId: context.project.githubRepoId,
159
+ repositoryFullName: context.project.githubRepo,
160
+ })
161
+ const commit = gitSha
162
+ ? (
163
+ await listBranchCommits({
164
+ accessToken: installationToken.token,
165
+ branch: gitBranch,
166
+ repo: context.project.githubRepo,
167
+ perPage: 5,
168
+ })
169
+ ).find((candidate) => candidate.sha === gitSha)
170
+ : await getBranchHeadCommit({
171
+ accessToken: installationToken.token,
172
+ branch: gitBranch,
173
+ repo: context.project.githubRepo,
174
+ })
175
+
176
+ if (!commit) {
177
+ throw new PublicError('DEPLOYMENT_COMMIT_UNAVAILABLE')
178
+ }
179
+
180
+ const deployment = await createDeploymentForEnvironment({
181
+ workspaceAdmission,
182
+ deploymentId: admission?.resourceId,
183
+ admission,
184
+ environmentId: environmentConfig.id,
185
+ gitBranch,
186
+ gitSha: commit.sha,
187
+ trigger: admission ? 'api' : 'user',
188
+ commitMeta: toDeploymentCommitMeta(commit),
189
+ createdByUserId: userId,
190
+ })
191
+
192
+ return {
193
+ deploymentId: deployment.deploymentId,
194
+ }
195
+ },
196
+ { kind: 'user', userId },
197
+ )
198
+ }
199
+
200
+ export async function createRollbackDeployment({
201
+ deploymentId,
202
+ userId,
203
+ teamSlug,
204
+ projectName,
205
+ admission,
206
+ }: {
207
+ deploymentId: string
208
+ userId: string
209
+ teamSlug: string
210
+ projectName: string
211
+ admission?: ApiMutationAdmission
212
+ }) {
213
+ const context = await getProjectContext({
214
+ userId,
215
+ teamSlug,
216
+ projectName,
217
+ scope: 'shell',
218
+ })
219
+ return withWorkspaceAccess(
220
+ context.team.id,
221
+ 'deployment.create',
222
+ async (workspaceAdmission) => {
223
+ const rows = await db
224
+ .select({
225
+ deployment: deployments,
226
+ })
227
+ .from(deployments)
228
+ .innerJoin(environments, eq(environments.id, deployments.environmentId))
229
+ .where(
230
+ and(
231
+ eq(deployments.id, deploymentId),
232
+ eq(environments.projectId, context.project.id),
233
+ ),
234
+ )
235
+ .limit(1)
236
+ const row = rows.at(0)
237
+ const source = row?.deployment
238
+
239
+ if (!source) {
240
+ throw new PublicError('DEPLOYMENT_NOT_FOUND')
241
+ }
242
+
243
+ if (source.status !== 'succeeded') {
244
+ throw new PublicError('DEPLOYMENT_ROLLBACK_UNSUCCESSFUL')
245
+ }
246
+
247
+ const deploymentConfig = parseDeploymentConfig(source.config)
248
+
249
+ if (!deploymentConfig) {
250
+ throw new PublicError('DEPLOYMENT_CONFIG_INVALID')
251
+ }
252
+
253
+ const serverRows = await db
254
+ .select()
255
+ .from(servers)
256
+ .where(eq(servers.id, source.serverId))
257
+ .limit(1)
258
+ const deploymentServer = serverRows.at(0)
259
+
260
+ if (!deploymentServer) {
261
+ throw new PublicError('DEPLOYMENT_SERVER_NOT_FOUND')
262
+ }
263
+
264
+ const serverStatus = getEffectiveServerStatus({
265
+ status: deploymentServer.status,
266
+ lastSeenAt: deploymentServer.lastSeenAt,
267
+ })
268
+
269
+ if (serverStatus !== 'connected') {
270
+ throw new PublicError('DEPLOYMENT_ROLLBACK_SERVER_OFFLINE')
271
+ }
272
+
273
+ if (
274
+ !canAgentRunDeployments({
275
+ protocolVersion: deploymentServer.agentProtocolVersion,
276
+ capabilities: deploymentServer.agentCapabilities,
277
+ })
278
+ ) {
279
+ throw new PublicError('DEPLOYMENT_ROLLBACK_AGENT_UPDATE_REQUIRED')
280
+ }
281
+
282
+ const rollbackDeploymentId = admission?.resourceId ?? crypto.randomUUID()
283
+ const rollbackHost = await resolveGeneratedHostname({
284
+ kind: 'deployment',
285
+ serverId: deploymentServer.id,
286
+ deploymentId: rollbackDeploymentId,
287
+ projectName: context.project.name,
288
+ serverPublicAddress: deploymentServer.publicAddress,
289
+ })
290
+ const rollbackAliasHostname = rollbackHost.hostname
291
+ const rollbackConfig = await replayDeploymentConfig({
292
+ projectId: context.project.id,
293
+ environmentId: source.environmentId,
294
+ serverId: deploymentServer.id,
295
+ source: deploymentConfig,
296
+ gitBranch: source.gitBranch,
297
+ hostname: rollbackAliasHostname,
298
+ https: rollbackHost.https,
299
+ })
300
+ const now = new Date()
301
+ const commitMeta = normalizeDeploymentCommitMeta(source.commitMeta)
302
+ const created = await enqueueDeployment(db, {
303
+ workspaceAdmission,
304
+ deployment: {
305
+ id: rollbackDeploymentId,
306
+ environmentId: source.environmentId,
307
+ serverId: deploymentServer.id,
308
+ trigger: 'rollback',
309
+ repoId: source.repoId,
310
+ repoFullName: source.repoFullName,
311
+ gitSha: source.gitSha,
312
+ gitBranch: source.gitBranch,
313
+ commitMeta,
314
+ config: rollbackConfig,
315
+ envVars: source.envVars,
316
+ runtimeId: source.runtimeId,
317
+ createdByUserId: userId,
318
+ },
319
+ projectId: context.project.id,
320
+ hostname: rollbackAliasHostname,
321
+ https: rollbackHost.https,
322
+ canceledMessage: 'Canceled because a rollback deployment was queued.',
323
+ now,
324
+ admission,
325
+ })
326
+
327
+ if (!created) throw new PublicError('DEPLOYMENT_ENVIRONMENT_INACTIVE')
328
+ await notifyProject(context.project.id)
329
+
330
+ return {
331
+ deploymentId: rollbackDeploymentId,
332
+ }
333
+ },
334
+ { kind: 'user', userId },
335
+ )
336
+ }
337
+
338
+ export async function createRedeployDeployment({
339
+ deploymentId,
340
+ userId,
341
+ teamSlug,
342
+ projectName,
343
+ admission,
344
+ }: {
345
+ deploymentId: string
346
+ userId: string
347
+ teamSlug: string
348
+ projectName: string
349
+ admission?: ApiMutationAdmission
350
+ }) {
351
+ const context = await getProjectContext({
352
+ userId,
353
+ teamSlug,
354
+ projectName,
355
+ scope: 'shell',
356
+ })
357
+ return withWorkspaceAccess(
358
+ context.team.id,
359
+ 'deployment.create',
360
+ async (workspaceAdmission) => {
361
+ const rows = await db
362
+ .select({ deployment: deployments })
363
+ .from(deployments)
364
+ .innerJoin(environments, eq(environments.id, deployments.environmentId))
365
+ .where(
366
+ and(
367
+ eq(deployments.id, deploymentId),
368
+ eq(environments.projectId, context.project.id),
369
+ ),
370
+ )
371
+ .limit(1)
372
+ const source = rows.at(0)?.deployment
373
+
374
+ if (!source) {
375
+ throw new PublicError('DEPLOYMENT_NOT_FOUND')
376
+ }
377
+
378
+ if (source.status === 'queued' || source.status === 'running') {
379
+ throw new PublicError('DEPLOYMENT_REDEPLOY_ACTIVE')
380
+ }
381
+
382
+ const deploymentConfig = parseDeploymentConfig(source.config)
383
+
384
+ if (!deploymentConfig) {
385
+ throw new PublicError('DEPLOYMENT_CONFIG_INVALID')
386
+ }
387
+
388
+ const serverRows = await db
389
+ .select()
390
+ .from(servers)
391
+ .where(eq(servers.id, source.serverId))
392
+ .limit(1)
393
+ const deploymentServer = serverRows.at(0)
394
+
395
+ if (!deploymentServer) {
396
+ throw new PublicError('DEPLOYMENT_SERVER_NOT_FOUND')
397
+ }
398
+
399
+ const serverStatus = getEffectiveServerStatus({
400
+ status: deploymentServer.status,
401
+ lastSeenAt: deploymentServer.lastSeenAt,
402
+ })
403
+
404
+ if (serverStatus !== 'connected') {
405
+ throw new PublicError('DEPLOYMENT_REDEPLOY_SERVER_OFFLINE')
406
+ }
407
+
408
+ if (
409
+ !canAgentRunDeployments({
410
+ protocolVersion: deploymentServer.agentProtocolVersion,
411
+ capabilities: deploymentServer.agentCapabilities,
412
+ })
413
+ ) {
414
+ throw new PublicError('DEPLOYMENT_REDEPLOY_AGENT_UPDATE_REQUIRED')
415
+ }
416
+
417
+ const redeploymentId = admission?.resourceId ?? crypto.randomUUID()
418
+ const redeploymentHost = await resolveGeneratedHostname({
419
+ kind: 'deployment',
420
+ serverId: deploymentServer.id,
421
+ deploymentId: redeploymentId,
422
+ projectName: context.project.name,
423
+ serverPublicAddress: deploymentServer.publicAddress,
424
+ })
425
+ const redeploymentAliasHostname = redeploymentHost.hostname
426
+ const redeploymentConfig = await replayDeploymentConfig({
427
+ projectId: context.project.id,
428
+ environmentId: source.environmentId,
429
+ serverId: deploymentServer.id,
430
+ source: deploymentConfig,
431
+ gitBranch: source.gitBranch,
432
+ hostname: redeploymentAliasHostname,
433
+ https: redeploymentHost.https,
434
+ })
435
+ const now = new Date()
436
+ const commitMeta = normalizeDeploymentCommitMeta(source.commitMeta)
437
+ const created = await enqueueDeployment(db, {
438
+ workspaceAdmission,
439
+ deployment: {
440
+ id: redeploymentId,
441
+ environmentId: source.environmentId,
442
+ serverId: deploymentServer.id,
443
+ trigger: admission ? 'api' : 'user',
444
+ repoId: source.repoId,
445
+ repoFullName: source.repoFullName,
446
+ gitSha: source.gitSha,
447
+ gitBranch: source.gitBranch,
448
+ commitMeta,
449
+ config: redeploymentConfig,
450
+ envVars: source.envVars,
451
+ runtimeId: source.runtimeId,
452
+ createdByUserId: userId,
453
+ },
454
+ projectId: context.project.id,
455
+ hostname: redeploymentAliasHostname,
456
+ https: redeploymentHost.https,
457
+ canceledMessage: 'Canceled because a redeployment was queued.',
458
+ now,
459
+ admission,
460
+ })
461
+
462
+ if (!created) throw new PublicError('DEPLOYMENT_ENVIRONMENT_INACTIVE')
463
+ await notifyProject(context.project.id)
464
+
465
+ return {
466
+ deploymentId: redeploymentId,
467
+ }
468
+ },
469
+ { kind: 'user', userId },
470
+ )
471
+ }
472
+
473
+ export async function getManualDeploymentOptions({
474
+ environmentId,
475
+ branch,
476
+ userId,
477
+ teamSlug,
478
+ projectName,
479
+ }: {
480
+ branch?: string
481
+ environmentId: string
482
+ userId: string
483
+ teamSlug: string
484
+ projectName: string
485
+ }) {
486
+ const context = await getProjectContext({
487
+ userId,
488
+ teamSlug,
489
+ projectName,
490
+ scope: 'shell',
491
+ })
492
+ const environment = context.environments.find(
493
+ (candidate) => candidate.id === environmentId,
494
+ )
495
+
496
+ if (!environment) {
497
+ throw new PublicError('DEPLOYMENT_ENVIRONMENT_NOT_FOUND')
498
+ }
499
+
500
+ if (!context.project.githubInstallationId || !context.project.githubRepoId) {
501
+ throw new PublicError('DEPLOYMENT_GITHUB_INSTALLATION_REQUIRED')
502
+ }
503
+
504
+ const installationToken = await createInstallationAccessToken({
505
+ installationId: context.project.githubInstallationId,
506
+ repositoryId: context.project.githubRepoId,
507
+ repositoryFullName: context.project.githubRepo,
508
+ })
509
+ const branches = await listRepositoryBranches({
510
+ accessToken: installationToken.token,
511
+ repo: context.project.githubRepo,
512
+ })
513
+ const matchingBranches =
514
+ groupBranchesByEnvironment(context.environments, branches).get(
515
+ environment.id,
516
+ ) ?? []
517
+ if (branch && !matchingBranches.includes(branch)) {
518
+ throw new PublicError('DEPLOYMENT_BRANCH_MISMATCH')
519
+ }
520
+ const selectedBranch = branch ?? matchingBranches[0] ?? null
521
+ const commits = selectedBranch
522
+ ? (
523
+ await listBranchCommits({
524
+ accessToken: installationToken.token,
525
+ branch: selectedBranch,
526
+ repo: context.project.githubRepo,
527
+ perPage: 5,
528
+ })
529
+ ).map((commit) => ({ ...commit, branch: selectedBranch }))
530
+ : []
531
+
532
+ return {
533
+ environment: {
534
+ id: environment.id,
535
+ name: environment.name,
536
+ branch: environment.branch,
537
+ color: environment.color,
538
+ },
539
+ branches: matchingBranches,
540
+ selectedBranch,
541
+ commits,
542
+ }
543
+ }
544
+
545
+ export async function createDeploymentsForGitHubPush({
546
+ deliveryId,
547
+ installationId,
548
+ repositoryId,
549
+ repositoryFullName,
550
+ branch,
551
+ gitSha,
552
+ message,
553
+ authorName = null,
554
+ authoredAt = null,
555
+ commitUrl = null,
556
+ githubSenderId,
557
+ }: {
558
+ deliveryId: string
559
+ installationId: number
560
+ repositoryId: number
561
+ repositoryFullName: string
562
+ branch: string
563
+ gitSha: string
564
+ message: string | null
565
+ authorName?: string | null
566
+ authoredAt?: string | null
567
+ commitUrl?: string | null
568
+ githubSenderId?: string
569
+ }) {
570
+ const candidateRows = await db
571
+ .select({
572
+ project: projects,
573
+ environment: environments,
574
+ server: servers,
575
+ })
576
+ .from(projects)
577
+ .innerJoin(environments, eq(environments.projectId, projects.id))
578
+ .leftJoin(
579
+ servers,
580
+ eq(
581
+ servers.id,
582
+ sql`coalesce(${environments.serverId}, ${projects.defaultServerId})`,
583
+ ),
584
+ )
585
+ .where(
586
+ and(
587
+ eq(projects.githubInstallationId, installationId),
588
+ eq(projects.githubRepoId, repositoryId),
589
+ eq(projects.status, 'active'),
590
+ eq(environments.status, 'active'),
591
+ ),
592
+ )
593
+ .orderBy(asc(projects.id), asc(environments.position))
594
+ // Repository IDs survive renames; only a signed delivery for the same
595
+ // installation may refresh the mutable full name.
596
+ if (candidateRows.length > 0) {
597
+ await db
598
+ .update(projects)
599
+ .set({ githubRepo: repositoryFullName, updatedAt: new Date() })
600
+ .where(
601
+ and(
602
+ eq(projects.githubInstallationId, installationId),
603
+ eq(projects.githubRepoId, repositoryId),
604
+ eq(projects.status, 'active'),
605
+ sql`${projects.githubRepo} <> ${repositoryFullName}`,
606
+ ),
607
+ )
608
+ }
609
+ const candidatesByProject = new Map<
610
+ string,
611
+ Array<(typeof candidateRows)[number]>
612
+ >()
613
+
614
+ for (const row of candidateRows) {
615
+ const candidates = candidatesByProject.get(row.project.id) ?? []
616
+ candidates.push(row)
617
+ candidatesByProject.set(row.project.id, candidates)
618
+ }
619
+
620
+ const rows = Array.from(candidatesByProject.values()).flatMap(
621
+ (candidates) => {
622
+ const environmentConfig = getEnvironmentForBranch(
623
+ branch,
624
+ candidates.map((candidate) =>
625
+ mapEnvironmentConfig(
626
+ candidate.environment,
627
+ candidate.project.defaultServerId,
628
+ ),
629
+ ),
630
+ )
631
+
632
+ if (!environmentConfig) {
633
+ return []
634
+ }
635
+
636
+ const matched = candidates.find(
637
+ (candidate) =>
638
+ candidate.environment.id === environmentConfig.id ||
639
+ candidate.environment.slug === environmentConfig.slug,
640
+ )
641
+
642
+ return matched ? [matched] : []
643
+ },
644
+ )
645
+
646
+ const created: Array<{
647
+ deploymentId: string
648
+ projectId: string
649
+ environmentId: string
650
+ }> = []
651
+ const skipped: Array<{
652
+ projectId: string
653
+ environmentId: string
654
+ reason: string
655
+ }> = []
656
+
657
+ let unrecordedFailure = false
658
+ for (const row of rows) {
659
+ try {
660
+ const serverStatus = row.server
661
+ ? getEffectiveServerStatus({
662
+ status: row.server.status,
663
+ lastSeenAt: row.server.lastSeenAt,
664
+ })
665
+ : null
666
+
667
+ if (!row.server || row.server.status === 'deleted') {
668
+ skipped.push({
669
+ projectId: row.project.id,
670
+ environmentId: row.environment.id,
671
+ reason: 'server_disconnected',
672
+ })
673
+ continue
674
+ }
675
+ const skipReason =
676
+ serverStatus !== 'connected'
677
+ ? 'server_disconnected'
678
+ : !canAgentRunDeployments({
679
+ protocolVersion: row.server.agentProtocolVersion,
680
+ capabilities: row.server.agentCapabilities,
681
+ })
682
+ ? 'agent_update_required'
683
+ : null
684
+ const failureMessage =
685
+ skipReason === 'server_disconnected'
686
+ ? 'GitHub push could not deploy because the assigned server is disconnected.'
687
+ : skipReason === 'agent_update_required'
688
+ ? 'GitHub push could not deploy because the assigned agent requires an update.'
689
+ : undefined
690
+
691
+ const deployment = await createDeploymentForEnvironment({
692
+ githubDeliveryId: deliveryId,
693
+ environmentId: row.environment.id,
694
+ gitBranch: branch,
695
+ gitSha,
696
+ trigger: 'webhook',
697
+ commitMeta: {
698
+ message,
699
+ authorName,
700
+ authoredAt,
701
+ url: commitUrl,
702
+ ...(githubSenderId ? { githubSenderId } : {}),
703
+ },
704
+ failureMessage,
705
+ })
706
+
707
+ if (deployment.status === 'created' && skipReason) {
708
+ skipped.push({
709
+ projectId: row.project.id,
710
+ environmentId: row.environment.id,
711
+ reason: skipReason,
712
+ })
713
+ } else if (deployment.status === 'created') {
714
+ created.push({
715
+ deploymentId: deployment.deploymentId,
716
+ projectId: row.project.id,
717
+ environmentId: row.environment.id,
718
+ })
719
+ } else {
720
+ skipped.push({
721
+ projectId: row.project.id,
722
+ environmentId: row.environment.id,
723
+ reason: 'duplicate_delivery',
724
+ })
725
+ }
726
+ } catch (error) {
727
+ // A bad environment must not discard this delivery for unrelated projects.
728
+ // Record a failed attempt without requiring hostname/certificate readiness.
729
+ const failureMessage = getPublicErrorMessage(
730
+ error,
731
+ 'GitHub push could not deploy. Review workspace settings and assigned server readiness.',
732
+ )
733
+ try {
734
+ await createDeploymentForEnvironment({
735
+ githubDeliveryId: deliveryId,
736
+ environmentId: row.environment.id,
737
+ gitBranch: branch,
738
+ gitSha,
739
+ trigger: 'webhook',
740
+ commitMeta: {
741
+ message,
742
+ authorName,
743
+ authoredAt,
744
+ url: commitUrl,
745
+ ...(githubSenderId ? { githubSenderId } : {}),
746
+ },
747
+ failureMessage,
748
+ })
749
+ } catch (recordError) {
750
+ unrecordedFailure = true
751
+ console.error(
752
+ 'Unable to record failed push deployment',
753
+ safeErrorDiagnostics(recordError),
754
+ )
755
+ }
756
+ console.error(
757
+ 'Push deployment admission failed',
758
+ safeErrorDiagnostics(error),
759
+ )
760
+ skipped.push({
761
+ projectId: row.project.id,
762
+ environmentId: row.environment.id,
763
+ reason: 'deployment_failed',
764
+ })
765
+ }
766
+ }
767
+
768
+ if (unrecordedFailure) throw new PublicError('INTERNAL_ERROR')
769
+
770
+ return {
771
+ matched: rows.length,
772
+ created,
773
+ skipped,
774
+ }
775
+ }
776
+
777
+ async function createDeploymentForEnvironment({
778
+ deploymentId = crypto.randomUUID(),
779
+ environmentId,
780
+ gitBranch,
781
+ gitSha,
782
+ trigger,
783
+ commitMeta,
784
+ createdByUserId,
785
+ githubDeliveryId,
786
+ failureMessage,
787
+ admission,
788
+ workspaceAdmission: existingAdmission,
789
+ }: {
790
+ deploymentId?: string
791
+ environmentId: string
792
+ gitBranch: string
793
+ gitSha: string
794
+ trigger: 'user' | 'webhook' | 'api' | 'rollback'
795
+ commitMeta: DeploymentCommitMeta
796
+ createdByUserId?: string
797
+ githubDeliveryId?: string
798
+ failureMessage?: string
799
+ admission?: ApiMutationAdmission
800
+ workspaceAdmission?: WorkspaceAdmission
801
+ }): Promise<{
802
+ status: 'created' | 'existing'
803
+ deploymentId: string
804
+ }> {
805
+ const now = new Date()
806
+ const environmentRows = await db
807
+ .select({
808
+ environment: environments,
809
+ project: projects,
810
+ server: servers,
811
+ })
812
+ .from(environments)
813
+ .innerJoin(projects, eq(projects.id, environments.projectId))
814
+ .leftJoin(
815
+ servers,
816
+ eq(
817
+ servers.id,
818
+ sql`coalesce(${environments.serverId}, ${projects.defaultServerId})`,
819
+ ),
820
+ )
821
+ .where(eq(environments.id, environmentId))
822
+ .limit(1)
823
+ const environmentRow = environmentRows.at(0)
824
+
825
+ if (!environmentRow) {
826
+ throw new PublicError('DEPLOYMENT_ENVIRONMENT_NOT_FOUND')
827
+ }
828
+ const apply = async (
829
+ workspaceAdmission?: WorkspaceAdmission,
830
+ ): Promise<{ status: 'created' | 'existing'; deploymentId: string }> => {
831
+ if (!environmentRow.server) {
832
+ throw new PublicError('DEPLOYMENT_SERVER_REQUIRED')
833
+ }
834
+ if (!environmentRow.project.githubRepoId) {
835
+ throw new PublicError('PROJECT_REPOSITORY_MISSING')
836
+ }
837
+ const server = environmentRow.server
838
+ const repoId = environmentRow.project.githubRepoId
839
+
840
+ if (server.status === 'deleted') {
841
+ throw new PublicError('DEPLOYMENT_SERVER_DELETED')
842
+ }
843
+
844
+ const [aliasRows, domainRows] = await Promise.all([
845
+ db
846
+ .select()
847
+ .from(aliases)
848
+ .where(
849
+ and(
850
+ eq(aliases.projectId, environmentRow.project.id),
851
+ eq(aliases.environmentId, environmentRow.environment.id),
852
+ ),
853
+ ),
854
+ db
855
+ .select()
856
+ .from(domains)
857
+ .where(
858
+ and(
859
+ eq(domains.projectId, environmentRow.project.id),
860
+ eq(domains.environmentId, environmentRow.environment.id),
861
+ ),
862
+ ),
863
+ ])
864
+
865
+ const branchAlias = aliasRows.find(
866
+ (alias) => alias.type === 'branch' && alias.branch === gitBranch,
867
+ )
868
+ const generatedBranchHost = failureMessage
869
+ ? { hostname: '', https: false }
870
+ : await resolveGeneratedHostname({
871
+ kind: 'branch',
872
+ serverId: server.id,
873
+ branch: gitBranch,
874
+ projectName: environmentRow.project.name,
875
+ serverPublicAddress: server.publicAddress,
876
+ })
877
+ const requestedBranchAliasHostname =
878
+ branchAlias?.hostname ?? generatedBranchHost.hostname
879
+ const branchAliasHostname = failureMessage
880
+ ? ''
881
+ : await resolveAvailableAliasHostname({
882
+ excludeAliasId: branchAlias?.id,
883
+ hostname: requestedBranchAliasHostname,
884
+ uniquenessKey: `${environmentRow.project.id}:${gitBranch}`,
885
+ })
886
+ const generatedDeploymentHost = failureMessage
887
+ ? { hostname: '', https: false }
888
+ : await resolveGeneratedHostname({
889
+ kind: 'deployment',
890
+ serverId: server.id,
891
+ deploymentId,
892
+ projectName: environmentRow.project.name,
893
+ serverPublicAddress: server.publicAddress,
894
+ })
895
+ const requestedDeploymentAliasHostname = generatedDeploymentHost.hostname
896
+ const deploymentAliasHostname = failureMessage
897
+ ? ''
898
+ : await resolveAvailableAliasHostname({
899
+ hostname: requestedDeploymentAliasHostname,
900
+ uniquenessKey: deploymentId,
901
+ })
902
+ const deploymentConfig = createDeploymentConfig({
903
+ environment: environmentRow.environment,
904
+ aliasRows,
905
+ domainRows,
906
+ gitBranch,
907
+ extraHostnames: failureMessage
908
+ ? []
909
+ : [
910
+ {
911
+ hostname: branchAliasHostname,
912
+ environmentId,
913
+ basePath: null,
914
+ https: generatedBranchHost.https,
915
+ },
916
+ {
917
+ hostname: deploymentAliasHostname,
918
+ environmentId,
919
+ basePath: null,
920
+ https: generatedDeploymentHost.https,
921
+ },
922
+ ],
923
+ project: environmentRow.project,
924
+ })
925
+ const envVars = encryptDeploymentEnvVars(
926
+ getDeploymentEnvVars({
927
+ environment: environmentRow.environment,
928
+ project: environmentRow.project,
929
+ }),
930
+ )
931
+
932
+ const created = await enqueueDeployment(db, {
933
+ workspaceAdmission,
934
+ deployment: {
935
+ id: deploymentId,
936
+ environmentId,
937
+ trigger,
938
+ githubDeliveryId,
939
+ serverId: server.id,
940
+ repoId,
941
+ repoFullName: environmentRow.project.githubRepo,
942
+ gitSha,
943
+ gitBranch,
944
+ commitMeta: normalizeDeploymentCommitMeta(commitMeta),
945
+ config: deploymentConfig,
946
+ envVars,
947
+ runtimeId: environmentRow.environment.runtimeId,
948
+ createdByUserId,
949
+ },
950
+ projectId: environmentRow.project.id,
951
+ hostname: deploymentAliasHostname,
952
+ branchHostname: branchAlias ? undefined : branchAliasHostname,
953
+ https: generatedDeploymentHost.https,
954
+ failureMessage,
955
+ canceledMessage: `Canceled because a newer ${trigger} deployment was queued.`,
956
+ now,
957
+ admission,
958
+ })
959
+
960
+ if (created) await notifyProject(environmentRow.project.id)
961
+ return { status: created ? 'created' : 'existing', deploymentId }
962
+ }
963
+ if (failureMessage) return apply()
964
+ if (existingAdmission) {
965
+ requireWorkspaceAdmission(
966
+ existingAdmission,
967
+ environmentRow.project.teamId,
968
+ 'deployment.create',
969
+ )
970
+ return apply(existingAdmission)
971
+ }
972
+ return withWorkspaceAccess(
973
+ environmentRow.project.teamId,
974
+ 'deployment.create',
975
+ apply,
976
+ createdByUserId
977
+ ? { kind: 'user', userId: createdByUserId }
978
+ : { kind: 'system', source: 'github.push' },
979
+ )
980
+ }
981
+
982
+ function createDeploymentConfig({
983
+ environment,
984
+ aliasRows,
985
+ domainRows,
986
+ extraHostnames = [],
987
+ gitBranch,
988
+ project,
989
+ }: {
990
+ environment: typeof environments.$inferSelect
991
+ aliasRows: Array<typeof aliases.$inferSelect>
992
+ domainRows: Array<typeof domains.$inferSelect>
993
+ extraHostnames?: Array<RouteHostname>
994
+ gitBranch: string
995
+ project: typeof projects.$inferSelect
996
+ }): DeploymentConfig {
997
+ const { routes } = resolveDeploymentPublicRoutes({
998
+ environment,
999
+ aliasRows,
1000
+ domainRows,
1001
+ extraHostnames,
1002
+ gitBranch,
1003
+ project,
1004
+ })
1005
+ const runtime = requireProjectRuntimeConfig(project.runtimeConfig)
1006
+ const releaseService = runtime.releaseService ?? routes.at(0)?.service ?? null
1007
+
1008
+ return {
1009
+ schemaVersion: 1,
1010
+ runtime: {
1011
+ ...runtime,
1012
+ path: runtime.path ?? 'compose.yml',
1013
+ releaseService,
1014
+ },
1015
+ routes,
1016
+ }
1017
+ }
1018
+
1019
+ function resolveDeploymentPublicRoutes({
1020
+ environment,
1021
+ aliasRows,
1022
+ domainRows,
1023
+ extraHostnames = [],
1024
+ gitBranch,
1025
+ project,
1026
+ }: {
1027
+ environment: typeof environments.$inferSelect
1028
+ aliasRows: Array<typeof aliases.$inferSelect>
1029
+ domainRows: Array<typeof domains.$inferSelect>
1030
+ extraHostnames?: Array<RouteHostname>
1031
+ gitBranch?: string
1032
+ project: typeof projects.$inferSelect
1033
+ }) {
1034
+ const projectRoutes = getEffectiveProjectRoutes(
1035
+ normalizeProjectRoutes(project.routes),
1036
+ environment.id,
1037
+ )
1038
+ const environmentAliases = aliasRows.filter(
1039
+ (alias) =>
1040
+ alias.environmentId === environment.id &&
1041
+ (alias.type === 'environment' ||
1042
+ (alias.type === 'branch' && alias.branch === gitBranch)),
1043
+ )
1044
+ const environmentDomains = domainRows.filter(
1045
+ (domain) =>
1046
+ domain.environmentId === environment.id && domain.status === 'active',
1047
+ )
1048
+ const routeHostnames = toRouteHostnames(
1049
+ environmentAliases,
1050
+ environmentDomains,
1051
+ ).concat(extraHostnames)
1052
+ const defaultHost = environmentAliases.at(0)?.hostname ?? null
1053
+ const routes = expandPublicRoutesFromHostnames(projectRoutes, routeHostnames)
1054
+
1055
+ return {
1056
+ defaultHost,
1057
+ routes,
1058
+ }
1059
+ }
1060
+
1061
+ function getDeploymentEnvVars({
1062
+ environment,
1063
+ project,
1064
+ }: {
1065
+ environment: typeof environments.$inferSelect
1066
+ project: typeof projects.$inferSelect
1067
+ }): Array<DeploymentEnvVar> {
1068
+ return getEnvVarsForEnvironment(
1069
+ readProjectEnvVars(project),
1070
+ environment.id,
1071
+ ).map((envVar) => ({
1072
+ key: envVar.key,
1073
+ value: envVar.value,
1074
+ }))
1075
+ }
1076
+
1077
+ async function replayDeploymentConfig({
1078
+ projectId,
1079
+ environmentId,
1080
+ serverId,
1081
+ source,
1082
+ gitBranch,
1083
+ hostname,
1084
+ https,
1085
+ }: {
1086
+ projectId: string
1087
+ environmentId: string
1088
+ serverId: string
1089
+ source: DeploymentConfig
1090
+ gitBranch: string
1091
+ hostname: string
1092
+ https: boolean
1093
+ }): Promise<DeploymentConfig> {
1094
+ const [project] = await db
1095
+ .select()
1096
+ .from(projects)
1097
+ .where(eq(projects.id, projectId))
1098
+ .limit(1)
1099
+ if (!project || project.status !== 'active')
1100
+ throw new PublicError('PROJECT_NOT_FOUND')
1101
+ const [environment] = await db
1102
+ .select()
1103
+ .from(environments)
1104
+ .where(eq(environments.id, environmentId))
1105
+ .limit(1)
1106
+ if (!environment || environment.status !== 'active')
1107
+ throw new PublicError('DEPLOYMENT_ENVIRONMENT_INACTIVE')
1108
+ if ((environment.serverId ?? project.defaultServerId) !== serverId)
1109
+ throw new PublicError('DEPLOYMENT_SERVER_CHANGED')
1110
+ const [aliasRows, domainRows] = await Promise.all([
1111
+ db.select().from(aliases).where(eq(aliases.projectId, project.id)),
1112
+ db.select().from(domains).where(eq(domains.projectId, project.id)),
1113
+ ])
1114
+ const { routes } = resolveDeploymentPublicRoutes({
1115
+ project,
1116
+ environment,
1117
+ aliasRows,
1118
+ domainRows,
1119
+ gitBranch,
1120
+ extraHostnames: [{ hostname, https, environmentId, basePath: null }],
1121
+ })
1122
+ return { ...source, routes }
1123
+ }