devpush 0.0.0-stage → 1.0.0-beta.3
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.
- package/README.md +6 -2
- package/agent-release.json +37 -0
- package/dist/package/LICENSE.md +21 -0
- package/dist/package/migrate.mjs +182 -0
- package/dist/package/plugins.mjs +125 -0
- package/dist/package/vite.mjs +439 -0
- package/package.json +170 -4
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon.svg +1 -0
- package/public/logo-email.png +0 -0
- package/public/robots.txt +3 -0
- package/public/social.png +0 -0
- package/src/cloudflare-runtime.d.ts +78 -0
- package/src/components/about-dialog.tsx +70 -0
- package/src/components/copy-button.tsx +74 -0
- package/src/components/logo-mark.tsx +20 -0
- package/src/components/pending-collection.tsx +103 -0
- package/src/components/resource-avatar.tsx +65 -0
- package/src/components/route-error-state.tsx +95 -0
- package/src/components/section-navigation.tsx +39 -0
- package/src/components/settings-layout.tsx +111 -0
- package/src/components/storage-capabilities.tsx +11 -0
- package/src/components/table-pager.tsx +101 -0
- package/src/components/ui/alert-dialog.tsx +180 -0
- package/src/components/ui/alert.tsx +78 -0
- package/src/components/ui/avatar.tsx +58 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/button-group.tsx +38 -0
- package/src/components/ui/button.tsx +67 -0
- package/src/components/ui/card.tsx +82 -0
- package/src/components/ui/chart.tsx +307 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/combobox.tsx +256 -0
- package/src/components/ui/dialog.tsx +163 -0
- package/src/components/ui/dropdown-menu.tsx +110 -0
- package/src/components/ui/empty.tsx +65 -0
- package/src/components/ui/field.tsx +192 -0
- package/src/components/ui/input-group.tsx +118 -0
- package/src/components/ui/input-otp.tsx +70 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/pagination.tsx +34 -0
- package/src/components/ui/popover.tsx +86 -0
- package/src/components/ui/select.tsx +145 -0
- package/src/components/ui/skeleton.tsx +19 -0
- package/src/components/ui/sonner.tsx +47 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +81 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/features/admin/admin-settings.tsx +1048 -0
- package/src/features/auth/current-user-context-query.ts +11 -0
- package/src/features/auth/login-form.tsx +442 -0
- package/src/features/auth/sign-out-menu-item.tsx +42 -0
- package/src/features/deployments/deployment-actions.tsx +336 -0
- package/src/features/deployments/deployment-alias-badge.tsx +35 -0
- package/src/features/deployments/deployment-detail.tsx +596 -0
- package/src/features/deployments/deployment-filters.ts +66 -0
- package/src/features/deployments/deployment-list.tsx +465 -0
- package/src/features/deployments/deployment-visit-links.tsx +104 -0
- package/src/features/deployments/deployments-page.tsx +376 -0
- package/src/features/logs/log-scroll-area.tsx +69 -0
- package/src/features/notifications/email-preferences.tsx +232 -0
- package/src/features/onboarding/profile-onboarding.tsx +162 -0
- package/src/features/plugins/contributions.tsx +118 -0
- package/src/features/projects/backup-job-logs.tsx +143 -0
- package/src/features/projects/backup-policy-delete-dialog.tsx +82 -0
- package/src/features/projects/backup-restore-actions.tsx +573 -0
- package/src/features/projects/backup-status-icon.tsx +33 -0
- package/src/features/projects/backup-target-list.tsx +222 -0
- package/src/features/projects/backup-timestamp.tsx +32 -0
- package/src/features/projects/backup-volume-picker.tsx +158 -0
- package/src/features/projects/compose-service-input.tsx +64 -0
- package/src/features/projects/deploy-readiness.ts +28 -0
- package/src/features/projects/environment-dot.tsx +91 -0
- package/src/features/projects/log-keyword-filter.tsx +48 -0
- package/src/features/projects/manual-deploy-dialog.tsx +563 -0
- package/src/features/projects/pending-section.ts +26 -0
- package/src/features/projects/project-backup-details-pending.tsx +305 -0
- package/src/features/projects/project-backup-details.tsx +936 -0
- package/src/features/projects/project-backup-policy.tsx +644 -0
- package/src/features/projects/project-backup-utils.ts +190 -0
- package/src/features/projects/project-backups-pending.tsx +136 -0
- package/src/features/projects/project-backups.tsx +876 -0
- package/src/features/projects/project-environment-settings.tsx +1128 -0
- package/src/features/projects/project-heading.tsx +68 -0
- package/src/features/projects/project-layout.tsx +188 -0
- package/src/features/projects/project-logs.tsx +461 -0
- package/src/features/projects/project-overview.tsx +280 -0
- package/src/features/projects/project-route-pending.tsx +1064 -0
- package/src/features/projects/project-routing-settings.tsx +663 -0
- package/src/features/projects/project-settings-section.tsx +64 -0
- package/src/features/projects/project-settings-utils.ts +101 -0
- package/src/features/projects/project-settings.tsx +1472 -0
- package/src/features/projects/repository-picker-dialog.tsx +404 -0
- package/src/features/repositories/new-project-dialog-launcher.tsx +44 -0
- package/src/features/repositories/new-project-dialog.tsx +1204 -0
- package/src/features/repositories/new-project-pending.tsx +28 -0
- package/src/features/repositories/repository-list-pending.tsx +59 -0
- package/src/features/servers/server-availability-alert.tsx +44 -0
- package/src/features/servers/server-availability.ts +29 -0
- package/src/features/servers/server-avatar.tsx +52 -0
- package/src/features/servers/server-detail-pending.tsx +315 -0
- package/src/features/servers/server-detail.tsx +855 -0
- package/src/features/servers/server-metric-chart-pending.tsx +38 -0
- package/src/features/servers/server-metric-chart.tsx +182 -0
- package/src/features/servers/server-metric-format.ts +50 -0
- package/src/features/servers/server-metrics-pending.tsx +75 -0
- package/src/features/servers/server-metrics.tsx +136 -0
- package/src/features/servers/team-servers.tsx +432 -0
- package/src/features/settings/settings-route-pending.tsx +406 -0
- package/src/features/teams/backup-storage-settings.tsx +483 -0
- package/src/features/teams/navigation.ts +51 -0
- package/src/features/teams/new-team-dialog.tsx +132 -0
- package/src/features/teams/pending-section.ts +19 -0
- package/src/features/teams/team-header.tsx +1286 -0
- package/src/features/teams/team-heading.tsx +57 -0
- package/src/features/teams/team-home.tsx +391 -0
- package/src/features/teams/team-invite.tsx +155 -0
- package/src/features/teams/team-route-pending.tsx +505 -0
- package/src/features/teams/team-settings.tsx +1555 -0
- package/src/features/teams/use-workspace-updates.ts +126 -0
- package/src/features/users/api-keys-settings.tsx +544 -0
- package/src/features/users/api-scope-selection.tsx +269 -0
- package/src/features/users/connected-apps-settings.tsx +449 -0
- package/src/features/users/mcp-consent.tsx +220 -0
- package/src/features/users/profile-picture.tsx +171 -0
- package/src/features/users/user-settings.tsx +1064 -0
- package/src/lib/account-settings-schema.ts +78 -0
- package/src/lib/action-feedback.ts +81 -0
- package/src/lib/admin-search-schema.ts +8 -0
- package/src/lib/agent-release.d.mts +21 -0
- package/src/lib/agent-release.mjs +90 -0
- package/src/lib/allowlist-schema.ts +79 -0
- package/src/lib/api-key-schema.ts +176 -0
- package/src/lib/auth/client.ts +20 -0
- package/src/lib/avatar-schema.ts +11 -0
- package/src/lib/backup-cron.ts +118 -0
- package/src/lib/backup-database.ts +41 -0
- package/src/lib/backup-feature.ts +2 -0
- package/src/lib/backup-paths.ts +7 -0
- package/src/lib/backup-policy-schema.ts +85 -0
- package/src/lib/backup-resource-schema.ts +12 -0
- package/src/lib/backup-restore-schema.ts +15 -0
- package/src/lib/backup-storage-schema.ts +55 -0
- package/src/lib/deferred-module.ts +38 -0
- package/src/lib/deployment-logs-query.ts +63 -0
- package/src/lib/display-name.ts +7 -0
- package/src/lib/error-message.ts +119 -0
- package/src/lib/form-errors.ts +10 -0
- package/src/lib/internal-redirect.ts +25 -0
- package/src/lib/manual-deploy-options-query.ts +21 -0
- package/src/lib/notification-preferences.ts +43 -0
- package/src/lib/onboarding.ts +11 -0
- package/src/lib/operation-logs-query.ts +51 -0
- package/src/lib/page-head.ts +14 -0
- package/src/lib/project-context-query.ts +257 -0
- package/src/lib/project-creation-schema.ts +64 -0
- package/src/lib/project-env-schema.ts +23 -0
- package/src/lib/project-environments-schema.ts +56 -0
- package/src/lib/project-routing-schema.ts +94 -0
- package/src/lib/project-runtime-config.ts +131 -0
- package/src/lib/project-settings-schema.ts +75 -0
- package/src/lib/public-api-schema.ts +128 -0
- package/src/lib/public-error.ts +337 -0
- package/src/lib/query.ts +13 -0
- package/src/lib/repository-selection-query.ts +11 -0
- package/src/lib/resource-cache.ts +30 -0
- package/src/lib/runtime-id.ts +19 -0
- package/src/lib/search.ts +14 -0
- package/src/lib/server-context-query.ts +43 -0
- package/src/lib/server-name-schema.ts +11 -0
- package/src/lib/settings-conflict.ts +45 -0
- package/src/lib/settings-version.ts +7 -0
- package/src/lib/team-context-query.ts +126 -0
- package/src/lib/team-context-schema.ts +8 -0
- package/src/lib/team-forms-schema.ts +45 -0
- package/src/lib/team-role.ts +13 -0
- package/src/lib/team-settings-schema.ts +48 -0
- package/src/lib/urls.ts +3 -0
- package/src/lib/use-avatar-input.ts +61 -0
- package/src/lib/use-refresh-error.ts +18 -0
- package/src/lib/use-settings-version.ts +23 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validation.ts +5 -0
- package/src/lib/workspace-updates.ts +53 -0
- package/src/package/config.ts +10 -0
- package/src/package/env.mjs +54 -0
- package/src/package/migrate-api.d.ts +5 -0
- package/src/package/migrate.ts +56 -0
- package/src/package/plugins.ts +141 -0
- package/src/package/sqlite.mjs +2 -0
- package/src/package/vite-api.d.ts +5 -0
- package/src/package/vite.ts +324 -0
- package/src/package/watch-guard.cjs +26 -0
- package/src/plugins/client.ts +57 -0
- package/src/plugins/contract.ts +188 -0
- package/src/plugins/default.server.ts +4 -0
- package/src/plugins/default.ui.ts +4 -0
- package/src/plugins/operations.ts +45 -0
- package/src/plugins/server.ts +197 -0
- package/src/plugins/version.ts +1 -0
- package/src/routeTree.gen.ts +1275 -0
- package/src/router.tsx +46 -0
- package/src/routes/$teamSlug.index.tsx +53 -0
- package/src/routes/$teamSlug.new-project.tsx +13 -0
- package/src/routes/$teamSlug.projects.$projectName.backups.$backupId.tsx +40 -0
- package/src/routes/$teamSlug.projects.$projectName.backups.tsx +82 -0
- package/src/routes/$teamSlug.projects.$projectName.deployments.$deploymentId.tsx +52 -0
- package/src/routes/$teamSlug.projects.$projectName.deployments.tsx +86 -0
- package/src/routes/$teamSlug.projects.$projectName.logs.tsx +55 -0
- package/src/routes/$teamSlug.projects.$projectName.settings.tsx +51 -0
- package/src/routes/$teamSlug.projects.$projectName.tsx +106 -0
- package/src/routes/$teamSlug.projects.tsx +40 -0
- package/src/routes/$teamSlug.servers.$serverId.tsx +31 -0
- package/src/routes/$teamSlug.servers.index.tsx +59 -0
- package/src/routes/$teamSlug.servers.tsx +7 -0
- package/src/routes/$teamSlug.settings.tsx +56 -0
- package/src/routes/$teamSlug.tsx +86 -0
- package/src/routes/[.]well-known.oauth-authorization-server.api.auth.ts +13 -0
- package/src/routes/[.]well-known.oauth-protected-resource.mcp.ts +6 -0
- package/src/routes/__root.tsx +155 -0
- package/src/routes/admin.tsx +55 -0
- package/src/routes/api/agents/certificates.ts +21 -0
- package/src/routes/api/agents/credentials/rotate.ts +22 -0
- package/src/routes/api/agents/heartbeat.ts +42 -0
- package/src/routes/api/agents/job-logs.ts +22 -0
- package/src/routes/api/agents/jobs/complete.ts +22 -0
- package/src/routes/api/agents/jobs/next.ts +29 -0
- package/src/routes/api/agents/jobs/progress.ts +32 -0
- package/src/routes/api/agents/jobs/status.ts +22 -0
- package/src/routes/api/agents/register.ts +45 -0
- package/src/routes/api/agents/runtime-logs.ts +22 -0
- package/src/routes/api/agents/switch.ts +25 -0
- package/src/routes/api/auth/$.ts +38 -0
- package/src/routes/api/cron/backups.ts +33 -0
- package/src/routes/api/cron/maintenance.ts +30 -0
- package/src/routes/api/github/install.callback.ts +51 -0
- package/src/routes/api/github/install.ts +41 -0
- package/src/routes/api/github/webhook.ts +11 -0
- package/src/routes/api/health.ts +19 -0
- package/src/routes/api/install/agent.$token.ts +50 -0
- package/src/routes/api/install/agent.binary.ts +83 -0
- package/src/routes/api/install/agent.checksum.ts +45 -0
- package/src/routes/api/install/agent.ts +26 -0
- package/src/routes/api/plugins/$.ts +11 -0
- package/src/routes/api/v1/$.ts +15 -0
- package/src/routes/api/workspaces/updates.ts +8 -0
- package/src/routes/auth.login.tsx +50 -0
- package/src/routes/deployments.$deploymentId.unavailable.tsx +21 -0
- package/src/routes/index.tsx +23 -0
- package/src/routes/invites.$token.tsx +35 -0
- package/src/routes/mcp.ts +13 -0
- package/src/routes/oauth.consent.tsx +54 -0
- package/src/routes/onboarding.tsx +37 -0
- package/src/routes/onboarding.workspace.tsx +44 -0
- package/src/routes/settings.tsx +30 -0
- package/src/routes/uploads/avatars/$fileName.ts +44 -0
- package/src/server/admin/access.ts +67 -0
- package/src/server/admin/functions.ts +122 -0
- package/src/server/admin/service.ts +260 -0
- package/src/server/agents/backup-state.ts +210 -0
- package/src/server/agents/certificates.ts +96 -0
- package/src/server/agents/claims.ts +116 -0
- package/src/server/agents/cleanup.ts +52 -0
- package/src/server/agents/compatibility.ts +116 -0
- package/src/server/agents/completion.ts +83 -0
- package/src/server/agents/container-cleanup-state.ts +51 -0
- package/src/server/agents/deployment-state.ts +111 -0
- package/src/server/agents/job-payloads.ts +159 -0
- package/src/server/agents/jobs.ts +1293 -0
- package/src/server/agents/protocol.ts +400 -0
- package/src/server/agents/reconciliation.ts +43 -0
- package/src/server/agents/runtime-logs.ts +337 -0
- package/src/server/agents/runtime-state.ts +125 -0
- package/src/server/api/admission.ts +12 -0
- package/src/server/api/authorization.ts +115 -0
- package/src/server/api/credentials.ts +569 -0
- package/src/server/api/cursors.ts +72 -0
- package/src/server/api/errors.ts +24 -0
- package/src/server/api/functions.ts +60 -0
- package/src/server/api/handler.ts +274 -0
- package/src/server/api/mutation-operations.ts +913 -0
- package/src/server/api/operation-context.ts +139 -0
- package/src/server/api/operation-contract.ts +1099 -0
- package/src/server/api/operations.ts +64 -0
- package/src/server/api/read-operations.ts +891 -0
- package/src/server/api/retention.ts +33 -0
- package/src/server/auth/access.ts +109 -0
- package/src/server/auth/auth-methods.ts +28 -0
- package/src/server/auth/better-auth.ts +518 -0
- package/src/server/auth/context.ts +28 -0
- package/src/server/auth/email-change.ts +47 -0
- package/src/server/auth/email-delivery.ts +51 -0
- package/src/server/auth/provider-identity.ts +19 -0
- package/src/server/auth/provider-tokens.ts +27 -0
- package/src/server/auth/session.server.ts +19 -0
- package/src/server/backups/database-commands.ts +40 -0
- package/src/server/backups/destination.ts +12 -0
- package/src/server/backups/feature.ts +6 -0
- package/src/server/backups/policy-validation.ts +51 -0
- package/src/server/backups/queue-state.ts +137 -0
- package/src/server/backups/service.ts +918 -0
- package/src/server/backups/storage-functions.ts +42 -0
- package/src/server/backups/storage-state.ts +67 -0
- package/src/server/backups/storage.ts +237 -0
- package/src/server/config.ts +274 -0
- package/src/server/db/atomic.ts +16 -0
- package/src/server/db/d1-connection.ts +83 -0
- package/src/server/db/errors.ts +17 -0
- package/src/server/db/index.cloudflare.ts +15 -0
- package/src/server/db/index.ts +35 -0
- package/src/server/db/libsql-connection.ts +72 -0
- package/src/server/db/log-schema.ts +102 -0
- package/src/server/db/migrate-libsql.ts +29 -0
- package/src/server/db/migrate-sqlite.ts +47 -0
- package/src/server/db/migration-history.ts +12 -0
- package/src/server/db/migrations-logs/0000_initial.sql +48 -0
- package/src/server/db/migrations-logs/0001_runtime_log_order.sql +2 -0
- package/src/server/db/migrations-logs/meta/0000_snapshot.json +327 -0
- package/src/server/db/migrations-logs/meta/0001_snapshot.json +329 -0
- package/src/server/db/migrations-logs/meta/_journal.json +20 -0
- package/src/server/db/migrations-sqlite/0000_initial.sql +894 -0
- package/src/server/db/migrations-sqlite/0001_agent_indexes.sql +5 -0
- package/src/server/db/migrations-sqlite/meta/0000_snapshot.json +5816 -0
- package/src/server/db/migrations-sqlite/meta/0001_snapshot.json +5841 -0
- package/src/server/db/migrations-sqlite/meta/_journal.json +20 -0
- package/src/server/db/oauth-schema.ts +216 -0
- package/src/server/db/remote-config.ts +47 -0
- package/src/server/db/schema.ts +1218 -0
- package/src/server/db/sqlite-connection.ts +94 -0
- package/src/server/db/sqlite-files.ts +27 -0
- package/src/server/db/sqlite-params.ts +19 -0
- package/src/server/db/worker-connection.ts +47 -0
- package/src/server/deleted-resources.ts +133 -0
- package/src/server/deletion-recovery.ts +272 -0
- package/src/server/email/layout.tsx +85 -0
- package/src/server/email/notification-template.tsx +62 -0
- package/src/server/email/otp-template.tsx +48 -0
- package/src/server/email/service.ts +251 -0
- package/src/server/email/team-invite-template.tsx +59 -0
- package/src/server/email/theme.ts +10 -0
- package/src/server/github/http.ts +14 -0
- package/src/server/github/install-state.ts +48 -0
- package/src/server/github/service.ts +831 -0
- package/src/server/github/webhook-handler.ts +198 -0
- package/src/server/github/webhooks.ts +205 -0
- package/src/server/http/auth.ts +11 -0
- package/src/server/http/error-diagnostics.ts +66 -0
- package/src/server/http/plugin-request.ts +15 -0
- package/src/server/http/proxy-request.ts +53 -0
- package/src/server/http/public-errors.ts +48 -0
- package/src/server/http/rate-limit.ts +59 -0
- package/src/server/http/request-body.ts +54 -0
- package/src/server/http/request-ip.ts +54 -0
- package/src/server/http/security-headers.ts +36 -0
- package/src/server/install/agent-binary.ts +230 -0
- package/src/server/install/agent-script.ts +565 -0
- package/src/server/install/release-binary.ts +65 -0
- package/src/server/logs/budget.ts +72 -0
- package/src/server/logs/cleanup.ts +64 -0
- package/src/server/logs/deployments.ts +98 -0
- package/src/server/logs/operations.ts +145 -0
- package/src/server/logs/retention.ts +39 -0
- package/src/server/maintenance.ts +61 -0
- package/src/server/mcp/connections.ts +358 -0
- package/src/server/mcp/functions.ts +63 -0
- package/src/server/mcp/handler.ts +295 -0
- package/src/server/mcp/oauth-discovery.cloudflare.ts +6 -0
- package/src/server/mcp/oauth-discovery.ts +8 -0
- package/src/server/mcp/oauth-provider.ts +220 -0
- package/src/server/mcp/urls.ts +20 -0
- package/src/server/notifications/functions.ts +51 -0
- package/src/server/notifications/observe.ts +303 -0
- package/src/server/notifications/outbox.ts +335 -0
- package/src/server/notifications/preferences.ts +131 -0
- package/src/server/notifications/query.ts +16 -0
- package/src/server/plugins/scope.ts +17 -0
- package/src/server/plugins/server-context.ts +157 -0
- package/src/server/plugins/services.server.ts +10 -0
- package/src/server/plugins/workspace-access.ts +74 -0
- package/src/server/projects/backup-policy-state.ts +68 -0
- package/src/server/projects/backups.ts +219 -0
- package/src/server/projects/compose-metadata.ts +174 -0
- package/src/server/projects/configuration-state.ts +90 -0
- package/src/server/projects/context.ts +909 -0
- package/src/server/projects/creation-state.ts +46 -0
- package/src/server/projects/deletion-state.ts +69 -0
- package/src/server/projects/deployment-config.ts +139 -0
- package/src/server/projects/deployment-logs.ts +97 -0
- package/src/server/projects/deployments.ts +1123 -0
- package/src/server/projects/domain-state.ts +71 -0
- package/src/server/projects/environment-branches.ts +75 -0
- package/src/server/projects/environment-state.ts +158 -0
- package/src/server/projects/environments.ts +424 -0
- package/src/server/projects/functions.ts +748 -0
- package/src/server/projects/generated-hostname.ts +77 -0
- package/src/server/projects/hostnames.ts +244 -0
- package/src/server/projects/mappers.ts +457 -0
- package/src/server/projects/names.ts +41 -0
- package/src/server/projects/project-env-vars.ts +28 -0
- package/src/server/projects/queue-state.ts +118 -0
- package/src/server/projects/repository.ts +368 -0
- package/src/server/projects/route-state.ts +37 -0
- package/src/server/projects/runtime-logs.ts +97 -0
- package/src/server/projects/settings-context.ts +58 -0
- package/src/server/projects/settings.ts +738 -0
- package/src/server/projects/types.ts +355 -0
- package/src/server/realtime/agent-events.ts +32 -0
- package/src/server/realtime/node-broadcaster.ts +71 -0
- package/src/server/realtime/node-handler.ts +55 -0
- package/src/server/realtime/service.ts +125 -0
- package/src/server/realtime/workspace-object.cloudflare.ts +62 -0
- package/src/server/realtime/workspace-object.ts +3 -0
- package/src/server/resources/color.ts +13 -0
- package/src/server/runtime/background.ts +21 -0
- package/src/server/runtime/node-asset-cache-plugin.ts +12 -0
- package/src/server/runtime/node-scheduler-plugin.ts +17 -0
- package/src/server/runtime/node-scheduler.ts +51 -0
- package/src/server/runtime/platform-env.cloudflare.ts +17 -0
- package/src/server/runtime/platform-env.ts +19 -0
- package/src/server/runtime/scheduled-task-error.ts +9 -0
- package/src/server/scheduled-tasks.ts +28 -0
- package/src/server/secrets/crypto.ts +56 -0
- package/src/server/servers/functions.ts +233 -0
- package/src/server/servers/lifecycle.ts +122 -0
- package/src/server/servers/metric-history-schema.ts +24 -0
- package/src/server/servers/metric-history.ts +55 -0
- package/src/server/servers/monitoring.ts +592 -0
- package/src/server/servers/service.ts +1211 -0
- package/src/server/servers/status.ts +32 -0
- package/src/server/servers/switch.ts +96 -0
- package/src/server/settings-version.ts +43 -0
- package/src/server/storage/capabilities.ts +6 -0
- package/src/server/storage/local-object-store.cloudflare.ts +7 -0
- package/src/server/storage/local-object-store.ts +50 -0
- package/src/server/storage/object-store.ts +160 -0
- package/src/server/teams/creation.ts +51 -0
- package/src/server/teams/delete-policy.ts +13 -0
- package/src/server/teams/deletion-state.ts +80 -0
- package/src/server/teams/functions.ts +324 -0
- package/src/server/teams/invitations.ts +38 -0
- package/src/server/teams/membership.ts +86 -0
- package/src/server/teams/queries.ts +261 -0
- package/src/server/teams/service.ts +1127 -0
- package/src/server/teams/types.ts +101 -0
- package/src/server/uploads/avatar.ts +133 -0
- package/src/server/users/deletion-state.ts +93 -0
- package/src/server/users/functions.ts +160 -0
- package/src/server/users/onboarding.ts +17 -0
- package/src/server/users/service.ts +644 -0
- package/src/server.ts +55 -0
- package/src/styles/shadcn.css +653 -0
- package/src/styles/theme.css +76 -0
- package/src/styles.css +177 -0
- package/wrangler.template.jsonc +37 -0
|
@@ -0,0 +1,1293 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withWorkspaceAccess,
|
|
3
|
+
isWorkspacePolicyRefusal,
|
|
4
|
+
} from '#devpush/server/plugins/workspace-access'
|
|
5
|
+
import type { BackupDestination } from '#devpush/server/backups/service'
|
|
6
|
+
import { backupDestinationSnapshot } from '#devpush/server/backups/destination'
|
|
7
|
+
import { refreshBackupCredentials } from '#devpush/server/backups/storage'
|
|
8
|
+
import { getPublicErrorMessage } from '#devpush/lib/public-error'
|
|
9
|
+
import { config as serverConfig } from '#devpush/server/config'
|
|
10
|
+
import {
|
|
11
|
+
recoverStaleCleanupJobs,
|
|
12
|
+
finishContainerCleanup,
|
|
13
|
+
} from './container-cleanup-state'
|
|
14
|
+
import {
|
|
15
|
+
failStaleAgentJob,
|
|
16
|
+
failClaimedAgentJob,
|
|
17
|
+
finishBackup,
|
|
18
|
+
finishBackupCleanup,
|
|
19
|
+
} from './backup-state'
|
|
20
|
+
import { claimBackupCandidate, claimCleanup, claimDeployment } from './claims'
|
|
21
|
+
import { failRunningDeployments, finishDeployment } from './deployment-state'
|
|
22
|
+
import { isAbsolute, relative } from 'node:path/posix'
|
|
23
|
+
|
|
24
|
+
import { and, asc, eq, inArray, lt, lte, or, isNull, sql } from 'drizzle-orm'
|
|
25
|
+
|
|
26
|
+
import { db } from '#devpush/server/db'
|
|
27
|
+
import { notifyServer } from '#devpush/server/realtime/service'
|
|
28
|
+
import {
|
|
29
|
+
appendDeploymentLogs,
|
|
30
|
+
nextDeploymentLogSeq,
|
|
31
|
+
} from '#devpush/server/logs/deployments'
|
|
32
|
+
import {
|
|
33
|
+
agentJobs,
|
|
34
|
+
aliases,
|
|
35
|
+
backupRuns,
|
|
36
|
+
deployments,
|
|
37
|
+
environments,
|
|
38
|
+
projects,
|
|
39
|
+
servers,
|
|
40
|
+
} from '#devpush/server/db/schema'
|
|
41
|
+
import { createInstallationAccessToken } from '#devpush/server/github/service'
|
|
42
|
+
import { queueBackupRetentionCleanupJobs } from '#devpush/server/backups/service'
|
|
43
|
+
import {
|
|
44
|
+
decryptDeploymentEnvVars,
|
|
45
|
+
parseDeploymentConfig,
|
|
46
|
+
toDeploymentCommitMeta,
|
|
47
|
+
} from '#devpush/server/projects/deployment-config'
|
|
48
|
+
import type { DeploymentConfig } from '#devpush/server/projects/deployment-config'
|
|
49
|
+
import { requireAgentServer } from '#devpush/server/servers/service'
|
|
50
|
+
import { AgentRequestError } from './protocol'
|
|
51
|
+
import { decryptSecret } from '#devpush/server/secrets/crypto'
|
|
52
|
+
import {
|
|
53
|
+
currentAgentProtocolVersion,
|
|
54
|
+
hasAgentCapability,
|
|
55
|
+
} from './compatibility'
|
|
56
|
+
import {
|
|
57
|
+
parseBackupCleanupPayload,
|
|
58
|
+
parseBackupPayload,
|
|
59
|
+
parseDeprovisionPayload,
|
|
60
|
+
parseRestorePayload,
|
|
61
|
+
} from './job-payloads'
|
|
62
|
+
import type {
|
|
63
|
+
AgentBackupCleanupCompletion,
|
|
64
|
+
AgentBackupCleanupJob,
|
|
65
|
+
AgentBackupCompletion,
|
|
66
|
+
AgentBackupJob,
|
|
67
|
+
AgentCleanupCompletion,
|
|
68
|
+
AgentCleanupJob,
|
|
69
|
+
AgentDeployJob,
|
|
70
|
+
AgentJob,
|
|
71
|
+
AgentJobCompletion,
|
|
72
|
+
AgentJobProgress,
|
|
73
|
+
AgentJobStatus,
|
|
74
|
+
AgentRestoreCompletion,
|
|
75
|
+
AgentRestoreJob,
|
|
76
|
+
AgentEnvVar,
|
|
77
|
+
} from './protocol'
|
|
78
|
+
|
|
79
|
+
type InstallationAccessTokenFactory = typeof createInstallationAccessToken
|
|
80
|
+
|
|
81
|
+
export async function claimNextAgentJob({
|
|
82
|
+
serverId,
|
|
83
|
+
token,
|
|
84
|
+
protocolVersion,
|
|
85
|
+
capabilities,
|
|
86
|
+
createAccessToken = createInstallationAccessToken,
|
|
87
|
+
}: {
|
|
88
|
+
serverId: string
|
|
89
|
+
token: string
|
|
90
|
+
protocolVersion?: number
|
|
91
|
+
capabilities?: Array<string>
|
|
92
|
+
createAccessToken?: InstallationAccessTokenFactory
|
|
93
|
+
}): Promise<AgentJob | null> {
|
|
94
|
+
const server = await requireAgentServer({ serverId, token })
|
|
95
|
+
if (server.status !== 'deleted')
|
|
96
|
+
await recoverStaleCleanupJobs(db, { serverId, now: new Date() })
|
|
97
|
+
await Promise.all([
|
|
98
|
+
failStaleRunningDeployments(serverId),
|
|
99
|
+
failStaleAgentJobs(serverId),
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
if (protocolVersion !== currentAgentProtocolVersion) {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const cleanupJob = hasAgentCapability(capabilities, 'jobs.compose.cleanup.v2')
|
|
107
|
+
? await claimNextCleanupJob({ serverId })
|
|
108
|
+
: null
|
|
109
|
+
|
|
110
|
+
if (cleanupJob) {
|
|
111
|
+
return cleanupJob
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (server.status === 'deleted') {
|
|
115
|
+
return hasAgentCapability(capabilities, 'jobs.compose.deploy.v1')
|
|
116
|
+
? claimNextDeploymentJob({
|
|
117
|
+
serverId,
|
|
118
|
+
token,
|
|
119
|
+
createAccessToken,
|
|
120
|
+
skipAgentAuth: true,
|
|
121
|
+
})
|
|
122
|
+
: null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const backupTypes = [
|
|
126
|
+
...(hasAgentCapability(capabilities, 'jobs.backup.v1')
|
|
127
|
+
? (['backup'] as const)
|
|
128
|
+
: []),
|
|
129
|
+
...(hasAgentCapability(capabilities, 'jobs.backup.cleanup.v1')
|
|
130
|
+
? (['backup_cleanup'] as const)
|
|
131
|
+
: []),
|
|
132
|
+
...(hasAgentCapability(capabilities, 'jobs.restore.v1')
|
|
133
|
+
? (['restore'] as const)
|
|
134
|
+
: []),
|
|
135
|
+
]
|
|
136
|
+
const backupJob =
|
|
137
|
+
backupTypes.length > 0
|
|
138
|
+
? await claimNextBackupJob({
|
|
139
|
+
serverId,
|
|
140
|
+
types: backupTypes,
|
|
141
|
+
})
|
|
142
|
+
: null
|
|
143
|
+
|
|
144
|
+
if (backupJob) {
|
|
145
|
+
return backupJob
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return hasAgentCapability(capabilities, 'jobs.compose.deploy.v1')
|
|
149
|
+
? claimNextDeploymentJob({
|
|
150
|
+
serverId,
|
|
151
|
+
token,
|
|
152
|
+
createAccessToken,
|
|
153
|
+
skipAgentAuth: true,
|
|
154
|
+
})
|
|
155
|
+
: null
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function claimNextBackupJob({
|
|
159
|
+
serverId,
|
|
160
|
+
types,
|
|
161
|
+
}: {
|
|
162
|
+
serverId: string
|
|
163
|
+
types: Array<'backup' | 'restore' | 'backup_cleanup'>
|
|
164
|
+
}): Promise<AgentBackupJob | AgentBackupCleanupJob | AgentRestoreJob | null> {
|
|
165
|
+
const [owner] = await db
|
|
166
|
+
.select({ teamId: servers.teamId })
|
|
167
|
+
.from(servers)
|
|
168
|
+
.where(eq(servers.id, serverId))
|
|
169
|
+
.limit(1)
|
|
170
|
+
if (!owner) return null
|
|
171
|
+
const teamId = owner.teamId
|
|
172
|
+
let claimed: typeof agentJobs.$inferSelect | null = null
|
|
173
|
+
// A lost compare-and-set means another request consumed this candidate.
|
|
174
|
+
// Read the next one immediately instead of delaying it until another poll.
|
|
175
|
+
for (let attempt = 0; !claimed && attempt < 8; attempt++) {
|
|
176
|
+
const now = new Date()
|
|
177
|
+
const [snapshot] = await db
|
|
178
|
+
.select({
|
|
179
|
+
job: agentJobs,
|
|
180
|
+
payloadSnapshot: sql<string>`${agentJobs.payload}`,
|
|
181
|
+
})
|
|
182
|
+
.from(agentJobs)
|
|
183
|
+
.where(
|
|
184
|
+
and(
|
|
185
|
+
eq(agentJobs.serverId, serverId),
|
|
186
|
+
inArray(agentJobs.type, types),
|
|
187
|
+
eq(agentJobs.status, 'queued'),
|
|
188
|
+
lte(agentJobs.availableAt, now),
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
.orderBy(asc(agentJobs.createdAt), asc(agentJobs.id))
|
|
192
|
+
.limit(1)
|
|
193
|
+
if (!snapshot) return null
|
|
194
|
+
const candidate = snapshot.job
|
|
195
|
+
const payload =
|
|
196
|
+
candidate.type === 'backup' ? parseBackupPayload(candidate.payload) : null
|
|
197
|
+
try {
|
|
198
|
+
const claim = () =>
|
|
199
|
+
claimBackupCandidate(db, {
|
|
200
|
+
job: candidate,
|
|
201
|
+
payloadSnapshot: snapshot.payloadSnapshot,
|
|
202
|
+
backupRunId: payload?.backupRunId ?? null,
|
|
203
|
+
now,
|
|
204
|
+
})
|
|
205
|
+
claimed =
|
|
206
|
+
candidate.type === 'backup_cleanup'
|
|
207
|
+
? await claim()
|
|
208
|
+
: await withWorkspaceAccess(
|
|
209
|
+
teamId,
|
|
210
|
+
candidate.type === 'restore' ? 'restore.claim' : 'backup.claim',
|
|
211
|
+
claim,
|
|
212
|
+
{ kind: 'system', source: 'agent' },
|
|
213
|
+
)
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (!isWorkspacePolicyRefusal(error)) throw error
|
|
216
|
+
// Skip productive work but continue looking for retention cleanup.
|
|
217
|
+
return types.includes('backup_cleanup')
|
|
218
|
+
? claimNextBackupJob({ serverId, types: ['backup_cleanup'] })
|
|
219
|
+
: null
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!claimed) return null
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
if (claimed.type === 'backup') {
|
|
227
|
+
const payload = parseBackupPayload(claimed.payload)
|
|
228
|
+
|
|
229
|
+
if (!payload) {
|
|
230
|
+
await failCleanupJob(claimed.id, 'Backup job payload is invalid.')
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
type: 'backup',
|
|
236
|
+
id: claimed.id,
|
|
237
|
+
backup: await hydrateBackupPayload(payload, teamId),
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (claimed.type === 'backup_cleanup') {
|
|
242
|
+
const payload = parseBackupCleanupPayload(claimed.payload)
|
|
243
|
+
|
|
244
|
+
if (!payload) {
|
|
245
|
+
await failCleanupJob(
|
|
246
|
+
claimed.id,
|
|
247
|
+
'Backup cleanup job payload is invalid.',
|
|
248
|
+
)
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
type: 'backup_cleanup',
|
|
254
|
+
id: claimed.id,
|
|
255
|
+
cleanup: await hydrateBackupCleanupPayload(payload, teamId),
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (claimed.type === 'restore') {
|
|
260
|
+
const payload = parseRestorePayload(claimed.payload)
|
|
261
|
+
|
|
262
|
+
if (!payload) {
|
|
263
|
+
await failCleanupJob(claimed.id, 'Restore job payload is invalid.')
|
|
264
|
+
return null
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
type: 'restore',
|
|
269
|
+
id: claimed.id,
|
|
270
|
+
restore: await hydrateRestorePayload(payload, teamId),
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return null
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const payload =
|
|
277
|
+
claimed.type === 'backup' ? parseBackupPayload(claimed.payload) : null
|
|
278
|
+
const cleanup =
|
|
279
|
+
claimed.type === 'backup_cleanup'
|
|
280
|
+
? parseBackupCleanupPayload(claimed.payload)
|
|
281
|
+
: null
|
|
282
|
+
await failClaimedAgentJob(db, {
|
|
283
|
+
job: claimed,
|
|
284
|
+
now: new Date(),
|
|
285
|
+
message: getPublicErrorMessage(
|
|
286
|
+
error,
|
|
287
|
+
'Backup storage credentials could not be loaded.',
|
|
288
|
+
),
|
|
289
|
+
backupRunId: payload?.backupRunId ?? null,
|
|
290
|
+
cleanupRunIds: cleanup?.backupRunIds ?? [],
|
|
291
|
+
})
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function hydrateBackupPayload(
|
|
297
|
+
payload: NonNullable<ReturnType<typeof parseBackupPayload>>,
|
|
298
|
+
teamId: string,
|
|
299
|
+
) {
|
|
300
|
+
return {
|
|
301
|
+
...payload,
|
|
302
|
+
destination: await hydrateBackupDestination(
|
|
303
|
+
teamId,
|
|
304
|
+
payload.destination,
|
|
305
|
+
payload.retention.remote.enabled,
|
|
306
|
+
),
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function hydrateBackupCleanupPayload(
|
|
310
|
+
payload: NonNullable<ReturnType<typeof parseBackupCleanupPayload>>,
|
|
311
|
+
teamId: string,
|
|
312
|
+
) {
|
|
313
|
+
return {
|
|
314
|
+
...payload,
|
|
315
|
+
...(payload.destination
|
|
316
|
+
? {
|
|
317
|
+
destination: await hydrateBackupDestination(
|
|
318
|
+
teamId,
|
|
319
|
+
payload.destination,
|
|
320
|
+
payload.remoteObjectKeys.length > 0,
|
|
321
|
+
),
|
|
322
|
+
}
|
|
323
|
+
: {}),
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function hydrateRestorePayload(
|
|
327
|
+
payload: NonNullable<ReturnType<typeof parseRestorePayload>>,
|
|
328
|
+
teamId: string,
|
|
329
|
+
) {
|
|
330
|
+
return {
|
|
331
|
+
...payload,
|
|
332
|
+
...(payload.destination
|
|
333
|
+
? {
|
|
334
|
+
destination: await hydrateBackupDestination(
|
|
335
|
+
teamId,
|
|
336
|
+
payload.destination,
|
|
337
|
+
payload.artifacts.some((artifact) => artifact.storage === 'remote'),
|
|
338
|
+
),
|
|
339
|
+
}
|
|
340
|
+
: {}),
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function hydrateBackupDestination(
|
|
344
|
+
teamId: string,
|
|
345
|
+
destination: BackupDestination,
|
|
346
|
+
remote: boolean,
|
|
347
|
+
) {
|
|
348
|
+
const snapshot = backupDestinationSnapshot(destination)
|
|
349
|
+
if (!remote) return snapshot
|
|
350
|
+
const current = await refreshBackupCredentials(teamId, snapshot)
|
|
351
|
+
if (!current.secretAccessKeyCiphertext)
|
|
352
|
+
throw new Error('Backup storage credentials missing.')
|
|
353
|
+
return {
|
|
354
|
+
...snapshot,
|
|
355
|
+
accessKeyId: current.accessKeyId,
|
|
356
|
+
secretAccessKey: decryptSecret(current.secretAccessKeyCiphertext),
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function claimNextDeploymentJob({
|
|
361
|
+
serverId,
|
|
362
|
+
token,
|
|
363
|
+
createAccessToken = createInstallationAccessToken,
|
|
364
|
+
skipAgentAuth = false,
|
|
365
|
+
}: {
|
|
366
|
+
serverId: string
|
|
367
|
+
token: string
|
|
368
|
+
createAccessToken?: InstallationAccessTokenFactory
|
|
369
|
+
skipAgentAuth?: boolean
|
|
370
|
+
}): Promise<AgentDeployJob | null> {
|
|
371
|
+
if (!skipAgentAuth) {
|
|
372
|
+
await requireAgentServer({ serverId, token })
|
|
373
|
+
await failStaleRunningDeployments(serverId)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
await failStaleQueuedDeployments(serverId)
|
|
377
|
+
const deployment = await claimDeployment(db, serverId, new Date())
|
|
378
|
+
if (!deployment) return null
|
|
379
|
+
const [identity] = await db
|
|
380
|
+
.select({ environment: environments, project: projects })
|
|
381
|
+
.from(environments)
|
|
382
|
+
.innerJoin(projects, eq(projects.id, environments.projectId))
|
|
383
|
+
.where(eq(environments.id, deployment.environmentId))
|
|
384
|
+
.limit(1)
|
|
385
|
+
const claimed = identity ? { deployment, ...identity } : null
|
|
386
|
+
|
|
387
|
+
if (!claimed) {
|
|
388
|
+
await failClaimedDeployment(
|
|
389
|
+
deployment.id,
|
|
390
|
+
'Deployment environment no longer exists.',
|
|
391
|
+
)
|
|
392
|
+
return null
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const deploymentConfig = parseDeploymentConfig(claimed.deployment.config)
|
|
396
|
+
|
|
397
|
+
if (!deploymentConfig) {
|
|
398
|
+
await failClaimedDeployment(
|
|
399
|
+
claimed.deployment.id,
|
|
400
|
+
'Deployment config is invalid.',
|
|
401
|
+
)
|
|
402
|
+
return null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!claimed.project.githubInstallationId || !claimed.deployment.repoId) {
|
|
406
|
+
await failClaimedDeployment(
|
|
407
|
+
claimed.deployment.id,
|
|
408
|
+
'Project is missing a GitHub App installation.',
|
|
409
|
+
)
|
|
410
|
+
return null
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let accessToken: { token: string }
|
|
414
|
+
try {
|
|
415
|
+
accessToken = await createAccessToken({
|
|
416
|
+
installationId: claimed.project.githubInstallationId,
|
|
417
|
+
repositoryId: claimed.deployment.repoId,
|
|
418
|
+
repositoryFullName: claimed.deployment.repoFullName,
|
|
419
|
+
})
|
|
420
|
+
} catch (error) {
|
|
421
|
+
await failClaimedDeployment(
|
|
422
|
+
claimed.deployment.id,
|
|
423
|
+
error instanceof Error
|
|
424
|
+
? error.message
|
|
425
|
+
: 'Unable to create GitHub installation token.',
|
|
426
|
+
)
|
|
427
|
+
return null
|
|
428
|
+
}
|
|
429
|
+
let envVars: Array<AgentEnvVar>
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
envVars = decryptDeploymentEnvVars(claimed.deployment.envVars)
|
|
433
|
+
} catch (error) {
|
|
434
|
+
await failClaimedDeployment(
|
|
435
|
+
claimed.deployment.id,
|
|
436
|
+
error instanceof Error
|
|
437
|
+
? error.message
|
|
438
|
+
: 'Unable to decrypt deployment environment variables.',
|
|
439
|
+
)
|
|
440
|
+
return null
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const runtimeEnvVars = buildAgentDeploymentEnvVars({
|
|
444
|
+
deployment: claimed.deployment,
|
|
445
|
+
environment: claimed.environment,
|
|
446
|
+
envVars,
|
|
447
|
+
project: claimed.project,
|
|
448
|
+
config: deploymentConfig,
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
const [deploymentAlias] = await db
|
|
452
|
+
.select({ hostname: aliases.hostname })
|
|
453
|
+
.from(aliases)
|
|
454
|
+
.where(
|
|
455
|
+
and(
|
|
456
|
+
eq(aliases.deploymentId, claimed.deployment.id),
|
|
457
|
+
eq(aliases.type, 'deployment'),
|
|
458
|
+
),
|
|
459
|
+
)
|
|
460
|
+
.limit(1)
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
fallbackHost: deploymentAlias?.hostname,
|
|
464
|
+
type: 'deploy',
|
|
465
|
+
id: claimed.deployment.id,
|
|
466
|
+
logStartSeq: await nextDeploymentLogSeq(claimed.deployment.id),
|
|
467
|
+
repo: {
|
|
468
|
+
fullName: claimed.deployment.repoFullName,
|
|
469
|
+
branch: claimed.deployment.gitBranch,
|
|
470
|
+
sha: claimed.deployment.gitSha,
|
|
471
|
+
token: accessToken.token,
|
|
472
|
+
},
|
|
473
|
+
project: {
|
|
474
|
+
name: claimed.project.name,
|
|
475
|
+
},
|
|
476
|
+
runtime: {
|
|
477
|
+
...deploymentConfig.runtime,
|
|
478
|
+
path: deploymentConfig.runtime.path ?? 'compose.yml',
|
|
479
|
+
projectName: claimed.deployment.runtimeId,
|
|
480
|
+
action: claimed.deployment.trigger === 'rollback' ? 'rollback' : 'deploy',
|
|
481
|
+
},
|
|
482
|
+
envVars: runtimeEnvVars,
|
|
483
|
+
routes: deploymentConfig.routes,
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function buildAgentDeploymentEnvVars({
|
|
488
|
+
deployment,
|
|
489
|
+
environment,
|
|
490
|
+
envVars,
|
|
491
|
+
project,
|
|
492
|
+
config,
|
|
493
|
+
}: {
|
|
494
|
+
deployment: {
|
|
495
|
+
id: string
|
|
496
|
+
createdAt: Date
|
|
497
|
+
gitBranch: string
|
|
498
|
+
gitSha: string
|
|
499
|
+
commitMeta: unknown
|
|
500
|
+
repoFullName: string
|
|
501
|
+
}
|
|
502
|
+
environment: {
|
|
503
|
+
id: string
|
|
504
|
+
slug: string
|
|
505
|
+
}
|
|
506
|
+
envVars: Array<AgentEnvVar>
|
|
507
|
+
project: {
|
|
508
|
+
id: string
|
|
509
|
+
teamId: string
|
|
510
|
+
}
|
|
511
|
+
config: DeploymentConfig
|
|
512
|
+
}) {
|
|
513
|
+
const next = [...envVars]
|
|
514
|
+
const rawCommitMeta =
|
|
515
|
+
typeof deployment.commitMeta === 'object' && deployment.commitMeta !== null
|
|
516
|
+
? (deployment.commitMeta as {
|
|
517
|
+
authorName?: string | null
|
|
518
|
+
authoredAt?: string | null
|
|
519
|
+
message?: string | null
|
|
520
|
+
url?: string | null
|
|
521
|
+
})
|
|
522
|
+
: {}
|
|
523
|
+
const commitMeta = toDeploymentCommitMeta({
|
|
524
|
+
authorName: rawCommitMeta.authorName,
|
|
525
|
+
authoredAt: rawCommitMeta.authoredAt,
|
|
526
|
+
message: rawCommitMeta.message ?? null,
|
|
527
|
+
url: rawCommitMeta.url,
|
|
528
|
+
})
|
|
529
|
+
const [repoOwner, repoName] = deployment.repoFullName.split('/', 2)
|
|
530
|
+
const defaultRoute = config.routes.at(0)
|
|
531
|
+
const runtimeVars: Record<string, string | null | undefined> = {
|
|
532
|
+
DEVPUSH: 'true',
|
|
533
|
+
DEVPUSH_URL: defaultRoute
|
|
534
|
+
? `${defaultRoute.https ? 'https' : 'http'}://${defaultRoute.host}`
|
|
535
|
+
: null,
|
|
536
|
+
DEVPUSH_DOMAIN: defaultRoute?.host,
|
|
537
|
+
DEVPUSH_TEAM_ID: project.teamId,
|
|
538
|
+
DEVPUSH_PROJECT_ID: project.id,
|
|
539
|
+
DEVPUSH_ENVIRONMENT: environment.slug || environment.id,
|
|
540
|
+
DEVPUSH_DEPLOYMENT_ID: deployment.id,
|
|
541
|
+
DEVPUSH_DEPLOYMENT_CREATED_AT: deployment.createdAt.toISOString(),
|
|
542
|
+
DEVPUSH_GIT_PROVIDER: 'github',
|
|
543
|
+
DEVPUSH_GIT_REPO: deployment.repoFullName,
|
|
544
|
+
DEVPUSH_GIT_REF: deployment.gitBranch,
|
|
545
|
+
DEVPUSH_GIT_COMMIT_SHA: deployment.gitSha,
|
|
546
|
+
DEVPUSH_GIT_COMMIT_AUTHOR: commitMeta.authorName,
|
|
547
|
+
DEVPUSH_GIT_COMMIT_MESSAGE: commitMeta.message,
|
|
548
|
+
DEVPUSH_GIT_REPO_OWNER: repoOwner,
|
|
549
|
+
DEVPUSH_GIT_REPO_NAME: repoName,
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
for (const [key, value] of Object.entries(runtimeVars)) {
|
|
553
|
+
if (value && !next.some((envVar) => envVar.key === key)) {
|
|
554
|
+
next.push({ key, value })
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return next
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function claimNextCleanupJob({
|
|
562
|
+
serverId,
|
|
563
|
+
}: {
|
|
564
|
+
serverId: string
|
|
565
|
+
}): Promise<AgentCleanupJob | null> {
|
|
566
|
+
const claimed = await claimCleanup(db, serverId, new Date())
|
|
567
|
+
|
|
568
|
+
if (!claimed) {
|
|
569
|
+
return null
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (claimed.type !== 'deprovision' && claimed.type !== 'purge') {
|
|
573
|
+
await failCleanupJob(
|
|
574
|
+
claimed.id,
|
|
575
|
+
`Unsupported agent job type "${claimed.type}".`,
|
|
576
|
+
)
|
|
577
|
+
return null
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const payload = parseDeprovisionPayload(claimed.payload)
|
|
581
|
+
|
|
582
|
+
if (!payload) {
|
|
583
|
+
await failCleanupJob(claimed.id, 'Cleanup job payload is invalid.')
|
|
584
|
+
return null
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// A previously orphaned runtime may have been assigned back before this job
|
|
588
|
+
// became available. Never execute an old purge against a currently owned runtime.
|
|
589
|
+
const protectedRows = await db
|
|
590
|
+
.select({ runtimeId: environments.runtimeId })
|
|
591
|
+
.from(environments)
|
|
592
|
+
.innerJoin(projects, eq(projects.id, environments.projectId))
|
|
593
|
+
.where(
|
|
594
|
+
and(
|
|
595
|
+
eq(environments.status, 'active'),
|
|
596
|
+
sql`coalesce(${environments.serverId}, ${projects.defaultServerId}) = ${serverId}`,
|
|
597
|
+
sql`${environments.runtimeId} in (select value from json_each(${JSON.stringify(payload.runtimeIds)}))`,
|
|
598
|
+
),
|
|
599
|
+
)
|
|
600
|
+
const activeRows = await db
|
|
601
|
+
.select({ runtimeId: deployments.runtimeId })
|
|
602
|
+
.from(deployments)
|
|
603
|
+
.where(
|
|
604
|
+
and(
|
|
605
|
+
eq(deployments.serverId, serverId),
|
|
606
|
+
inArray(deployments.status, ['queued', 'running']),
|
|
607
|
+
sql`${deployments.runtimeId} in (select value from json_each(${JSON.stringify(payload.runtimeIds)}))`,
|
|
608
|
+
),
|
|
609
|
+
)
|
|
610
|
+
const protectedIds = new Set(
|
|
611
|
+
[...protectedRows, ...activeRows].map((row) => row.runtimeId),
|
|
612
|
+
)
|
|
613
|
+
const runtimeIds = payload.runtimeIds.filter((id) => !protectedIds.has(id))
|
|
614
|
+
if (runtimeIds.length === 0) {
|
|
615
|
+
await finishContainerCleanup(db, {
|
|
616
|
+
job: claimed,
|
|
617
|
+
completion: {
|
|
618
|
+
jobId: claimed.id,
|
|
619
|
+
attempt: claimed.attempt,
|
|
620
|
+
status: 'succeeded',
|
|
621
|
+
},
|
|
622
|
+
now: new Date(),
|
|
623
|
+
})
|
|
624
|
+
return null
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (runtimeIds.length !== payload.runtimeIds.length) {
|
|
628
|
+
const trimmed = await db
|
|
629
|
+
.update(agentJobs)
|
|
630
|
+
.set({ payload: { ...payload, runtimeIds } })
|
|
631
|
+
.where(
|
|
632
|
+
and(
|
|
633
|
+
eq(agentJobs.id, claimed.id),
|
|
634
|
+
eq(agentJobs.status, 'running'),
|
|
635
|
+
eq(agentJobs.attempt, claimed.attempt),
|
|
636
|
+
sql`${agentJobs.claimedAt} is ${claimed.claimedAt?.getTime() ?? null}`,
|
|
637
|
+
),
|
|
638
|
+
)
|
|
639
|
+
.returning({ id: agentJobs.id })
|
|
640
|
+
if (trimmed.length === 0) return null
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
type: claimed.type,
|
|
645
|
+
id: claimed.id,
|
|
646
|
+
attempt: claimed.attempt,
|
|
647
|
+
cleanup: {
|
|
648
|
+
runtimeIds,
|
|
649
|
+
removeVolumes: payload.removeVolumes,
|
|
650
|
+
reason: payload.reason,
|
|
651
|
+
},
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const deploymentTimeoutMs = 3 * 60 * 60 * 1000
|
|
656
|
+
const lostAgentTimeoutMs = 10 * 60 * 1000
|
|
657
|
+
const backupJobTimeoutMs = 25 * 60 * 60 * 1000
|
|
658
|
+
|
|
659
|
+
function agentJobTimeoutMs(type: string) {
|
|
660
|
+
return type === 'backup' || type === 'backup_cleanup' || type === 'restore'
|
|
661
|
+
? backupJobTimeoutMs
|
|
662
|
+
: deploymentTimeoutMs
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export async function failStaleQueuedDeployments(
|
|
666
|
+
serverId?: string,
|
|
667
|
+
now = new Date(),
|
|
668
|
+
) {
|
|
669
|
+
const cutoff = new Date(
|
|
670
|
+
now.getTime() - serverConfig.DEVPUSH_DEPLOYMENT_QUEUE_TTL_HOURS * 60 * 60 * 1000,
|
|
671
|
+
)
|
|
672
|
+
const changed = await db
|
|
673
|
+
.update(deployments)
|
|
674
|
+
.set({
|
|
675
|
+
status: 'failed',
|
|
676
|
+
phase: 'timed_out',
|
|
677
|
+
errorMessage: 'Deployment expired while waiting for the server.',
|
|
678
|
+
finishedAt: now,
|
|
679
|
+
})
|
|
680
|
+
.where(
|
|
681
|
+
and(
|
|
682
|
+
eq(deployments.status, 'queued'),
|
|
683
|
+
lte(deployments.createdAt, cutoff),
|
|
684
|
+
serverId ? eq(deployments.serverId, serverId) : undefined,
|
|
685
|
+
),
|
|
686
|
+
)
|
|
687
|
+
.returning({ id: deployments.id, serverId: deployments.serverId })
|
|
688
|
+
await Promise.all(
|
|
689
|
+
[...new Set(changed.map((row) => row.serverId))].map((id) =>
|
|
690
|
+
notifyServer(id, 'deployments'),
|
|
691
|
+
),
|
|
692
|
+
)
|
|
693
|
+
return changed.map((row) => row.id)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export async function failStaleRunningDeployments(serverId?: string) {
|
|
697
|
+
const now = new Date()
|
|
698
|
+
const cutoff = new Date(now.getTime() - deploymentTimeoutMs)
|
|
699
|
+
const heartbeatCutoff = new Date(now.getTime() - lostAgentTimeoutMs)
|
|
700
|
+
const staleRows = await db
|
|
701
|
+
.select({
|
|
702
|
+
id: deployments.id,
|
|
703
|
+
serverId: deployments.serverId,
|
|
704
|
+
startedAt: deployments.startedAt,
|
|
705
|
+
})
|
|
706
|
+
.from(deployments)
|
|
707
|
+
.innerJoin(servers, eq(servers.id, deployments.serverId))
|
|
708
|
+
.where(
|
|
709
|
+
and(
|
|
710
|
+
serverId ? eq(deployments.serverId, serverId) : undefined,
|
|
711
|
+
eq(deployments.status, 'running'),
|
|
712
|
+
or(
|
|
713
|
+
lt(deployments.startedAt, cutoff),
|
|
714
|
+
and(
|
|
715
|
+
lt(deployments.startedAt, heartbeatCutoff),
|
|
716
|
+
or(
|
|
717
|
+
isNull(servers.lastSeenAt),
|
|
718
|
+
lt(servers.lastSeenAt, heartbeatCutoff),
|
|
719
|
+
),
|
|
720
|
+
),
|
|
721
|
+
),
|
|
722
|
+
),
|
|
723
|
+
)
|
|
724
|
+
const expired = staleRows.filter(
|
|
725
|
+
(row) => row.startedAt && row.startedAt < cutoff,
|
|
726
|
+
)
|
|
727
|
+
const disconnected = staleRows.filter((row) => !expired.includes(row))
|
|
728
|
+
const failed = [
|
|
729
|
+
...(await failRunningDeployments(db, {
|
|
730
|
+
ids: expired.map((row) => row.id),
|
|
731
|
+
message: 'Deployment exceeded the three-hour execution limit.',
|
|
732
|
+
phase: 'timed_out',
|
|
733
|
+
startedBefore: cutoff,
|
|
734
|
+
now,
|
|
735
|
+
})),
|
|
736
|
+
...(await failRunningDeployments(db, {
|
|
737
|
+
ids: disconnected.map((row) => row.id),
|
|
738
|
+
message:
|
|
739
|
+
'Deployment failed after ten minutes without an agent heartbeat.',
|
|
740
|
+
phase: 'timed_out',
|
|
741
|
+
startedBefore: heartbeatCutoff,
|
|
742
|
+
heartbeatBefore: heartbeatCutoff,
|
|
743
|
+
now,
|
|
744
|
+
})),
|
|
745
|
+
]
|
|
746
|
+
const failedIds = new Set(failed)
|
|
747
|
+
const affectedServers = new Set(
|
|
748
|
+
staleRows.filter((row) => failedIds.has(row.id)).map((row) => row.serverId),
|
|
749
|
+
)
|
|
750
|
+
await Promise.all(
|
|
751
|
+
[...affectedServers].map((id) => notifyServer(id, 'deployments')),
|
|
752
|
+
)
|
|
753
|
+
return failed
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export async function failStaleAgentJobs(serverId?: string) {
|
|
757
|
+
const now = new Date()
|
|
758
|
+
const cutoff = now.getTime() - backupJobTimeoutMs
|
|
759
|
+
const stale = await db
|
|
760
|
+
.select({
|
|
761
|
+
job: agentJobs,
|
|
762
|
+
payloadSnapshot: sql<string>`${agentJobs.payload}`,
|
|
763
|
+
})
|
|
764
|
+
.from(agentJobs)
|
|
765
|
+
.where(
|
|
766
|
+
and(
|
|
767
|
+
serverId ? eq(agentJobs.serverId, serverId) : undefined,
|
|
768
|
+
sql`${agentJobs.type} in ('backup', 'restore', 'backup_cleanup')`,
|
|
769
|
+
sql`((${agentJobs.status} = 'running' and (${agentJobs.claimedAt} is null or ${agentJobs.claimedAt} < ${cutoff}))
|
|
770
|
+
or (${agentJobs.status} = 'queued' and ${agentJobs.availableAt} < ${cutoff}))`,
|
|
771
|
+
),
|
|
772
|
+
)
|
|
773
|
+
.orderBy(asc(agentJobs.availableAt))
|
|
774
|
+
.limit(500)
|
|
775
|
+
const failed: Array<{ id: string }> = []
|
|
776
|
+
const affectedServers = new Set<string>()
|
|
777
|
+
for (const { job, payloadSnapshot } of stale) {
|
|
778
|
+
const backup =
|
|
779
|
+
job.type === 'backup' ? parseBackupPayload(job.payload) : null
|
|
780
|
+
const cleanup =
|
|
781
|
+
job.type === 'backup_cleanup'
|
|
782
|
+
? parseBackupCleanupPayload(job.payload)
|
|
783
|
+
: null
|
|
784
|
+
if (
|
|
785
|
+
await failStaleAgentJob(db, {
|
|
786
|
+
job,
|
|
787
|
+
payloadSnapshot,
|
|
788
|
+
now,
|
|
789
|
+
timeoutMs: agentJobTimeoutMs(job.type),
|
|
790
|
+
message: 'Agent job timed out before completion was reported.',
|
|
791
|
+
backupRunId: backup?.backupRunId ?? null,
|
|
792
|
+
cleanupRunIds: cleanup?.backupRunIds ?? [],
|
|
793
|
+
})
|
|
794
|
+
) {
|
|
795
|
+
failed.push({ id: job.id })
|
|
796
|
+
affectedServers.add(job.serverId)
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
await Promise.all(
|
|
800
|
+
[...affectedServers].map((id) => notifyServer(id, 'deployments')),
|
|
801
|
+
)
|
|
802
|
+
return failed
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export async function getDeploymentJobStatus({
|
|
806
|
+
serverId,
|
|
807
|
+
token,
|
|
808
|
+
deploymentId,
|
|
809
|
+
}: {
|
|
810
|
+
serverId: string
|
|
811
|
+
token: string
|
|
812
|
+
deploymentId: string
|
|
813
|
+
}): Promise<AgentJobStatus> {
|
|
814
|
+
await requireAgentServer({ serverId, token })
|
|
815
|
+
|
|
816
|
+
const rows = await db
|
|
817
|
+
.select({ status: deployments.status })
|
|
818
|
+
.from(deployments)
|
|
819
|
+
.where(
|
|
820
|
+
and(eq(deployments.id, deploymentId), eq(deployments.serverId, serverId)),
|
|
821
|
+
)
|
|
822
|
+
.limit(1)
|
|
823
|
+
const row = rows.at(0)
|
|
824
|
+
|
|
825
|
+
if (!row) {
|
|
826
|
+
throw new AgentRequestError(404, 'Deployment not found for this server.')
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return {
|
|
830
|
+
deploymentId,
|
|
831
|
+
status: row.status,
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export async function completeDeploymentJob({
|
|
836
|
+
serverId,
|
|
837
|
+
token,
|
|
838
|
+
completion,
|
|
839
|
+
}: {
|
|
840
|
+
serverId: string
|
|
841
|
+
token: string
|
|
842
|
+
completion: AgentJobCompletion
|
|
843
|
+
}) {
|
|
844
|
+
await requireAgentServer({ serverId, token })
|
|
845
|
+
|
|
846
|
+
const rows = await db
|
|
847
|
+
.select({
|
|
848
|
+
deployment: deployments,
|
|
849
|
+
projectId: environments.projectId,
|
|
850
|
+
})
|
|
851
|
+
.from(deployments)
|
|
852
|
+
.innerJoin(environments, eq(environments.id, deployments.environmentId))
|
|
853
|
+
.where(
|
|
854
|
+
and(
|
|
855
|
+
eq(deployments.id, completion.deploymentId),
|
|
856
|
+
eq(deployments.serverId, serverId),
|
|
857
|
+
),
|
|
858
|
+
)
|
|
859
|
+
.limit(1)
|
|
860
|
+
const row = rows.at(0)
|
|
861
|
+
|
|
862
|
+
if (!row) {
|
|
863
|
+
throw new AgentRequestError(404, 'Deployment not found for this server.')
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const terminal = ['canceled', 'failed', 'succeeded'].includes(
|
|
867
|
+
row.deployment.status,
|
|
868
|
+
)
|
|
869
|
+
if (!terminal && row.deployment.status !== 'running') {
|
|
870
|
+
throw new AgentRequestError(409, 'Deployment is not running.')
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Commit captured output before changing main state. Stable sequences make a
|
|
874
|
+
// retry safe after either store succeeds but the response is lost.
|
|
875
|
+
await appendDeploymentLogs(
|
|
876
|
+
{
|
|
877
|
+
projectId: row.projectId,
|
|
878
|
+
environmentId: row.deployment.environmentId,
|
|
879
|
+
deploymentId: row.deployment.id,
|
|
880
|
+
serverId,
|
|
881
|
+
branch: row.deployment.gitBranch,
|
|
882
|
+
},
|
|
883
|
+
completion.logStartSeq,
|
|
884
|
+
completion.logs,
|
|
885
|
+
)
|
|
886
|
+
if (terminal) return { ok: true, ignored: true }
|
|
887
|
+
|
|
888
|
+
const now = new Date()
|
|
889
|
+
const latestEvent = completion.events.at(-1)
|
|
890
|
+
const latestMessage = getLatestProgressMessage(completion)
|
|
891
|
+
|
|
892
|
+
await finishDeployment(db, {
|
|
893
|
+
deployment: row.deployment,
|
|
894
|
+
projectId: row.projectId,
|
|
895
|
+
status: completion.status,
|
|
896
|
+
phase:
|
|
897
|
+
completion.status === 'canceled'
|
|
898
|
+
? 'canceled'
|
|
899
|
+
: (latestEvent?.phase ?? 'completed'),
|
|
900
|
+
// Preserve the queued full commit ID for later redeployments.
|
|
901
|
+
gitSha: row.deployment.gitSha,
|
|
902
|
+
errorMessage:
|
|
903
|
+
completion.status === 'failed' || completion.status === 'canceled'
|
|
904
|
+
? latestMessage
|
|
905
|
+
: row.deployment.errorMessage,
|
|
906
|
+
now,
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
return { ok: true }
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
export async function completeCleanupJob({
|
|
913
|
+
serverId,
|
|
914
|
+
token,
|
|
915
|
+
completion,
|
|
916
|
+
}: {
|
|
917
|
+
serverId: string
|
|
918
|
+
token: string
|
|
919
|
+
completion: AgentCleanupCompletion
|
|
920
|
+
}) {
|
|
921
|
+
await requireAgentServer({
|
|
922
|
+
serverId,
|
|
923
|
+
token,
|
|
924
|
+
cleanupJobId: completion.jobId,
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
const rows = await db
|
|
928
|
+
.select({ job: agentJobs })
|
|
929
|
+
.from(agentJobs)
|
|
930
|
+
.where(
|
|
931
|
+
and(
|
|
932
|
+
eq(agentJobs.id, completion.jobId),
|
|
933
|
+
eq(agentJobs.serverId, serverId),
|
|
934
|
+
inArray(agentJobs.type, ['deprovision', 'purge']),
|
|
935
|
+
),
|
|
936
|
+
)
|
|
937
|
+
.limit(1)
|
|
938
|
+
const row = rows.at(0)
|
|
939
|
+
|
|
940
|
+
if (!row) {
|
|
941
|
+
throw new AgentRequestError(404, 'Cleanup job not found for this server.')
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (row.job.status !== 'running' || row.job.attempt !== completion.attempt) {
|
|
945
|
+
return { ok: true, ignored: true }
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const changed = await finishContainerCleanup(db, {
|
|
949
|
+
job: row.job,
|
|
950
|
+
completion,
|
|
951
|
+
now: new Date(),
|
|
952
|
+
})
|
|
953
|
+
if (!changed) return { ok: true, ignored: true }
|
|
954
|
+
|
|
955
|
+
return { ok: true }
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
export async function completeBackupJob({
|
|
959
|
+
serverId,
|
|
960
|
+
token,
|
|
961
|
+
completion,
|
|
962
|
+
}: {
|
|
963
|
+
serverId: string
|
|
964
|
+
token: string
|
|
965
|
+
completion: AgentBackupCompletion
|
|
966
|
+
}) {
|
|
967
|
+
await requireAgentServer({ serverId, token })
|
|
968
|
+
|
|
969
|
+
const rows = await db
|
|
970
|
+
.select({ job: agentJobs, backupRun: backupRuns })
|
|
971
|
+
.from(agentJobs)
|
|
972
|
+
.innerJoin(backupRuns, eq(backupRuns.id, completion.backupRunId))
|
|
973
|
+
.where(
|
|
974
|
+
and(
|
|
975
|
+
eq(agentJobs.id, completion.jobId),
|
|
976
|
+
eq(agentJobs.serverId, serverId),
|
|
977
|
+
eq(agentJobs.type, 'backup'),
|
|
978
|
+
eq(backupRuns.id, completion.backupRunId),
|
|
979
|
+
eq(backupRuns.serverId, serverId),
|
|
980
|
+
),
|
|
981
|
+
)
|
|
982
|
+
.limit(1)
|
|
983
|
+
const row = rows.at(0)
|
|
984
|
+
|
|
985
|
+
if (!row) {
|
|
986
|
+
throw new AgentRequestError(404, 'Backup job not found for this server.')
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (row.job.status !== 'running') {
|
|
990
|
+
return { ok: true, ignored: true }
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const payload = parseBackupPayload(row.job.payload)
|
|
994
|
+
if (!payload || payload.backupRunId !== completion.backupRunId) {
|
|
995
|
+
throw new AgentRequestError(
|
|
996
|
+
409,
|
|
997
|
+
'Backup completion does not match the claimed job.',
|
|
998
|
+
)
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
for (const artifact of completion.artifacts ?? []) {
|
|
1002
|
+
if (
|
|
1003
|
+
(artifact.storage === 'local' &&
|
|
1004
|
+
!pathIsWithin(payload.localBasePath, artifact.objectKey)) ||
|
|
1005
|
+
(artifact.storage === 'remote' &&
|
|
1006
|
+
!remoteKeyIsWithin(payload.artifactPrefix, artifact.objectKey))
|
|
1007
|
+
) {
|
|
1008
|
+
throw new AgentRequestError(
|
|
1009
|
+
409,
|
|
1010
|
+
'Backup artifact is outside the claimed backup location.',
|
|
1011
|
+
)
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const now = new Date()
|
|
1016
|
+
const errorMessage =
|
|
1017
|
+
completion.status === 'failed'
|
|
1018
|
+
? (completion.message ?? 'Backup failed.')
|
|
1019
|
+
: completion.status === 'canceled'
|
|
1020
|
+
? (completion.message ?? 'Backup canceled.')
|
|
1021
|
+
: null
|
|
1022
|
+
|
|
1023
|
+
const changed = await finishBackup(db, {
|
|
1024
|
+
serverId,
|
|
1025
|
+
completion,
|
|
1026
|
+
errorMessage,
|
|
1027
|
+
now,
|
|
1028
|
+
manifest: completion.manifest ?? row.backupRun.manifest,
|
|
1029
|
+
})
|
|
1030
|
+
|
|
1031
|
+
if (
|
|
1032
|
+
changed &&
|
|
1033
|
+
(completion.status === 'succeeded' ||
|
|
1034
|
+
(completion.status === 'failed' &&
|
|
1035
|
+
typeof completion.manifest === 'object' &&
|
|
1036
|
+
completion.manifest !== null &&
|
|
1037
|
+
'localRecoveryAvailable' in completion.manifest &&
|
|
1038
|
+
completion.manifest.localRecoveryAvailable === true))
|
|
1039
|
+
) {
|
|
1040
|
+
await queueBackupRetentionCleanupJobs({
|
|
1041
|
+
projectId: row.backupRun.projectId,
|
|
1042
|
+
environmentId: row.backupRun.environmentId,
|
|
1043
|
+
now,
|
|
1044
|
+
})
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return { ok: true }
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function pathIsWithin(root: string, candidate: string) {
|
|
1051
|
+
if (!isAbsolute(root) || !isAbsolute(candidate)) return false
|
|
1052
|
+
const path = relative(root, candidate)
|
|
1053
|
+
return path !== '' && path !== '..' && !path.startsWith('../')
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function remoteKeyIsWithin(prefix: string, candidate: string) {
|
|
1057
|
+
const normalizedPrefix = prefix.replace(/^\/+|\/+$/g, '')
|
|
1058
|
+
const normalizedCandidate = candidate.replace(/^\/+/, '')
|
|
1059
|
+
return (
|
|
1060
|
+
normalizedPrefix !== '' &&
|
|
1061
|
+
normalizedCandidate.startsWith(`${normalizedPrefix}/`) &&
|
|
1062
|
+
!normalizedCandidate.split('/').includes('..')
|
|
1063
|
+
)
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
export async function completeBackupCleanupJob({
|
|
1067
|
+
serverId,
|
|
1068
|
+
token,
|
|
1069
|
+
completion,
|
|
1070
|
+
}: {
|
|
1071
|
+
serverId: string
|
|
1072
|
+
token: string
|
|
1073
|
+
completion: AgentBackupCleanupCompletion
|
|
1074
|
+
}) {
|
|
1075
|
+
await requireAgentServer({ serverId, token })
|
|
1076
|
+
|
|
1077
|
+
const rows = await db
|
|
1078
|
+
.select({ job: agentJobs })
|
|
1079
|
+
.from(agentJobs)
|
|
1080
|
+
.where(
|
|
1081
|
+
and(
|
|
1082
|
+
eq(agentJobs.id, completion.jobId),
|
|
1083
|
+
eq(agentJobs.serverId, serverId),
|
|
1084
|
+
eq(agentJobs.type, 'backup_cleanup'),
|
|
1085
|
+
),
|
|
1086
|
+
)
|
|
1087
|
+
.limit(1)
|
|
1088
|
+
const row = rows.at(0)
|
|
1089
|
+
|
|
1090
|
+
if (!row) {
|
|
1091
|
+
throw new AgentRequestError(
|
|
1092
|
+
404,
|
|
1093
|
+
'Backup cleanup job not found for this server.',
|
|
1094
|
+
)
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (row.job.status !== 'running') {
|
|
1098
|
+
return { ok: true, ignored: true }
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const payload = parseBackupCleanupPayload(row.job.payload)
|
|
1102
|
+
if (!payload) {
|
|
1103
|
+
throw new AgentRequestError(409, 'Backup cleanup job payload is invalid.')
|
|
1104
|
+
}
|
|
1105
|
+
const reportedBackupRunIds = Array.from(
|
|
1106
|
+
new Set(completion.backupRunIds.map((id) => id.trim()).filter(Boolean)),
|
|
1107
|
+
).sort()
|
|
1108
|
+
const backupRunIds = Array.from(new Set(payload.backupRunIds)).sort()
|
|
1109
|
+
|
|
1110
|
+
if (
|
|
1111
|
+
backupRunIds.length === 0 ||
|
|
1112
|
+
reportedBackupRunIds.length !== backupRunIds.length ||
|
|
1113
|
+
reportedBackupRunIds.some((id, index) => id !== backupRunIds[index])
|
|
1114
|
+
) {
|
|
1115
|
+
throw new AgentRequestError(
|
|
1116
|
+
409,
|
|
1117
|
+
'Backup cleanup completion does not match the claimed job.',
|
|
1118
|
+
)
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const now = new Date()
|
|
1122
|
+
const errorMessage =
|
|
1123
|
+
completion.status === 'failed'
|
|
1124
|
+
? (completion.message ?? 'Backup cleanup failed.')
|
|
1125
|
+
: completion.status === 'canceled'
|
|
1126
|
+
? (completion.message ?? 'Backup cleanup canceled.')
|
|
1127
|
+
: null
|
|
1128
|
+
|
|
1129
|
+
await finishBackupCleanup(db, {
|
|
1130
|
+
serverId,
|
|
1131
|
+
completion,
|
|
1132
|
+
errorMessage,
|
|
1133
|
+
now,
|
|
1134
|
+
backupRunIds,
|
|
1135
|
+
localObjectKeys: payload.localObjectKeys,
|
|
1136
|
+
remoteObjectKeys: payload.remoteObjectKeys,
|
|
1137
|
+
})
|
|
1138
|
+
|
|
1139
|
+
return { ok: true }
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
export async function completeRestoreJob({
|
|
1143
|
+
serverId,
|
|
1144
|
+
token,
|
|
1145
|
+
completion,
|
|
1146
|
+
}: {
|
|
1147
|
+
serverId: string
|
|
1148
|
+
token: string
|
|
1149
|
+
completion: AgentRestoreCompletion
|
|
1150
|
+
}) {
|
|
1151
|
+
await requireAgentServer({ serverId, token })
|
|
1152
|
+
|
|
1153
|
+
const rows = await db
|
|
1154
|
+
.select({ job: agentJobs })
|
|
1155
|
+
.from(agentJobs)
|
|
1156
|
+
.where(
|
|
1157
|
+
and(
|
|
1158
|
+
eq(agentJobs.id, completion.jobId),
|
|
1159
|
+
eq(agentJobs.serverId, serverId),
|
|
1160
|
+
eq(agentJobs.type, 'restore'),
|
|
1161
|
+
),
|
|
1162
|
+
)
|
|
1163
|
+
.limit(1)
|
|
1164
|
+
const row = rows.at(0)
|
|
1165
|
+
|
|
1166
|
+
if (!row) {
|
|
1167
|
+
throw new AgentRequestError(404, 'Restore job not found for this server.')
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (row.job.status !== 'running') {
|
|
1171
|
+
return { ok: true, ignored: true }
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const now = new Date()
|
|
1175
|
+
const errorMessage =
|
|
1176
|
+
completion.status === 'failed'
|
|
1177
|
+
? (completion.message ?? 'Restore failed.')
|
|
1178
|
+
: completion.status === 'canceled'
|
|
1179
|
+
? (completion.message ?? 'Restore canceled.')
|
|
1180
|
+
: null
|
|
1181
|
+
|
|
1182
|
+
await db
|
|
1183
|
+
.update(agentJobs)
|
|
1184
|
+
.set({
|
|
1185
|
+
status: completion.status,
|
|
1186
|
+
errorMessage,
|
|
1187
|
+
finishedAt: now,
|
|
1188
|
+
updatedAt: now,
|
|
1189
|
+
})
|
|
1190
|
+
.where(
|
|
1191
|
+
and(eq(agentJobs.id, completion.jobId), eq(agentJobs.status, 'running')),
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
return { ok: true }
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
export async function recordDeploymentProgress({
|
|
1198
|
+
serverId,
|
|
1199
|
+
token,
|
|
1200
|
+
progress,
|
|
1201
|
+
}: {
|
|
1202
|
+
serverId: string
|
|
1203
|
+
token: string
|
|
1204
|
+
progress: AgentJobProgress
|
|
1205
|
+
}) {
|
|
1206
|
+
await requireAgentServer({ serverId, token })
|
|
1207
|
+
|
|
1208
|
+
const rows = await db
|
|
1209
|
+
.select({ deployment: deployments, projectId: environments.projectId })
|
|
1210
|
+
.from(deployments)
|
|
1211
|
+
.innerJoin(environments, eq(environments.id, deployments.environmentId))
|
|
1212
|
+
.where(
|
|
1213
|
+
and(
|
|
1214
|
+
eq(deployments.id, progress.deploymentId),
|
|
1215
|
+
eq(deployments.serverId, serverId),
|
|
1216
|
+
),
|
|
1217
|
+
)
|
|
1218
|
+
.limit(1)
|
|
1219
|
+
const row = rows.at(0)
|
|
1220
|
+
|
|
1221
|
+
if (!row) {
|
|
1222
|
+
throw new AgentRequestError(404, 'Deployment not found for this server.')
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const terminal = ['canceled', 'failed', 'succeeded'].includes(
|
|
1226
|
+
row.deployment.status,
|
|
1227
|
+
)
|
|
1228
|
+
if (!terminal && row.deployment.status !== 'running') {
|
|
1229
|
+
throw new AgentRequestError(409, 'Deployment is not running.')
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
await appendDeploymentLogs(
|
|
1233
|
+
{
|
|
1234
|
+
projectId: row.projectId,
|
|
1235
|
+
environmentId: row.deployment.environmentId,
|
|
1236
|
+
deploymentId: row.deployment.id,
|
|
1237
|
+
serverId,
|
|
1238
|
+
branch: row.deployment.gitBranch,
|
|
1239
|
+
},
|
|
1240
|
+
progress.logStartSeq,
|
|
1241
|
+
progress.logs,
|
|
1242
|
+
)
|
|
1243
|
+
if (terminal) return { ok: true, ignored: true }
|
|
1244
|
+
const latestEvent = progress.events.at(-1)
|
|
1245
|
+
if (latestEvent) {
|
|
1246
|
+
await db
|
|
1247
|
+
.update(deployments)
|
|
1248
|
+
.set({ phase: latestEvent.phase })
|
|
1249
|
+
.where(
|
|
1250
|
+
and(
|
|
1251
|
+
eq(deployments.id, progress.deploymentId),
|
|
1252
|
+
eq(deployments.status, 'running'),
|
|
1253
|
+
),
|
|
1254
|
+
)
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return { ok: true }
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
async function failCleanupJob(jobId: string, message: string) {
|
|
1261
|
+
const changed = await db
|
|
1262
|
+
.update(agentJobs)
|
|
1263
|
+
.set({
|
|
1264
|
+
status: 'failed',
|
|
1265
|
+
errorMessage: message,
|
|
1266
|
+
finishedAt: new Date(),
|
|
1267
|
+
updatedAt: new Date(),
|
|
1268
|
+
})
|
|
1269
|
+
.where(eq(agentJobs.id, jobId))
|
|
1270
|
+
.returning({ serverId: agentJobs.serverId })
|
|
1271
|
+
if (changed[0]) await notifyServer(changed[0].serverId, 'deployments')
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
async function failClaimedDeployment(deploymentId: string, message: string) {
|
|
1275
|
+
const changed = await failRunningDeployments(db, {
|
|
1276
|
+
ids: [deploymentId],
|
|
1277
|
+
message,
|
|
1278
|
+
phase: 'failed',
|
|
1279
|
+
now: new Date(),
|
|
1280
|
+
})
|
|
1281
|
+
if (changed.length === 0) return
|
|
1282
|
+
const [deployment] = await db
|
|
1283
|
+
.select({ serverId: deployments.serverId })
|
|
1284
|
+
.from(deployments)
|
|
1285
|
+
.where(eq(deployments.id, deploymentId))
|
|
1286
|
+
.limit(1)
|
|
1287
|
+
if (deployment?.serverId)
|
|
1288
|
+
await notifyServer(deployment.serverId, 'deployments')
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function getLatestProgressMessage(progress: AgentJobCompletion) {
|
|
1292
|
+
return progress.events.at(-1)?.message ?? progress.logs.at(-1) ?? null
|
|
1293
|
+
}
|