vibecarbon 0.1.0
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/LICENSE +663 -0
- package/README.md +188 -0
- package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
- package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
- package/carbon/.claude/settings.json +29 -0
- package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
- package/carbon/.dockerignore +57 -0
- package/carbon/.env.example +219 -0
- package/carbon/.github/copilot-instructions.md +3 -0
- package/carbon/.github/workflows/deploy.yml +346 -0
- package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
- package/carbon/.windsurfrules +74 -0
- package/carbon/AGENTS.md +422 -0
- package/carbon/CLAUDE.md +3 -0
- package/carbon/DEVELOPMENT.md +187 -0
- package/carbon/Dockerfile +98 -0
- package/carbon/LICENSE +21 -0
- package/carbon/PRODUCTION.md +364 -0
- package/carbon/README.md +193 -0
- package/carbon/TESTING.md +138 -0
- package/carbon/backup/Dockerfile +75 -0
- package/carbon/backup/backup.sh +95 -0
- package/carbon/backup/compose-backup.sh +140 -0
- package/carbon/biome.json +83 -0
- package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
- package/carbon/cloud-init/k3s/master-init.sh +215 -0
- package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
- package/carbon/cloud-init/k3s/worker-init.sh +147 -0
- package/carbon/components.json +24 -0
- package/carbon/content/blog/authentication-guide.mdx +34 -0
- package/carbon/content/blog/getting-started.mdx +41 -0
- package/carbon/content/changelog/v0-1-0.mdx +40 -0
- package/carbon/content/docs/analytics.mdx +90 -0
- package/carbon/content/docs/authentication.mdx +231 -0
- package/carbon/content/docs/background-jobs.mdx +116 -0
- package/carbon/content/docs/cli.mdx +630 -0
- package/carbon/content/docs/database.mdx +236 -0
- package/carbon/content/docs/deployment.mdx +227 -0
- package/carbon/content/docs/development.mdx +238 -0
- package/carbon/content/docs/environments.mdx +84 -0
- package/carbon/content/docs/getting-started.mdx +112 -0
- package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
- package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
- package/carbon/content/docs/optional-services.mdx +160 -0
- package/carbon/db/Dockerfile +23 -0
- package/carbon/docker-compose.dev-init.yml +5 -0
- package/carbon/docker-compose.metabase.override.yml +14 -0
- package/carbon/docker-compose.metabase.prod.yml +28 -0
- package/carbon/docker-compose.metabase.yml +84 -0
- package/carbon/docker-compose.n8n.override.yml +14 -0
- package/carbon/docker-compose.n8n.prod.yml +31 -0
- package/carbon/docker-compose.n8n.yml +97 -0
- package/carbon/docker-compose.observability.override.yml +18 -0
- package/carbon/docker-compose.observability.prod.yml +28 -0
- package/carbon/docker-compose.observability.yml +125 -0
- package/carbon/docker-compose.override.yml +28 -0
- package/carbon/docker-compose.prod.yml +294 -0
- package/carbon/docker-compose.yml +508 -0
- package/carbon/docker-entrypoint.sh +12 -0
- package/carbon/ha/activate-standby.sh +52 -0
- package/carbon/ha/primary-init.sql +36 -0
- package/carbon/ha/standby-init.sh +74 -0
- package/carbon/k8s/LICENSE +99 -0
- package/carbon/k8s/README.md +272 -0
- package/carbon/k8s/base/app/deployment.yaml +130 -0
- package/carbon/k8s/base/app/hpa.yaml +44 -0
- package/carbon/k8s/base/app/kustomization.yaml +10 -0
- package/carbon/k8s/base/app/network-policy.yaml +124 -0
- package/carbon/k8s/base/app/pdb.yaml +14 -0
- package/carbon/k8s/base/app/rbac.yaml +36 -0
- package/carbon/k8s/base/app/service.yaml +16 -0
- package/carbon/k8s/base/backup/cronjob.yaml +120 -0
- package/carbon/k8s/base/backup/kustomization.yaml +7 -0
- package/carbon/k8s/base/backup/network-policy.yaml +31 -0
- package/carbon/k8s/base/backup/rbac.yaml +7 -0
- package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
- package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
- package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
- package/carbon/k8s/base/config/configmap.yaml +48 -0
- package/carbon/k8s/base/config/kustomization.yaml +8 -0
- package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
- package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
- package/carbon/k8s/base/kustomization.yaml +43 -0
- package/carbon/k8s/base/namespace.yaml +7 -0
- package/carbon/k8s/base/network-policies.yaml +95 -0
- package/carbon/k8s/base/registry/kustomization.yaml +5 -0
- package/carbon/k8s/base/registry/local-registry.yaml +193 -0
- package/carbon/k8s/base/traefik/certificate.yaml +21 -0
- package/carbon/k8s/base/traefik/configmap.yaml +13 -0
- package/carbon/k8s/base/traefik/deployment.yaml +165 -0
- package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
- package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
- package/carbon/k8s/base/traefik/middleware.yaml +109 -0
- package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
- package/carbon/k8s/base/traefik/service.yaml +38 -0
- package/carbon/k8s/flux/README.md +49 -0
- package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
- package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
- package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
- package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
- package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
- package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
- package/carbon/k8s/infra/kustomization.yaml +21 -0
- package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
- package/carbon/k8s/overlays/local/configmap.yaml +23 -0
- package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
- package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
- package/carbon/k8s/overlays/local/namespace.yaml +8 -0
- package/carbon/k8s/overlays/local/secrets.yaml +32 -0
- package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
- package/carbon/k8s/test-local.sh +318 -0
- package/carbon/k8s/values/supabase.values.yaml +133 -0
- package/carbon/package.json +154 -0
- package/carbon/runtime.Dockerfile +17 -0
- package/carbon/scripts/_dev-jwt.mjs +45 -0
- package/carbon/scripts/check-rls.ts +53 -0
- package/carbon/scripts/dev-init.js +191 -0
- package/carbon/scripts/dev.js +191 -0
- package/carbon/scripts/docker-down.js +63 -0
- package/carbon/scripts/docker-logs.js +59 -0
- package/carbon/scripts/docker-up.js +222 -0
- package/carbon/scripts/generate-dev-configs.sh +131 -0
- package/carbon/scripts/generate-rss.ts +116 -0
- package/carbon/scripts/generate-sitemap.ts +102 -0
- package/carbon/scripts/k8s-apply.js +71 -0
- package/carbon/scripts/k8s-delete.js +75 -0
- package/carbon/scripts/lib/manifest.js +176 -0
- package/carbon/scripts/secret-scan.mjs +278 -0
- package/carbon/scripts/validate-dev-configs.sh +101 -0
- package/carbon/src/client/App.tsx +202 -0
- package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
- package/carbon/src/client/assets/hyperformant-light.svg +29 -0
- package/carbon/src/client/assets/logos/aider.svg +1 -0
- package/carbon/src/client/assets/logos/antigravity.svg +1 -0
- package/carbon/src/client/assets/logos/bolt.svg +1 -0
- package/carbon/src/client/assets/logos/claude-code.svg +1 -0
- package/carbon/src/client/assets/logos/copilot.svg +1 -0
- package/carbon/src/client/assets/logos/cursor.svg +1 -0
- package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
- package/carbon/src/client/assets/logos/lovable.svg +1 -0
- package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
- package/carbon/src/client/assets/logos/v0.svg +1 -0
- package/carbon/src/client/assets/logos/windsurf.svg +1 -0
- package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
- package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
- package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
- package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
- package/carbon/src/client/components/AppSidebar.tsx +760 -0
- package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
- package/carbon/src/client/components/CTAFooter.tsx +59 -0
- package/carbon/src/client/components/ComparisonSection.tsx +132 -0
- package/carbon/src/client/components/ContentPanel.tsx +66 -0
- package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
- package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
- package/carbon/src/client/components/FAQSection.tsx +76 -0
- package/carbon/src/client/components/FileUpload.tsx +210 -0
- package/carbon/src/client/components/HeaderActions.tsx +17 -0
- package/carbon/src/client/components/Hero.tsx +608 -0
- package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
- package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
- package/carbon/src/client/components/Logo.tsx +87 -0
- package/carbon/src/client/components/LogoStrip.tsx +76 -0
- package/carbon/src/client/components/MetricsStrip.tsx +48 -0
- package/carbon/src/client/components/Nav.tsx +195 -0
- package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
- package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
- package/carbon/src/client/components/PageHeader.tsx +24 -0
- package/carbon/src/client/components/PlanGate.tsx +36 -0
- package/carbon/src/client/components/PricingSection.tsx +371 -0
- package/carbon/src/client/components/SEO.tsx +34 -0
- package/carbon/src/client/components/SmoothScroll.tsx +45 -0
- package/carbon/src/client/components/TechStackSection.tsx +165 -0
- package/carbon/src/client/components/WorkflowSection.tsx +604 -0
- package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
- package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
- package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
- package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
- package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
- package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
- package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
- package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
- package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
- package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
- package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
- package/carbon/src/client/components/effects/index.ts +2 -0
- package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
- package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
- package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
- package/carbon/src/client/components/scroll/index.ts +2 -0
- package/carbon/src/client/components/ui/accordion.tsx +71 -0
- package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
- package/carbon/src/client/components/ui/alert.tsx +73 -0
- package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
- package/carbon/src/client/components/ui/avatar.tsx +91 -0
- package/carbon/src/client/components/ui/badge.tsx +50 -0
- package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
- package/carbon/src/client/components/ui/button-group.tsx +78 -0
- package/carbon/src/client/components/ui/button.tsx +120 -0
- package/carbon/src/client/components/ui/calendar.tsx +182 -0
- package/carbon/src/client/components/ui/card.tsx +85 -0
- package/carbon/src/client/components/ui/carousel.tsx +227 -0
- package/carbon/src/client/components/ui/chart.tsx +357 -0
- package/carbon/src/client/components/ui/checkbox.tsx +27 -0
- package/carbon/src/client/components/ui/collapsible.tsx +15 -0
- package/carbon/src/client/components/ui/command.tsx +178 -0
- package/carbon/src/client/components/ui/context-menu.tsx +233 -0
- package/carbon/src/client/components/ui/dialog.tsx +132 -0
- package/carbon/src/client/components/ui/drawer.tsx +118 -0
- package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
- package/carbon/src/client/components/ui/empty.tsx +94 -0
- package/carbon/src/client/components/ui/field.tsx +226 -0
- package/carbon/src/client/components/ui/hover-card.tsx +44 -0
- package/carbon/src/client/components/ui/input-group.tsx +146 -0
- package/carbon/src/client/components/ui/input-otp.tsx +83 -0
- package/carbon/src/client/components/ui/input.tsx +20 -0
- package/carbon/src/client/components/ui/item.tsx +188 -0
- package/carbon/src/client/components/ui/kbd.tsx +26 -0
- package/carbon/src/client/components/ui/label.tsx +21 -0
- package/carbon/src/client/components/ui/menubar.tsx +250 -0
- package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
- package/carbon/src/client/components/ui/pagination.tsx +102 -0
- package/carbon/src/client/components/ui/popover.tsx +75 -0
- package/carbon/src/client/components/ui/progress.tsx +66 -0
- package/carbon/src/client/components/ui/radio-group.tsx +36 -0
- package/carbon/src/client/components/ui/resizable.tsx +46 -0
- package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
- package/carbon/src/client/components/ui/select.tsx +186 -0
- package/carbon/src/client/components/ui/separator.tsx +21 -0
- package/carbon/src/client/components/ui/sheet.tsx +124 -0
- package/carbon/src/client/components/ui/sidebar.tsx +702 -0
- package/carbon/src/client/components/ui/skeleton.tsx +13 -0
- package/carbon/src/client/components/ui/slider.tsx +57 -0
- package/carbon/src/client/components/ui/sonner.tsx +45 -0
- package/carbon/src/client/components/ui/spinner.tsx +15 -0
- package/carbon/src/client/components/ui/switch.tsx +30 -0
- package/carbon/src/client/components/ui/table.tsx +89 -0
- package/carbon/src/client/components/ui/tabs.tsx +73 -0
- package/carbon/src/client/components/ui/textarea.tsx +18 -0
- package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
- package/carbon/src/client/components/ui/toggle.tsx +44 -0
- package/carbon/src/client/components/ui/tooltip.tsx +56 -0
- package/carbon/src/client/hooks/api/index.ts +14 -0
- package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
- package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
- package/carbon/src/client/hooks/use-mobile.ts +21 -0
- package/carbon/src/client/hooks/useMousePosition.ts +91 -0
- package/carbon/src/client/hooks/useNotifications.tsx +124 -0
- package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
- package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
- package/carbon/src/client/hooks/usePackageManager.ts +69 -0
- package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
- package/carbon/src/client/hooks/useRunningServices.ts +114 -0
- package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
- package/carbon/src/client/index.css +467 -0
- package/carbon/src/client/index.html +56 -0
- package/carbon/src/client/lib/admin-services.ts +151 -0
- package/carbon/src/client/lib/api.ts +32 -0
- package/carbon/src/client/lib/blog.ts +35 -0
- package/carbon/src/client/lib/changelog.ts +33 -0
- package/carbon/src/client/lib/docs-search.ts +101 -0
- package/carbon/src/client/lib/docs.ts +37 -0
- package/carbon/src/client/lib/i18n.ts +32 -0
- package/carbon/src/client/lib/supabase.ts +72 -0
- package/carbon/src/client/lib/tailwind-colors.ts +357 -0
- package/carbon/src/client/lib/theme.ts +117 -0
- package/carbon/src/client/lib/utils.ts +22 -0
- package/carbon/src/client/locales/de.json +529 -0
- package/carbon/src/client/locales/en.json +461 -0
- package/carbon/src/client/locales/es.json +529 -0
- package/carbon/src/client/locales/fr.json +529 -0
- package/carbon/src/client/locales/pt.json +529 -0
- package/carbon/src/client/main.tsx +56 -0
- package/carbon/src/client/mdx.d.ts +13 -0
- package/carbon/src/client/pages/ApiDocs.tsx +76 -0
- package/carbon/src/client/pages/AuthCallback.tsx +34 -0
- package/carbon/src/client/pages/Blog.tsx +167 -0
- package/carbon/src/client/pages/Changelog.tsx +171 -0
- package/carbon/src/client/pages/Charts.tsx +388 -0
- package/carbon/src/client/pages/Checkout.tsx +227 -0
- package/carbon/src/client/pages/Contact.tsx +174 -0
- package/carbon/src/client/pages/Dashboard.tsx +368 -0
- package/carbon/src/client/pages/Docs.tsx +372 -0
- package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
- package/carbon/src/client/pages/Home.tsx +187 -0
- package/carbon/src/client/pages/Legal.tsx +100 -0
- package/carbon/src/client/pages/Login.tsx +408 -0
- package/carbon/src/client/pages/MFAVerify.tsx +156 -0
- package/carbon/src/client/pages/NotFound.tsx +21 -0
- package/carbon/src/client/pages/Onboarding.tsx +246 -0
- package/carbon/src/client/pages/ResetPassword.tsx +200 -0
- package/carbon/src/client/pages/UIComponents.tsx +390 -0
- package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
- package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
- package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
- package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
- package/carbon/src/client/pages/admin/Logs.tsx +18 -0
- package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
- package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
- package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
- package/carbon/src/client/pages/admin/Settings.tsx +314 -0
- package/carbon/src/client/pages/admin/Theme.tsx +465 -0
- package/carbon/src/client/pages/admin/Users.tsx +365 -0
- package/carbon/src/client/pages/api-docs.css +156 -0
- package/carbon/src/client/pages/organizations/Details.tsx +200 -0
- package/carbon/src/client/pages/organizations/Members.tsx +402 -0
- package/carbon/src/client/pages/settings/Billing.tsx +473 -0
- package/carbon/src/client/pages/settings/Profile.tsx +160 -0
- package/carbon/src/client/pages/settings/Security.tsx +341 -0
- package/carbon/src/client/public/favicon.svg +19 -0
- package/carbon/src/client/public/robots.txt +8 -0
- package/carbon/src/server/billing/index.ts +104 -0
- package/carbon/src/server/billing/provider.ts +81 -0
- package/carbon/src/server/billing/providers/paddle.ts +314 -0
- package/carbon/src/server/billing/providers/polar.ts +325 -0
- package/carbon/src/server/billing/providers/stripe.ts +233 -0
- package/carbon/src/server/emails/templates.ts +116 -0
- package/carbon/src/server/index.ts +554 -0
- package/carbon/src/server/lib/auth.ts +6 -0
- package/carbon/src/server/lib/email.ts +64 -0
- package/carbon/src/server/lib/env.ts +112 -0
- package/carbon/src/server/lib/errors.ts +21 -0
- package/carbon/src/server/lib/logger.ts +17 -0
- package/carbon/src/server/lib/rate-limiter.ts +288 -0
- package/carbon/src/server/lib/request.ts +34 -0
- package/carbon/src/server/lib/stripe.ts +42 -0
- package/carbon/src/server/lib/supabase.ts +65 -0
- package/carbon/src/server/middleware/requirePlan.ts +80 -0
- package/carbon/src/server/routes/_internal/services-status.ts +958 -0
- package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
- package/carbon/src/server/routes/health.ts +48 -0
- package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
- package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
- package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
- package/carbon/src/server/routes/v1/auth.ts +390 -0
- package/carbon/src/server/routes/v1/billing.ts +718 -0
- package/carbon/src/server/routes/v1/contact.ts +93 -0
- package/carbon/src/server/routes/v1/index.ts +1333 -0
- package/carbon/src/server/routes/v1/newsletter.ts +181 -0
- package/carbon/src/server/routes/v1/performance.ts +157 -0
- package/carbon/src/server/routes/v1/stats.ts +170 -0
- package/carbon/src/server/routes/v1/theme.ts +106 -0
- package/carbon/src/server/routes/webhooks/billing.ts +376 -0
- package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
- package/carbon/src/server/types.ts +11 -0
- package/carbon/src/shared/pricing.ts +155 -0
- package/carbon/src/shared/types.ts +338 -0
- package/carbon/supabase/migrations/00001_init.sql +717 -0
- package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
- package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
- package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
- package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
- package/carbon/supabase/seed.sql +16 -0
- package/carbon/tests/_helpers/app.ts +45 -0
- package/carbon/tests/_helpers/env.ts +37 -0
- package/carbon/tests/_helpers/factories.ts +69 -0
- package/carbon/tests/_helpers/jwt.ts +23 -0
- package/carbon/tests/_helpers/setup-integration.ts +20 -0
- package/carbon/tests/_helpers/setup-rtl.ts +12 -0
- package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
- package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
- package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
- package/carbon/tests/integration/server/routes/health.test.ts +61 -0
- package/carbon/tests/structural/i18n-parity.test.ts +42 -0
- package/carbon/tests/unit/client/utils.test.ts +49 -0
- package/carbon/tests/unit/shared/pricing.test.ts +93 -0
- package/carbon/tsconfig.json +27 -0
- package/carbon/tsconfig.server.json +27 -0
- package/carbon/tsconfig.test.json +9 -0
- package/carbon/vite.config.ts +110 -0
- package/carbon/vitest.config.ts +74 -0
- package/carbon/volumes/db/jwt.sql +57 -0
- package/carbon/volumes/db/metabase-init.sh +29 -0
- package/carbon/volumes/db/n8n-init.sh +25 -0
- package/carbon/volumes/db/realtime.sql +33 -0
- package/carbon/volumes/db/roles.sql +93 -0
- package/carbon/volumes/db/set-passwords.sh +12 -0
- package/carbon/volumes/db/super-admin.dev.sql +113 -0
- package/carbon/volumes/db/super-admin.generated.sql +113 -0
- package/carbon/volumes/db/super-admin.sql +114 -0
- package/carbon/volumes/grafana/dashboards/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards/overview.json +523 -0
- package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
- package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
- package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
- package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
- package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
- package/carbon/volumes/kong/kong.yml +208 -0
- package/carbon/volumes/loki/loki-config.yml +58 -0
- package/carbon/volumes/n8n/hooks.js +131 -0
- package/carbon/volumes/n8n/scripts/setup.sh +66 -0
- package/carbon/volumes/prometheus/prometheus.yml +43 -0
- package/carbon/volumes/promtail/promtail-config.yml +64 -0
- package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
- package/carbon/volumes/traefik/middlewares.yml +95 -0
- package/carbon/volumes/traefik/vite-dev.yml +20 -0
- package/package.json +95 -0
- package/src/access.js +354 -0
- package/src/activate.js +187 -0
- package/src/add.js +718 -0
- package/src/backup.js +786 -0
- package/src/cli.js +350 -0
- package/src/configure.js +967 -0
- package/src/console.js +155 -0
- package/src/create.js +1499 -0
- package/src/deploy.js +311 -0
- package/src/destroy.js +2033 -0
- package/src/diagnose.js +735 -0
- package/src/down.js +80 -0
- package/src/failover.js +1032 -0
- package/src/lib/backup-s3.js +179 -0
- package/src/lib/build.js +33 -0
- package/src/lib/checksum.js +28 -0
- package/src/lib/ci-setup.js +666 -0
- package/src/lib/cli/help.js +129 -0
- package/src/lib/cli/parse-flags.js +160 -0
- package/src/lib/cli/select-action.js +65 -0
- package/src/lib/cli/select-environment.js +108 -0
- package/src/lib/cli/tty-guard.js +75 -0
- package/src/lib/cloudflare.js +447 -0
- package/src/lib/colors.js +52 -0
- package/src/lib/command.js +361 -0
- package/src/lib/config.js +359 -0
- package/src/lib/cost.js +103 -0
- package/src/lib/deploy/bundle.js +231 -0
- package/src/lib/deploy/compose/acme-verify.js +121 -0
- package/src/lib/deploy/compose/build-args.js +78 -0
- package/src/lib/deploy/compose/ha.js +1874 -0
- package/src/lib/deploy/compose/index.js +1294 -0
- package/src/lib/deploy/compose/reconcile.js +74 -0
- package/src/lib/deploy/github.js +382 -0
- package/src/lib/deploy/image.js +191 -0
- package/src/lib/deploy/index.js +119 -0
- package/src/lib/deploy/k8s/LICENSE +99 -0
- package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
- package/src/lib/deploy/k8s/ha/index.js +1000 -0
- package/src/lib/deploy/k8s/index.js +62 -0
- package/src/lib/deploy/k8s/k3s.js +2515 -0
- package/src/lib/deploy/orchestrator.js +1401 -0
- package/src/lib/deploy/prompts.js +545 -0
- package/src/lib/deploy/remote-build.js +130 -0
- package/src/lib/deploy/state.js +120 -0
- package/src/lib/deploy/utils.js +328 -0
- package/src/lib/deploy-logger.js +93 -0
- package/src/lib/dns-propagation.js +48 -0
- package/src/lib/environment.js +58 -0
- package/src/lib/fetch-retry.js +103 -0
- package/src/lib/github-environments.js +335 -0
- package/src/lib/hetzner-dns.js +377 -0
- package/src/lib/hetzner-guided-setup.js +275 -0
- package/src/lib/host-keys.js +37 -0
- package/src/lib/iac/cloud-init.js +52 -0
- package/src/lib/iac/index.js +325 -0
- package/src/lib/iac/programs/hetzner-compose.js +130 -0
- package/src/lib/iac/programs/hetzner-k8s.js +320 -0
- package/src/lib/kubectl.js +81 -0
- package/src/lib/licensing/index.js +259 -0
- package/src/lib/licensing/tiers.js +181 -0
- package/src/lib/licensing/validator.js +171 -0
- package/src/lib/merge-package-json.js +90 -0
- package/src/lib/operator-ip.js +381 -0
- package/src/lib/package-manager.js +111 -0
- package/src/lib/perf.js +71 -0
- package/src/lib/project-guard.js +39 -0
- package/src/lib/project.js +334 -0
- package/src/lib/providers/base.js +276 -0
- package/src/lib/providers/hetzner-s3.js +656 -0
- package/src/lib/providers/hetzner.js +755 -0
- package/src/lib/providers/index.js +164 -0
- package/src/lib/s3.js +510 -0
- package/src/lib/secret-scan.js +583 -0
- package/src/lib/secrets.js +63 -0
- package/src/lib/server-types.js +195 -0
- package/src/lib/shell.js +91 -0
- package/src/lib/ssh.js +241 -0
- package/src/lib/tracker.js +242 -0
- package/src/lib/upgrade-policy.js +170 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/version.js +5 -0
- package/src/remove.js +292 -0
- package/src/reset.js +97 -0
- package/src/restore.js +871 -0
- package/src/scale.js +1734 -0
- package/src/shell.js +222 -0
- package/src/status.js +981 -0
- package/src/up.js +264 -0
- package/src/upgrade.js +721 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fetchWithRetry — a small wrapper around global `fetch` that retries on
|
|
3
|
+
* transient network + upstream errors. Used by the Hetzner DNS and
|
|
4
|
+
* Cloudflare clients so a momentary "fetch failed" during deploy / scale
|
|
5
|
+
* doesn't kill an otherwise-healthy run.
|
|
6
|
+
*
|
|
7
|
+
* Transient conditions we retry:
|
|
8
|
+
* - `fetch failed` (Node undici wraps TCP RST / DNS blip / ECONNRESET)
|
|
9
|
+
* - HTTP 429 / 500 / 502 / 503 / 504
|
|
10
|
+
*
|
|
11
|
+
* Non-transient (4xx other than 429) short-circuit immediately so we don't
|
|
12
|
+
* retry a genuine bad-request / auth failure.
|
|
13
|
+
*
|
|
14
|
+
* Policy: up to 5 attempts, exponential backoff (1s, 2s, 4s, 8s). Cap total
|
|
15
|
+
* wait at ~15s so we don't silently stretch a deploy.
|
|
16
|
+
*
|
|
17
|
+
* Connection pooling: we install a global undici Agent with keep-alive so
|
|
18
|
+
* every fetch() call in the process shares a warm connection pool. A single
|
|
19
|
+
* deploy makes 50+ round-trips against 4 APIs (Hetzner Cloud, Hetzner DNS,
|
|
20
|
+
* Cloudflare, Hetzner S3) — without keep-alive each call pays a fresh
|
|
21
|
+
* TCP+TLS handshake, which magnifies flaky-network failure rates. With
|
|
22
|
+
* keep-alive, repeat calls reuse idle sockets, eliminating ~80% of the
|
|
23
|
+
* "fetch failed" class of errors we were papering over with retries.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { Agent, setGlobalDispatcher } from 'undici';
|
|
27
|
+
|
|
28
|
+
// Install once per process. Subsequent imports are no-ops (ES modules run
|
|
29
|
+
// top-level code exactly once). Tune connections for our fan-out: deploys
|
|
30
|
+
// talk to ~4 hosts in parallel; 16 per-origin is more than we need and
|
|
31
|
+
// leaves room for batch operations.
|
|
32
|
+
setGlobalDispatcher(
|
|
33
|
+
new Agent({
|
|
34
|
+
keepAliveTimeout: 30_000, // hold idle sockets 30s for reuse
|
|
35
|
+
keepAliveMaxTimeout: 60_000,
|
|
36
|
+
connections: 16, // per-origin cap
|
|
37
|
+
pipelining: 1, // conservative — not all APIs tolerate pipelining
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
42
|
+
const TRANSIENT_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
43
|
+
|
|
44
|
+
function isTransientNetworkError(err) {
|
|
45
|
+
if (!err) return false;
|
|
46
|
+
const msg = String(err.message || err);
|
|
47
|
+
// Node fetch wraps low-level failures into these strings — the underlying
|
|
48
|
+
// cause (ECONNRESET, ENOTFOUND, EAI_AGAIN) is buried in err.cause.
|
|
49
|
+
return (
|
|
50
|
+
/fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket hang up|network.*timeout|undici/i.test(
|
|
51
|
+
msg,
|
|
52
|
+
) ||
|
|
53
|
+
(err.cause &&
|
|
54
|
+
/ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|UND_ERR/i.test(String(err.cause.code ?? err.cause)))
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string | URL | Request} url
|
|
60
|
+
* @param {RequestInit & { maxAttempts?: number; label?: string }} [init]
|
|
61
|
+
*/
|
|
62
|
+
export async function fetchWithRetry(url, init = {}) {
|
|
63
|
+
const { maxAttempts = DEFAULT_MAX_ATTEMPTS, label, ...fetchInit } = init;
|
|
64
|
+
// Label the retry line so a flaky deploy's output makes sense at a glance.
|
|
65
|
+
// Prefer an explicit caller-supplied label; else derive from URL hostname.
|
|
66
|
+
const op =
|
|
67
|
+
label ||
|
|
68
|
+
(() => {
|
|
69
|
+
try {
|
|
70
|
+
return new URL(String(url)).host;
|
|
71
|
+
} catch {
|
|
72
|
+
return 'fetch';
|
|
73
|
+
}
|
|
74
|
+
})();
|
|
75
|
+
let lastErr;
|
|
76
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(url, fetchInit);
|
|
79
|
+
if (response.ok || !TRANSIENT_STATUS.has(response.status)) {
|
|
80
|
+
return response;
|
|
81
|
+
}
|
|
82
|
+
// Drain & retry — the retry path re-issues the request with a fresh body
|
|
83
|
+
// stream where applicable.
|
|
84
|
+
lastErr = new Error(`HTTP ${response.status} on ${url}`);
|
|
85
|
+
lastErr.status = response.status;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (!isTransientNetworkError(err)) throw err;
|
|
88
|
+
lastErr = err;
|
|
89
|
+
}
|
|
90
|
+
if (attempt === maxAttempts) break;
|
|
91
|
+
const backoffMs = Math.min(2 ** (attempt - 1) * 1000, 8000);
|
|
92
|
+
// Surface the retry so a user staring at a slow deploy knows we're
|
|
93
|
+
// not silently stuck. stderr keeps it out of captured stdout pipes.
|
|
94
|
+
const reason = lastErr?.status
|
|
95
|
+
? `HTTP ${lastErr.status}`
|
|
96
|
+
: (lastErr?.message || String(lastErr)).slice(0, 120);
|
|
97
|
+
console.error(
|
|
98
|
+
`[retry] ${op}: attempt ${attempt}/${maxAttempts} failed (${reason}); retrying in ${backoffMs}ms`,
|
|
99
|
+
);
|
|
100
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
101
|
+
}
|
|
102
|
+
throw lastErr;
|
|
103
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for managing GitHub Environments + secrets/vars on the customer's
|
|
3
|
+
* repo. Used by Phase 4.3b of the GitOps refactor — the deploy workflow
|
|
4
|
+
* (.github/workflows/deploy.yml) reads per-environment `${{ secrets.X }}`
|
|
5
|
+
* + `${{ vars.Y }}` to apply the vibecarbon-secrets Secret, hetzner-api-token
|
|
6
|
+
* Secret, and kube-system/hcloud Secret into the cluster.
|
|
7
|
+
*
|
|
8
|
+
* All calls go through the `gh` CLI (argv form, no shell interpolation) so
|
|
9
|
+
* secret values never leak through ps/logs/shell history. Secret upload
|
|
10
|
+
* pipes the value through stdin — `gh secret set NAME` (no --body) reads
|
|
11
|
+
* stdin — to keep the value off argv.
|
|
12
|
+
*
|
|
13
|
+
* Decision: secret scope (1=org/repo-level, 2=per-environment) matches
|
|
14
|
+
* the session 2026-04-20 discussion:
|
|
15
|
+
* Org-level: HETZNER_API_TOKEN, CLOUDFLARE_API_TOKEN,
|
|
16
|
+
* S3_ACCESS_KEY, S3_SECRET_KEY
|
|
17
|
+
* Per-env: KUBECONFIG_B64, DB_PASSWORD, JWT_SECRET,
|
|
18
|
+
* SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY,
|
|
19
|
+
* REALTIME_SECRET, DB_ENC_KEY, VAULT_ENC_KEY,
|
|
20
|
+
* PG_META_CRYPTO_KEY, LOGFLARE_API_KEY,
|
|
21
|
+
* ADMIN_EMAIL, ADMIN_PASSWORD, SMTP_*
|
|
22
|
+
* Per-env vars: HCLOUD_NETWORK_ID, SITE_URL, DNS_PROVIDER
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { runCommandAsync } from './command.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the current repo's `owner/name` from the gh CLI. Relies on a git
|
|
29
|
+
* remote being configured (which `vibecarbon create` sets up).
|
|
30
|
+
*/
|
|
31
|
+
async function getRepoSlug() {
|
|
32
|
+
const out = await runCommandAsync(
|
|
33
|
+
['gh', 'repo', 'view', '--json', 'nameWithOwner', '-q', '.nameWithOwner'],
|
|
34
|
+
{ silent: true },
|
|
35
|
+
);
|
|
36
|
+
return out.toString().trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ensure a GitHub Environment exists on the customer's repo. Idempotent.
|
|
41
|
+
* `gh api -X PUT` creates if missing, no-op if present.
|
|
42
|
+
*/
|
|
43
|
+
export async function ensureEnvironment(envName) {
|
|
44
|
+
const repo = await getRepoSlug();
|
|
45
|
+
await runCommandAsync(['gh', 'api', `repos/${repo}/environments/${envName}`, '-X', 'PUT'], {
|
|
46
|
+
silent: true,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Set a secret value via `gh secret set`, piping the value through stdin.
|
|
52
|
+
*
|
|
53
|
+
* Pipes stdin instead of passing the value on argv — `gh secret set NAME`
|
|
54
|
+
* (no --body) reads from stdin. Previously used `--body-file <path>` but
|
|
55
|
+
* that flag was removed from `gh` (verified 2026-04-22: only `-b/--body`,
|
|
56
|
+
* `-f/--env-file`, and stdin remain). Stdin has the same security
|
|
57
|
+
* properties as a 0600 temp file — never on argv, never on disk.
|
|
58
|
+
*
|
|
59
|
+
* `env` = undefined → repo-level secret; `env` = "prod" → environment-level.
|
|
60
|
+
*
|
|
61
|
+
* Returns false if the value is empty (skips the call — GH rejects empty
|
|
62
|
+
* secrets). True on successful write.
|
|
63
|
+
*/
|
|
64
|
+
async function setSecretFromBodyFile(key, value, env) {
|
|
65
|
+
if (!value) return false;
|
|
66
|
+
const args = ['gh', 'secret', 'set', key];
|
|
67
|
+
if (env) args.push('--env', env);
|
|
68
|
+
await runCommandAsync(args, { silent: true, input: value });
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set a variable (non-secret) value. `gh variable set` doesn't support
|
|
74
|
+
* --body-file, so the value goes on argv. Acceptable since these are
|
|
75
|
+
* non-secret (network IDs, URLs).
|
|
76
|
+
*/
|
|
77
|
+
async function setVariable(key, value, env) {
|
|
78
|
+
if (value === undefined || value === null || value === '') return false;
|
|
79
|
+
const args = ['gh', 'variable', 'set', key, '--body', String(value)];
|
|
80
|
+
if (env) args.push('--env', env);
|
|
81
|
+
await runCommandAsync(args, { silent: true });
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Seed the org-level (repo-level) secrets. Called once per repo — values
|
|
87
|
+
* are shared across all environments. If any are already set, they're
|
|
88
|
+
* overwritten (rotation path).
|
|
89
|
+
*
|
|
90
|
+
* @param {object} creds - Shape matches ~/.vibecarbon/credentials.json
|
|
91
|
+
*/
|
|
92
|
+
export async function seedOrgSecrets(creds) {
|
|
93
|
+
const hetznerToken = creds?.hetzner?.apiToken ?? creds?.hetznerApiToken ?? '';
|
|
94
|
+
const cloudflareToken = creds?.cloudflare?.apiToken ?? creds?.cloudflareApiToken ?? '';
|
|
95
|
+
const s3Access = creds?.s3?.accessKey ?? '';
|
|
96
|
+
const s3Secret = creds?.s3?.secretKey ?? '';
|
|
97
|
+
|
|
98
|
+
const applied = [];
|
|
99
|
+
if (await setSecretFromBodyFile('HETZNER_API_TOKEN', hetznerToken))
|
|
100
|
+
applied.push('HETZNER_API_TOKEN');
|
|
101
|
+
if (await setSecretFromBodyFile('CLOUDFLARE_API_TOKEN', cloudflareToken))
|
|
102
|
+
applied.push('CLOUDFLARE_API_TOKEN');
|
|
103
|
+
if (await setSecretFromBodyFile('S3_ACCESS_KEY', s3Access)) applied.push('S3_ACCESS_KEY');
|
|
104
|
+
if (await setSecretFromBodyFile('S3_SECRET_KEY', s3Secret)) applied.push('S3_SECRET_KEY');
|
|
105
|
+
return applied;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Seed the per-environment secrets + variables. `envVars` are sourced from
|
|
110
|
+
* .env.local (read by the caller) so we can re-use the same values as the
|
|
111
|
+
* imperative deploy path sources.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} envName - "dev" | "staging" | "prod"
|
|
114
|
+
* @param {object} secrets - Key-value map of secret names → values.
|
|
115
|
+
* @param {object} vars - Key-value map of variable names → values.
|
|
116
|
+
*/
|
|
117
|
+
export async function seedEnvironmentSecrets(envName, secrets, vars = {}) {
|
|
118
|
+
await ensureEnvironment(envName);
|
|
119
|
+
|
|
120
|
+
const appliedSecrets = [];
|
|
121
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
122
|
+
if (await setSecretFromBodyFile(key, value, envName)) appliedSecrets.push(key);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const appliedVars = [];
|
|
126
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
127
|
+
if (await setVariable(key, value, envName)) appliedVars.push(key);
|
|
128
|
+
}
|
|
129
|
+
return { secrets: appliedSecrets, vars: appliedVars };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Upload the cluster kubeconfig as a base64-encoded env secret. The deploy
|
|
134
|
+
* workflow base64-decodes it with `printf ... | base64 -d` into a file and
|
|
135
|
+
* points `$KUBECONFIG` at it. Base64 is used (rather than raw YAML) because
|
|
136
|
+
* the downstream shell interpolation needs a single-line value.
|
|
137
|
+
*/
|
|
138
|
+
export async function uploadKubeconfig(envName, kubeconfigPath) {
|
|
139
|
+
const { readFileSync } = await import('node:fs');
|
|
140
|
+
const raw = readFileSync(kubeconfigPath, 'utf-8');
|
|
141
|
+
const b64 = Buffer.from(raw, 'utf-8').toString('base64');
|
|
142
|
+
await ensureEnvironment(envName);
|
|
143
|
+
await setSecretFromBodyFile('KUBECONFIG_B64', b64, envName);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Trigger the customer's deploy workflow for a given environment and
|
|
148
|
+
* optionally wait for it to complete. `gh workflow run` fires it; the
|
|
149
|
+
* workflow's concurrency group serializes per-env so only one runs at a
|
|
150
|
+
* time.
|
|
151
|
+
*/
|
|
152
|
+
export async function triggerDeployWorkflow(envName) {
|
|
153
|
+
await runCommandAsync(
|
|
154
|
+
['gh', 'workflow', 'run', 'deploy.yml', '--ref', 'main', '-F', `environment=${envName}`],
|
|
155
|
+
{ silent: true },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Block until the most recent run of deploy.yml completes. Polls
|
|
161
|
+
* `gh run list` every 5s; surfaces the final status when done.
|
|
162
|
+
*/
|
|
163
|
+
export async function waitForLatestWorkflowRun(timeoutSec = 1800) {
|
|
164
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
165
|
+
const startedAt = Date.now();
|
|
166
|
+
// Give GitHub a couple seconds to register the run before we poll.
|
|
167
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
168
|
+
// Periodic state snapshot — every 2 min, log which workflow step is
|
|
169
|
+
// currently in-progress. Without this, a 60-minute wait is a black
|
|
170
|
+
// box; with it, the deploy log shows where the workflow spent its
|
|
171
|
+
// time. Captured by the deploy logger (#6) so it persists past
|
|
172
|
+
// teardown. Snapshot every ~24th poll (5s × 24 = 120s) — short enough
|
|
173
|
+
// to catch a mid-step hang, long enough to not spam.
|
|
174
|
+
let lastSnapshotAt = 0;
|
|
175
|
+
while (Date.now() < deadline) {
|
|
176
|
+
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
177
|
+
if (Date.now() - lastSnapshotAt >= 120_000) {
|
|
178
|
+
try {
|
|
179
|
+
const stateOut = await runCommandAsync(
|
|
180
|
+
[
|
|
181
|
+
'gh',
|
|
182
|
+
'run',
|
|
183
|
+
'list',
|
|
184
|
+
'--workflow',
|
|
185
|
+
'deploy.yml',
|
|
186
|
+
'--limit',
|
|
187
|
+
'1',
|
|
188
|
+
'--json',
|
|
189
|
+
'databaseId,status,conclusion,jobs',
|
|
190
|
+
],
|
|
191
|
+
{ silent: true, ignoreError: true },
|
|
192
|
+
);
|
|
193
|
+
if (stateOut) {
|
|
194
|
+
const info = JSON.parse(stateOut.toString())[0];
|
|
195
|
+
const jobs = (info?.jobs ?? [])
|
|
196
|
+
.map((j) => {
|
|
197
|
+
const inFlight =
|
|
198
|
+
j.steps?.find((s) => s.status === 'in_progress')?.name ??
|
|
199
|
+
(j.status === 'completed' ? `(done: ${j.conclusion})` : `(${j.status})`);
|
|
200
|
+
return ` ${j.name}: ${inFlight}`;
|
|
201
|
+
})
|
|
202
|
+
.join('\n');
|
|
203
|
+
console.log(
|
|
204
|
+
`[wait ${elapsed}s] deploy.yml run=${info?.databaseId ?? '?'} status=${info?.status ?? '?'}\n${jobs}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
/* snapshot is best-effort — never break the wait */
|
|
209
|
+
}
|
|
210
|
+
lastSnapshotAt = Date.now();
|
|
211
|
+
}
|
|
212
|
+
const out =
|
|
213
|
+
(
|
|
214
|
+
await runCommandAsync(
|
|
215
|
+
[
|
|
216
|
+
'gh',
|
|
217
|
+
'run',
|
|
218
|
+
'list',
|
|
219
|
+
'--workflow',
|
|
220
|
+
'deploy.yml',
|
|
221
|
+
'--limit',
|
|
222
|
+
'1',
|
|
223
|
+
'--json',
|
|
224
|
+
'status,conclusion,databaseId',
|
|
225
|
+
],
|
|
226
|
+
{ silent: true, ignoreError: true },
|
|
227
|
+
)
|
|
228
|
+
)?.toString() ?? '[]';
|
|
229
|
+
let run;
|
|
230
|
+
try {
|
|
231
|
+
run = JSON.parse(out)[0];
|
|
232
|
+
} catch {
|
|
233
|
+
run = null;
|
|
234
|
+
}
|
|
235
|
+
if (run?.status === 'completed') {
|
|
236
|
+
if (run.conclusion === 'success') return { ok: true, runId: run.databaseId };
|
|
237
|
+
// Fetch the failing-step log inline so the caller has something to
|
|
238
|
+
// act on. The throwaway repo used by e2e tests is destroyed
|
|
239
|
+
// right after a failure; post-mortem `gh run view` then 404s. Even
|
|
240
|
+
// outside e2e, surfacing the actual failure at throw time is
|
|
241
|
+
// far more useful than a "go look at this URL" pointer the operator
|
|
242
|
+
// then has to re-fetch manually. Best-effort — if the log fetch
|
|
243
|
+
// itself fails (403, run not yet indexed), proceed with the short
|
|
244
|
+
// error so we don't hide the original failure.
|
|
245
|
+
let logTail = '';
|
|
246
|
+
try {
|
|
247
|
+
const logOut = await runCommandAsync(
|
|
248
|
+
['gh', 'run', 'view', String(run.databaseId), '--log-failed'],
|
|
249
|
+
{ silent: true, ignoreError: true, timeout: 60_000 },
|
|
250
|
+
);
|
|
251
|
+
if (logOut) {
|
|
252
|
+
const text = logOut.toString();
|
|
253
|
+
// Tail size 20KB — deploy.yml's failure dump (CoreDNS config +
|
|
254
|
+
// CoreDNS logs + CCM logs + node info + pod network probes) can
|
|
255
|
+
// total 10-15KB; a 3KB tail truncates every useful line and
|
|
256
|
+
// leaves only the final error marker.
|
|
257
|
+
const tail = text.length > 20000 ? `…\n${text.slice(-20000)}` : text;
|
|
258
|
+
logTail = `\n--- failing-step log (last 20KB) ---\n${tail}`;
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
/* best-effort */
|
|
262
|
+
}
|
|
263
|
+
throw new Error(
|
|
264
|
+
`deploy.yml run ${run.databaseId} finished with conclusion=${run.conclusion}. ` +
|
|
265
|
+
`Inspect: gh run view ${run.databaseId} --log-failed${logTail}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
269
|
+
}
|
|
270
|
+
// Timeout: grab a log tail of the still-running workflow inline. In
|
|
271
|
+
// e2e runs the throwaway repo is deleted seconds after this
|
|
272
|
+
// throws, so a post-hoc `gh run view` 404s — we need the state now or
|
|
273
|
+
// we fly blind. Best-effort; a dry runId pointer is better than
|
|
274
|
+
// nothing when the log fetch itself fails.
|
|
275
|
+
let runId = null;
|
|
276
|
+
let logTail = '';
|
|
277
|
+
try {
|
|
278
|
+
const out = await runCommandAsync(
|
|
279
|
+
[
|
|
280
|
+
'gh',
|
|
281
|
+
'run',
|
|
282
|
+
'list',
|
|
283
|
+
'--workflow',
|
|
284
|
+
'deploy.yml',
|
|
285
|
+
'--limit',
|
|
286
|
+
'1',
|
|
287
|
+
'--json',
|
|
288
|
+
'databaseId,status',
|
|
289
|
+
],
|
|
290
|
+
{ silent: true, ignoreError: true },
|
|
291
|
+
);
|
|
292
|
+
if (out) {
|
|
293
|
+
const parsed = JSON.parse(out.toString())[0];
|
|
294
|
+
runId = parsed?.databaseId ?? null;
|
|
295
|
+
if (runId) {
|
|
296
|
+
// `gh run view --log` 404s on in-flight runs — the archive doesn't
|
|
297
|
+
// exist until the run completes. For in-flight state, parse the
|
|
298
|
+
// structured jobs JSON: it gives each job's status + per-step
|
|
299
|
+
// conclusion + timing, which is exactly the "which step is hung"
|
|
300
|
+
// signal we need.
|
|
301
|
+
const jobsOut = await runCommandAsync(
|
|
302
|
+
['gh', 'run', 'view', String(runId), '--json', 'status,conclusion,jobs,url,createdAt'],
|
|
303
|
+
{ silent: true, ignoreError: true, timeout: 60_000 },
|
|
304
|
+
);
|
|
305
|
+
if (jobsOut) {
|
|
306
|
+
try {
|
|
307
|
+
const info = JSON.parse(jobsOut.toString());
|
|
308
|
+
const stepSummary = (info.jobs ?? [])
|
|
309
|
+
.map((j) => {
|
|
310
|
+
const steps = (j.steps ?? [])
|
|
311
|
+
.map(
|
|
312
|
+
(s) =>
|
|
313
|
+
` ${s.conclusion ?? s.status} ${s.name}${s.startedAt ? ` (${s.startedAt})` : ''}`,
|
|
314
|
+
)
|
|
315
|
+
.join('\n');
|
|
316
|
+
return ` job: ${j.name} [${j.status}${j.conclusion ? `/${j.conclusion}` : ''}]\n${steps}`;
|
|
317
|
+
})
|
|
318
|
+
.join('\n');
|
|
319
|
+
logTail =
|
|
320
|
+
`\n--- in-flight run ${runId} @ ${info.url ?? '(no url)'} ---\n` +
|
|
321
|
+
` status=${info.status} conclusion=${info.conclusion ?? '(pending)'} started=${info.createdAt}\n` +
|
|
322
|
+
stepSummary;
|
|
323
|
+
} catch {
|
|
324
|
+
/* JSON parse failed — silent */
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
/* best-effort */
|
|
331
|
+
}
|
|
332
|
+
throw new Error(
|
|
333
|
+
`deploy.yml run did not complete within ${timeoutSec}s.${runId ? ` run=${runId}` : ''}${logTail}`,
|
|
334
|
+
);
|
|
335
|
+
}
|