Skip to content

feat: GitHub webhook deployments with BuildKit native git context#4876

Merged
chronark merged 32 commits intomainfrom
github-webhook
Feb 3, 2026
Merged

feat: GitHub webhook deployments with BuildKit native git context#4876
chronark merged 32 commits intomainfrom
github-webhook

Conversation

@chronark
Copy link
Collaborator

@chronark chronark commented Jan 29, 2026

Summary

Implements GitHub push webhook handling that triggers deployments using BuildKit native git context support. This replaces the previous S3-based source upload approach with a simpler architecture where BuildKit fetches directly from GitHub.

Architecture

GitHub Push → ctrl-api webhook → Create deployment record → Restate Deploy workflow
                                                                    ↓
                                              Depot BuildKit ← git context URL + token
                                                    ↓
                                              Container Registry → Deploy

Key Changes

New GitHub Deployment Flow

  • svc/ctrl/api/github_webhook.go - Receives push events, verifies signatures, creates deployment records, triggers Deploy workflow
  • svc/ctrl/worker/deploy/build.go - buildDockerImageFromGit() uses BuildKit native git context with GIT_AUTH_TOKEN.github.com secret
  • svc/ctrl/worker/github/client.go - GitHub App authentication for installation tokens

Simplified API

  • Removed S3 upload endpoints (generateUploadUrl, completeUpload)
  • createDeployment API now only accepts pre-built Docker images
  • Git-based builds only triggered via GitHub webhook integration

CLI Updates

  • cmd/deploy simplified to only support --docker-image flag
  • Removed --context and --dockerfile flags
  • Users who want source builds should connect GitHub repos

Config Cleanup

  • Removed unused env vars: UNKEY_CLICKHOUSE_URL (ctrl-api), UNKEY_REPOFETCH_IMAGE, UNKEY_VAULT_* (ctrl-api)
  • Removed empty GitHubConfig struct from ctrl-api

How BuildKit Git Context Works

BuildKit fetches repos directly using URL format:

https://github.com/owner/repo.git#<commit-sha>:<subdir>

Authentication for private repos via BuildKit secret provider:

secretsprovider.FromMap(map[string][]byte{
    "GIT_AUTH_TOKEN.github.com": []byte(githubToken),
})

Documentation

Added engineering docs at web/apps/engineering/content/docs/architecture/workflows/github-deployments.mdx

chronark and others added 16 commits January 26, 2026 19:13
Implements RFC 7519 JSON Web Tokens with algorithm-pinned security design.
Each verifier is constructed for a specific algorithm, preventing classic
JWT vulnerabilities where attackers forge signatures via algorithm confusion.

Supported algorithms:
- HS256 (HMAC-SHA256) for symmetric signing
- RS256 (RSA PKCS#1 v1.5 with SHA-256) for asymmetric signing

Features:
- Generic type parameters for compile-time claims type safety
- RegisteredClaims struct with standard JWT fields (iss, sub, aud, exp, nbf, iat, jti)
- Automatic Claims interface satisfaction via Go method promotion
- Constant-time signature comparison for HS256
- Pluggable clock for deterministic testing
- PEM key parsing with escaped newline handling

The package is interoperable with github.com/golang-jwt/jwt/v5 and other
standard JWT libraries.

Amp-Thread-ID: https://ampcode.com/threads/T-019c0899-274b-723e-882a-1294d2d219e0
Co-authored-by: Amp <amp@ampcode.com>
Adds comprehensive fuzz testing for the JWT package:

hs256_fuzz_test.go:
- Sign/verify roundtrip with random claims and secrets
- Arbitrary input handling (no panics)
- Signature, payload, and header manipulation detection
- Cross-secret verification rejection
- Time boundary validation

rs256_fuzz_test.go:
- Sign/verify roundtrip with random claims
- Arbitrary input handling (no panics)
- Signature, payload, and header manipulation detection
- Cross-key verification rejection
- Time boundary validation

claims_fuzz_test.go:
- RegisteredClaims.Validate with random exp/nbf/time combinations
- Edge cases (zero, negative, max int64 timestamps)
- Malformed token structure handling
- Algorithm confusion prevention

Uses pkg/fuzz for deterministic fuzzer input consumption.

Amp-Thread-ID: https://ampcode.com/threads/T-019c0899-274b-723e-882a-1294d2d219e0
Co-authored-by: Amp <amp@ampcode.com>
@vercel
Copy link

vercel bot commented Jan 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
dashboard Ignored Ignored Preview Feb 3, 2026 0:42am
engineering Ignored Ignored Preview Feb 3, 2026 0:42am

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

This PR transitions the deployment system from S3-backed build-from-source workflows to Docker image-based deployments with GitHub App integration. It removes build storage dependencies, adds GitHub webhook handling for push events, introduces repository connection database schema, and refactors the worker deployment service to support git-based builds via GitHub.

Changes

Cohort / File(s) Summary
Deployment API & Routes
svc/api/openapi/gen.go, svc/api/openapi/openapi-*.yaml, svc/api/openapi/spec/paths/v2/deploy/createDeployment/*, svc/api/openapi/spec/paths/v2/deploy/generateUploadUrl/*, svc/api/routes/v2_deploy_create_deployment/*, svc/api/routes/v2_deploy_generate_upload_url/*, svc/api/routes/BUILD.bazel, svc/api/routes/register.go, svc/api/openapi/BUILD.bazel
Removed S3 presigned upload URL flow (generateUploadUrl endpoint and related types). Simplified CreateDeploymentRequestBody to use direct DockerImage field instead of union-based source selection. Deleted associated OpenAPI schema files and route handlers.
GitHub Webhook Handler
svc/ctrl/api/github_webhook.go, svc/ctrl/api/github_webhook_integration_test.go, svc/ctrl/api/harness_test.go, svc/ctrl/api/deployment_integration_test.go, svc/ctrl/api/run.go, svc/ctrl/api/certificate.go, svc/ctrl/api/BUILD.bazel, svc/ctrl/api/config.go
Added GitHubWebhook handler for push and installation events with signature verification. Integrates with repository connection database, creates deployments, and triggers Restate workflows. Adds GitHubWebhookSecret to API config and conditional webhook registration.
GitHub Client & Authentication
svc/ctrl/worker/github/client.go, svc/ctrl/worker/github/doc.go, svc/ctrl/worker/github/BUILD.bazel
New GitHub App client for JWT-based authentication, installation token retrieval, tarball downloads, and webhook signature verification.
Database Schema & Generated Code
pkg/db/github_repo_connection_*.sql_generated.go, pkg/db/bulk_github_repo_connection_insert.sql_generated.go, pkg/db/querier_*.generated.go, pkg/db/queries/github_repo_connection_*.sql, pkg/db/BUILD.bazel, pkg/db/schema.sql, web/internal/db/src/schema/github_app.ts
Added github_repo_connections table and generated sqlc query/insert methods. Includes bulk insert, find by installation/repo, and index on installation_id.
S3 & Build Storage Removal
svc/ctrl/pkg/build/*, cmd/ctrl/api.go, cmd/ctrl/worker.go, svc/ctrl/services/deployment/create_s3_upload_url.go, svc/ctrl/services/deployment/service.go, svc/ctrl/services/deployment/BUILD.bazel, dev/k8s/manifests/ctrl-api.yaml, dev/k8s/manifests/ctrl-worker.yaml, svc/api/internal/testutil/mock_deployment_client.go
Removed build-s3-* flags and S3Config from API and worker commands. Deleted Depot build service implementation. Removed CreateS3UploadURL from mock and deployment service. Updated k8s manifests to remove S3 environment variables.
Worker Configuration & Deployment
svc/ctrl/worker/config.go, svc/ctrl/worker/run.go, svc/ctrl/worker/deploy/service.go, svc/ctrl/worker/deploy/deploy_handler.go, svc/ctrl/worker/deploy/build.go, svc/ctrl/worker/deploy/BUILD.bazel, svc/ctrl/worker/BUILD.bazel
Replaced BuildS3 config with GitHub configuration (AppID, PrivateKeyPEM). Refactored deployment workflow to support both DockerImage and GitSource variants. Added git-based build support via buildDockerImageFromGit with GitHub token retrieval and BuildKit integration.
Deployment Proto Definitions
svc/ctrl/proto/ctrl/v1/deployment.proto, svc/ctrl/proto/hydra/v1/deployment.proto, svc/ctrl/proto/hydra/v1/build.proto
Simplified CreateDeploymentRequest to use single docker_image field instead of oneof source. Added GitSource and DockerImage messages in Hydra deployment proto. Extended GitCommitInfo with author metadata. Removed BuildService from Hydra proto.
Test Infrastructure
pkg/dockertest/mysql.go, pkg/dockertest/restate.go, pkg/dockertest/restate_test.go, pkg/dockertest/docker.go, pkg/dockertest/doc.go, pkg/dockertest/BUILD.bazel, svc/api/cancel_test.go, svc/ctrl/integration/harness.go, svc/ctrl/integration/BUILD.bazel, web/apps/contributing/testing/integration-tests.mdx
Migrated from testutil/containers to dockertest for MySQL and added Restate test container support. Added HostURL helper for container port mapping. Updated integration test harnesses to use dockertest.
Configuration & Examples
dev/.env.github.example, web/apps/dashboard/lib/env.ts, web/apps/dashboard/lib/github.ts, web/apps/dashboard/lib/trpc/routers/github.ts, Makefile, dev/BUILD.bazel, dev/Tiltfile, dev/linters/exhaustruct/analyzer.go
Added GitHub App environment variables and webhook secret. Updated dashboard to parse GITHUB_APP_ID as integer and use UNKEY_GITHUB_PRIVATE_KEY_PEM. Added dashboard dev resource to Tiltfile. Updated integration test documentation and build artifacts.
Documentation
web/apps/engineering/content/docs/architecture/workflows/github-deployments.mdx, web/apps/engineering/content/docs/architecture/workflows/meta.json, svc/ctrl/api/doc.go, svc/ctrl/worker/doc.go, svc/ctrl/worker/certificate/bootstrap_infra_certs.go, svc/ctrl/worker/customdomain/doc.go, svc/ctrl/worker/certificate/process_challenge_handler.go, svc/ctrl/worker/clickhouseuser/configure_user_handler.go, svc/ctrl/worker/deploy/domains.go, svc/ctrl/worker/routing/service.go, svc/ctrl/api/types.go
Added comprehensive GitHub deployments workflow documentation. Updated service docs to generalize architecture descriptions. Added helper methods for certificate and domain handling with inline documentation.

Sequence Diagram(s)

sequenceDiagram
    participant GitHub as GitHub App
    participant Webhook as GitHubWebhook Handler
    participant DB as Database
    participant Restate as Restate Service
    participant Workflow as Deploy Workflow

    GitHub->>Webhook: POST /webhooks/github (push event)
    Webhook->>Webhook: Verify X-Hub-Signature-256
    Webhook->>DB: Find repo connection by installation_id
    Webhook->>DB: Resolve project & environment
    Webhook->>DB: Create deployment record (pending, git metadata)
    Webhook->>Restate: Submit DeployRequest with GitSource
    Restate->>Workflow: Trigger Deploy workflow
    Webhook->>GitHub: 200 OK
    
    Workflow->>Workflow: Parse GitSource (installation_id, repository, commit_sha)
    Workflow->>Workflow: Fetch GitHub installation token
    Workflow->>Workflow: Build Docker image via BuildKit + git context
    Workflow->>DB: Update deployment with image reference
    Workflow->>Restate: Trigger routing setup
    Workflow->>DB: Mark deployment ready
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • #4583: Implements corresponding API-side changes to the deployment request/response schema and route handler wiring for the simplified DockerImage-based deployment model.
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: GitHub webhook deployments with BuildKit native git context' clearly and specifically describes the main feature addition, focusing on the GitHub webhook deployment integration with BuildKit git context support.
Description check ✅ Passed The PR description provides comprehensive information covering the summary, architecture diagram, key changes across multiple files, API/CLI updates, configuration cleanup, BuildKit implementation details, and documentation additions. It addresses the core purpose and intent of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch github-webhook

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel vercel bot temporarily deployed to Preview – engineering January 29, 2026 15:41 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard January 29, 2026 15:41 Inactive
@chronark chronark changed the base branch from main to jwt January 29, 2026 16:02
Base automatically changed from jwt to main January 30, 2026 07:33
@vercel vercel bot temporarily deployed to Preview – engineering January 30, 2026 12:16 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard January 30, 2026 12:17 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard February 3, 2026 07:38 Inactive
@chronark chronark marked this pull request as ready for review February 3, 2026 09:14
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
svc/ctrl/worker/deploy/deploy_handler.go (1)

325-340: ⚠️ Potential issue | 🟡 Minor

Use txCtx inside transaction

Keep txn context consistent.

✅ Minimal fix
-						err = db.Query.InsertFrontlineRoute(runCtx, tx, db.InsertFrontlineRouteParams{
+						err = db.Query.InsertFrontlineRoute(txCtx, tx, db.InsertFrontlineRouteParams{
🤖 Fix all issues with AI agents
In `@cmd/ctrl/api.go`:
- Around line 81-82: The GitHub webhook secret flag ("github-app-webhook-secret"
/ env UNKEY_GITHUB_APP_WEBHOOK_SECRET) is currently allowed to be empty which
permits trivial HMAC forgery; update the CLI flag or startup validation to
enforce a non-empty secret: either mark the flag as required in the CLI flag
declaration (e.g., set the Required property on the String flag) or, in the
service bootstrap where the flag value is read (the code that reads the
"github-app-webhook-secret" value), validate that the value is not empty and log
an error and exit if it is. Ensure the check references the exact flag name
("github-app-webhook-secret") so the server will fail fast when the secret is
missing.

In `@pkg/dockertest/mysql.go`:
- Around line 68-75: The code hardcodes dockerCfg.Addr = "mysql:3306", which
won't resolve on Docker's default bridge; either wire up a user-defined network
in startContainer and set the container's network alias dynamically before
assigning dockerCfg.Addr to that alias:3306, or stop exporting/using DockerDSN
and remove the dockerCfg.Addr assignment (and related docs) so MySQLConfig
doesn't promise container DNS resolution; locate the dockerCfg.Addr assignment
in the MySQL config initialization and the container creation logic in
startContainer to implement one of these two fixes.

In `@svc/ctrl/api/github_webhook.go`:
- Around line 173-181: The current handler uses
db.Query.FindEnvironmentByProjectIdAndSlug and returns a 500 for any error which
causes GitHub to retry when the environment is simply missing; modify the error
handling in that call so that when the FindEnvironmentByProjectIdAndSlug returns
a “not found” error (e.g., sql.ErrNoRows or your repository's NotFound sentinel)
you treat it as a successful no-op: log at debug/info that the preview
environment is absent and respond with http.StatusOK (or simply return without
calling http.Error), while preserving the existing s.logger.Error +
http.StatusInternalServerError behavior for any other unexpected errors;
reference the call site that passes project.WorkspaceID, project.ID and envSlug
to locate the code to change.

In `@svc/ctrl/api/harness_test.go`:
- Around line 115-121: The current test spawns a goroutine that calls
require.NoError, which is unsafe because require.FailNow must run in the test
goroutine; instead capture the error from Run into a channel and assert in the
main test goroutine. Replace the goroutine's require.NoError call by launching
Run(ctrlCtx, apiConfig) inside the goroutine and sending its returned error (or
nil) on an errCh, then in the main goroutine receive from errCh and call
require.NoError(t, err). Keep existing ctx cancellation (ctrlCancel) and
t.Cleanup as-is to ensure cleanup.

In `@svc/ctrl/worker/deploy/build.go`:
- Around line 60-190: Validate the provided commit SHA in
buildDockerImageFromGit and fail fast if it's not a full 40‑character SHA: check
params.CommitSHA length == 40 and that it contains only hex characters (0-9,
a-f, A-F) before proceeding (e.g., before building gitContextURL or invoking
w.github.GetInstallationToken); if invalid return a clear error (e.g., "invalid
commit SHA: must be 40 hex characters") so the build is aborted early. Use the
params.CommitSHA symbol and place the check in the buildDockerImageFromGit
function prior to building the gitContextURL and starting the build.

In `@svc/ctrl/worker/deploy/deploy_handler.go`:
- Around line 126-146: The Docker image string isn't validated: in the type
switch handling req.GetSource().(type) ensure the dockerImage obtained from the
*hydrav1.DeployRequest_DockerImage branch is non-empty and return an error
(e.g., formatted with context) if empty to fail fast; likewise after calling
w.buildDockerImageFromGit check build.ImageName is non-empty before assigning to
dockerImage and return a clear error if it is empty. Target the dockerImage
variable, the *hydrav1.DeployRequest_DockerImage case, and the
w.buildDockerImageFromGit / build.ImageName result for these checks.

In `@svc/ctrl/worker/github/client.go`:
- Around line 68-125: The method GetInstallationToken currently constructs and
logs a token preview (tokenPreview / "jwt_preview") which exposes sensitive JWT
data; remove the token preview generation and the logger.Info call that logs
"Generated JWT for GitHub API" and the "jwt_preview" field. Keep generateJWT()
and use the returned token for the Authorization header, but do not create or
log any substring of the token anywhere in GetInstallationToken (remove the
tokenPreview variable and its usage).

In `@svc/ctrl/worker/github/doc.go`:
- Around line 1-5: Update the package doc comment in the github package (doc.go)
to remove references to S3 upload and instead describe the actual flow: GitHub
push → webhook → creation of a deployment record → invoking the Restate Deploy
workflow (via the GitHubService/DeploymentService orchestration) → BuildKit
fetching git context natively → pushing built images to the container registry;
edit the top-of-file comment to replace "tarball download, S3 upload" with this
sequence and mention BuildKit git context and container registry as the artifact
destination.

In `@web/apps/dashboard/lib/env.ts`:
- Line 86: GITHUB_APP_ID currently uses .transform((s) => Number.parseInt(s,
10)) which yields NaN for invalid inputs; replace that transform with a proper
zod coercion or piping so invalid values error: e.g. use z.coerce.number().int()
(or z.string().pipe(z.number().int())) for GITHUB_APP_ID to ensure non-numeric
or empty strings fail validation and produce a clear validation error.

In `@web/apps/dashboard/lib/trpc/routers/github.ts`:
- Around line 145-151: The outer .catch() that wraps the call to
fetchProjectInstallation replaces the original TRPCError thrown inside
fetchProjectInstallation and masks its message; remove the outer catch block so
the TRPCError from fetchProjectInstallation (which includes "Failed to load
GitHub installation") can propagate, or alternatively remove the inner catch in
fetchProjectInstallation—prefer removing the outer .catch() around the
fetchProjectInstallation(...) call so the original error and stack are preserved
and surfaced by the existing TRPCError.
🧹 Nitpick comments (9)
svc/ctrl/services/deployment/create_deployment_simple_test.go (1)

95-126: Wrap single-scenario tests with t.Run.
Guideline wants t.Run for scenarios.

Proposed fix
 func TestGitFieldValidation_NullHandling(t *testing.T) {
-	t.Parallel()
-
-	// Test empty protobuf fields
-	gitCommit := &ctrlv1.GitCommitInfo{
-		CommitMessage:   "",
-		AuthorHandle:    "",
-		AuthorAvatarUrl: "",
-		Timestamp:       0,
-	}
+	t.Run("null_handling", func(t *testing.T) {
+		t.Parallel()
+
+		// Test empty protobuf fields
+		gitCommit := &ctrlv1.GitCommitInfo{
+			CommitMessage:   "",
+			AuthorHandle:    "",
+			AuthorAvatarUrl: "",
+			Timestamp:       0,
+		}
 
-	// Empty strings should be returned as-is
-	require.Equal(t, "", gitCommit.GetCommitMessage())
-	require.Equal(t, "", gitCommit.GetAuthorHandle())
-	require.Equal(t, "", gitCommit.GetAuthorAvatarUrl())
-	require.Equal(t, int64(0), gitCommit.GetTimestamp())
+		// Empty strings should be returned as-is
+		require.Equal(t, "", gitCommit.GetCommitMessage())
+		require.Equal(t, "", gitCommit.GetAuthorHandle())
+		require.Equal(t, "", gitCommit.GetAuthorAvatarUrl())
+		require.Equal(t, int64(0), gitCommit.GetTimestamp())
 
-	// Test NULL database fields
-	deployment := db.Deployment{
-		GitCommitMessage:         sql.NullString{Valid: false},
-		GitCommitAuthorHandle:    sql.NullString{Valid: false},
-		GitCommitAuthorAvatarUrl: sql.NullString{Valid: false},
-		GitCommitTimestamp:       sql.NullInt64{Valid: false},
-	}
+		// Test NULL database fields
+		deployment := db.Deployment{
+			GitCommitMessage:         sql.NullString{Valid: false},
+			GitCommitAuthorHandle:    sql.NullString{Valid: false},
+			GitCommitAuthorAvatarUrl: sql.NullString{Valid: false},
+			GitCommitTimestamp:       sql.NullInt64{Valid: false},
+		}
 
-	// NULL fields should be invalid
-	require.False(t, deployment.GitCommitMessage.Valid)
-	require.False(t, deployment.GitCommitAuthorHandle.Valid)
-	require.False(t, deployment.GitCommitAuthorAvatarUrl.Valid)
-	require.False(t, deployment.GitCommitTimestamp.Valid)
+		// NULL fields should be invalid
+		require.False(t, deployment.GitCommitMessage.Valid)
+		require.False(t, deployment.GitCommitAuthorHandle.Valid)
+		require.False(t, deployment.GitCommitAuthorAvatarUrl.Valid)
+		require.False(t, deployment.GitCommitTimestamp.Valid)
+	})
 }
 func TestTimestampConversion(t *testing.T) {
-	t.Parallel()
-
-	// Test fixed timestamp for deterministic testing
-	now := time.Date(2024, 8, 21, 14, 30, 45, 123456789, time.UTC)
-	nowMillis := now.UnixMilli()
-
-	// Test protobuf timestamp
-	gitCommit := &ctrlv1.GitCommitInfo{
-		Timestamp: nowMillis,
-	}
-	require.Equal(t, nowMillis, gitCommit.GetTimestamp())
-
-	// Test database timestamp
-	deployment := db.Deployment{
-		GitCommitTimestamp: sql.NullInt64{Int64: nowMillis, Valid: true},
-	}
-	require.Equal(t, nowMillis, deployment.GitCommitTimestamp.Int64)
-	require.True(t, deployment.GitCommitTimestamp.Valid)
-
-	// Test conversion back to time
-	retrievedTime := time.UnixMilli(deployment.GitCommitTimestamp.Int64)
-	require.Equal(t, now.Unix(), retrievedTime.Unix()) // Compare at second precision
+	t.Run("timestamp_conversion", func(t *testing.T) {
+		t.Parallel()
+
+		// Test fixed timestamp for deterministic testing
+		now := time.Date(2024, 8, 21, 14, 30, 45, 123456789, time.UTC)
+		nowMillis := now.UnixMilli()
+
+		// Test protobuf timestamp
+		gitCommit := &ctrlv1.GitCommitInfo{
+			Timestamp: nowMillis,
+		}
+		require.Equal(t, nowMillis, gitCommit.GetTimestamp())
+
+		// Test database timestamp
+		deployment := db.Deployment{
+			GitCommitTimestamp: sql.NullInt64{Int64: nowMillis, Valid: true},
+		}
+		require.Equal(t, nowMillis, deployment.GitCommitTimestamp.Int64)
+		require.True(t, deployment.GitCommitTimestamp.Valid)
+
+		// Test conversion back to time
+		retrievedTime := time.UnixMilli(deployment.GitCommitTimestamp.Int64)
+		require.Equal(t, now.Unix(), retrievedTime.Unix()) // Compare at second precision
+	})
 }

Also applies to: 128-152

web/apps/dashboard/lib/github.ts (1)

69-69: Consider adding type safety to JSON response parsing.

response.json() returns Promise<any>. Using a Zod schema or explicit type guard would ensure the response structure matches InstallationAccessToken.

pkg/dockertest/mysql.go (1)

85-89: Add validation when schema path cannot be determined.

schemaSQLPath() returns empty string on failure (line 114), causing os.ReadFile("") to fail with an unclear "no such file or directory" error.

♻️ Proposed fix for clearer error
 	schemaPath := schemaSQLPath()
+	require.NotEmpty(t, schemaPath, "could not locate schema.sql - ensure TEST_SRCDIR/TEST_WORKSPACE are set or run from repo root")
 	schemaBytes, err := os.ReadFile(schemaPath)
 	require.NoError(t, err)
svc/ctrl/api/github_webhook_integration_test.go (1)

24-67: Use t.Run subtests for scenarios

Keeps scenario structure consistent.

♻️ Minimal refactor
-func TestGitHubWebhook_Push_TriggersDeployWorkflow(t *testing.T) {
-	// existing body
-}
-
-func TestGitHubWebhook_InvalidSignature(t *testing.T) {
-	// existing body
-}
+func TestGitHubWebhook(t *testing.T) {
+	t.Run("push triggers deploy workflow", func(t *testing.T) {
+		// existing TestGitHubWebhook_Push_TriggersDeployWorkflow body
+	})
+	t.Run("invalid signature", func(t *testing.T) {
+		// existing TestGitHubWebhook_InvalidSignature body
+	})
+}
As per coding guidelines, **/*_test.go: Organize tests using t.Run() for scenarios.
svc/api/routes/v2_deploy_create_deployment/handler.go (1)

90-96: Consider early validation of DockerImage.

The handler passes req.DockerImage directly without validation. While the ctrl service validates this (lines 48-52 of create_deployment.go), validating at the API boundary provides faster feedback and clearer error messages to API consumers.

🛡️ Suggested validation
+	if req.DockerImage == "" {
+		return fault.New("docker_image is required",
+			fault.Code(codes.App.BadRequest.URN()),
+			fault.Public("docker_image is required for deployment"),
+		)
+	}
+
 	// nolint: exhaustruct // optional proto fields, only setting whats provided
 	ctrlReq := &ctrlv1.CreateDeploymentRequest{
svc/ctrl/services/deployment/create_deployment.go (1)

245-252: trimLength may corrupt multi-byte UTF-8 characters.

The function operates on bytes, not runes, which can split multi-byte UTF-8 sequences (e.g., emoji, CJK characters) and produce invalid UTF-8. This could cause display issues or parsing errors downstream.

♻️ Rune-safe alternative
 func trimLength(s string, characters int) string {
-	if len(s) > characters {
-		return s[:characters]
+	runes := []rune(s)
+	if len(runes) > characters {
+		return string(runes[:characters])
 	}
 	return s
 }
svc/ctrl/worker/deploy/service.go (1)

12-16: Add documentation to clarify BuildPlatform field relationship.

The Architecture field is intentionally derived from Platform during parsing (in config layer), and both are stored for convenience—consumers get the parsed component without re-parsing the platform string. Add a doc comment to the struct in deploy/service.go explaining this relationship to prevent confusion:

// BuildPlatform specifies the target platform for container builds.
// Architecture is parsed from Platform (e.g., "amd64" from "linux/amd64")
// and stored for convenient access by build backends.
type BuildPlatform struct {
	Platform     string  // Full OCI platform spec, e.g., "linux/amd64"
	Architecture string  // Extracted from Platform, e.g., "amd64"
}
cmd/ctrl/worker.go (1)

98-101: Add GitHub config validation to Validate() method.

Both github-app-id and github-private-key-pem are optional, but if one is provided without the other, GitHub-triggered builds will silently fail at runtime. The GitHubConfig.Enabled() helper already demonstrates the intent that both must be set together.

Add validation to svc/ctrl/worker/config.go Validate() method to ensure either both are set or neither:

Suggested validation
if (c.GitHub.AppID != 0) != (c.GitHub.PrivateKeyPEM != "") {
    return fmt.Errorf("github-app-id and github-private-key-pem must both be set or both be empty")
}
svc/api/routes/v2_deploy_create_deployment/400_test.go (1)

41-56: Tests for "missing dockerImage" and "empty dockerImage" may be redundant.

Both test cases result in an empty DockerImage string being sent (Go zero value vs explicit ""). If the handler treats them identically, consider consolidating. If there's a semantic difference (omitted vs explicit empty), this is fine.

Also applies to: 194-209

@vercel vercel bot temporarily deployed to Preview – dashboard February 3, 2026 12:02 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard February 3, 2026 12:08 Inactive
@vercel vercel bot temporarily deployed to Preview – dashboard February 3, 2026 12:14 Inactive
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
svc/ctrl/api/certificate.go (2)

25-31: ⚠️ Potential issue | 🟡 Minor

Make startup wait ctx-aware.
Sleep blocks shutdown. Use ctx.Done or timer.

Suggested fix
 func (c *certificateBootstrap) run(ctx context.Context) {
 	// Wait for services to be ready
-	time.Sleep(5 * time.Second)
+	timer := time.NewTimer(5 * time.Second)
+	defer timer.Stop()
+	select {
+	case <-ctx.Done():
+		return
+	case <-timer.C:
+	}

67-106: ⚠️ Potential issue | 🟠 Major

Make domain + challenge insert atomic.
If challenge insert fails, domain stays verified but no ACME record. Wrap in tx or add compensating delete.

🤖 Fix all issues with AI agents
In `@dev/.env.github.example`:
- Around line 7-18: The multiline PEM in UNKEY_GITHUB_PRIVATE_KEY_PEM breaks
dotenv parsing; replace it with a single-line, \n-escaped PEM (or point to a
file path) and ensure the value is not wrapped in raw newlines, and remove
surrounding quotes from NEXT_PUBLIC_GITHUB_APP_NAME so it is unquoted; update
any relevant references to UNKEY_GITHUB_PRIVATE_KEY_PEM and
UNKEY_GITHUB_APP_WEBHOOK_SECRET to read the escaped string (or file path) at
runtime and keep NEXT_PUBLIC_GITHUB_APP_NAME as an unquoted identifier.

In `@svc/ctrl/api/doc.go`:
- Around line 9-14: Update the package doc comment in doc.go to remove the
obsolete phrase "build artifact storage" and reflow the sentence so it reads
smoothly (e.g., drop "build artifact storage, and" leaving the list: sentinel
instances, Restate for durable workflow execution, ACME providers for automatic
TLS certificates). Edit the top-of-file package comment that contains the
sentence mentioning "build artifact storage" so the responsibilities reflect
current components (sentinel instances, Restate, ACME providers, and the Connect
RPC services for control plane operations, deployment workflows, ACME
management, OpenAPI specs, and cluster coordination).

In `@svc/ctrl/api/github_webhook.go`:
- Around line 214-218: When Deploy().Send() fails leaving the previously-created
deployment record stuck in pending, update the error path to roll back or mark
that deployment as failed: locate the code that creates the deployment record
(the variable/record created before calling Deploy().Send()) and in the error
branch where you currently log and return (the block handling err from
Deploy().Send()), call the deployment-store update method (or delete) to set the
deployment status to failed and include the error message, or delete the pending
record entirely; ensure this uses the same deployment ID/variable and the store
methods (e.g., deploymentStore.UpdateStatus or deploymentStore.Delete) so the
pending record is not left orphaned before returning the HTTP 500.

In `@svc/ctrl/api/types.go`:
- Around line 1-27: Change the pushCommit.Timestamp field from string to
time.Time in the pushCommit struct and add import time to enable automatic
RFC3339 unmarshalling at the JSON boundary; then remove the manual timestamp
parsing inside extractGitCommitInfo and update that function to accept/use the
pushCommit.Timestamp (time.Time) directly, handling potential unmarshal errors
by surfacing them from the JSON decode step rather than silently defaulting to
zero. Ensure you update any other uses of pushCommit.Timestamp to the time.Time
type and adjust error handling where extractGitCommitInfo previously parsed
strings.

In `@svc/ctrl/worker/github/client.go`:
- Around line 183-193: The VerifyWebhookSignature function currently accepts an
empty secret which makes signature verification trivial; update
VerifyWebhookSignature to immediately return false if secret is empty (e.g.,
check secret == "" or len(secret) == 0 at the top of the function) before
computing hmacSHA256 or comparing signatures so forged signatures cannot succeed
when no secret is provided.
🧹 Nitpick comments (8)
dev/Tiltfile (1)

232-243: Track dashboard .env changes to trigger restarts.

deps=[] means Tilt won’t notice updates to web/apps/dashboard/.env, but the dev server only reads env at startup. Consider adding the env file to deps so changes trigger a restart automatically.

♻️ Suggested change
-    deps=[],
+    deps=['../web/apps/dashboard/.env'],
pkg/db/schema.sql (1)

647-647: Consider composite index for query optimization.

The FindGithubRepoConnection query filters by both installation_id AND repository_id. A composite index would allow the query to use a single index scan:

-CREATE INDEX `installation_id_idx` ON `github_repo_connections` (`installation_id`);
+CREATE INDEX `installation_id_repository_id_idx` ON `github_repo_connections` (`installation_id`, `repository_id`);

The single-column index works but the composite index is optimal for this query pattern.

svc/ctrl/api/BUILD.bazel (1)

44-68: Consider tagging these as integration/large tests.
Given the dockertest dependency, it may help to prevent these from running in default unit test suites.

♻️ Suggested tweak
 go_test(
     name = "api_test",
+    size = "large",
+    tags = ["integration"],
     srcs = [
         "deployment_integration_test.go",
         "github_webhook_integration_test.go",
         "harness_test.go",
     ],
svc/ctrl/worker/certificate/process_challenge_handler.go (2)

247-248: Remove unused parameter.

The workspaceId parameter (second positional) is unused (_). Consider removing it from the signature if not needed.

Proposed fix
-func (s *Service) obtainCertificate(ctx context.Context, _ string, dom db.CustomDomain, domain string) (EncryptedCertificate, error) {
+func (s *Service) obtainCertificate(ctx context.Context, dom db.CustomDomain, domain string) (EncryptedCertificate, error) {

Update the call site at line 108 accordingly.


348-360: Discarded error may hide DB issues.

Both the outer and inner errors are silently discarded (_, _ = restate.Run). While the intent is compensation cleanup, consider logging the outer Run error as well if it fails.

Proposed fix
 func (s *Service) markChallengeFailed(ctx restate.ObjectContext, domainID string) {
-	_, _ = restate.Run(ctx, func(stepCtx restate.RunContext) (restate.Void, error) {
+	if _, err := restate.Run(ctx, func(stepCtx restate.RunContext) (restate.Void, error) {
 		if updateErr := db.Query.UpdateAcmeChallengeStatus(stepCtx, s.db.RW(), db.UpdateAcmeChallengeStatusParams{
 			DomainID:  domainID,
 			Status:    db.AcmeChallengesStatusFailed,
 			UpdatedAt: sql.NullInt64{Valid: true, Int64: time.Now().UnixMilli()},
 		}); updateErr != nil {
 			s.logger.Error("failed to update challenge status", "error", updateErr, "domain_id", domainID)
 		}
 		return restate.Void{}, nil
-	}, restate.WithName("mark failed"))
+	}, restate.WithName("mark failed")); err != nil {
+		s.logger.Error("restate run failed for mark failed", "error", err, "domain_id", domainID)
+	}
 }
svc/ctrl/worker/config.go (1)

339-347: Validate GitHub config all-or-nothing.
Partial config passes today; fail fast for clearer startup errors.

🔧 Suggested change
 	if err := assert.NotEmpty(c.ClickhouseURL, "ClickhouseURL is required"); err != nil {
 		return err
 	}
+
+	if c.GitHub.AppID != 0 || c.GitHub.PrivateKeyPEM != "" {
+		if err := assert.All(
+			assert.NotNilAndNotZero(c.GitHub.AppID, "github app id is required"),
+			assert.NotEmpty(c.GitHub.PrivateKeyPEM, "github private key pem is required"),
+		); err != nil {
+			return err
+		}
+	}
 
 	// Validate build platform format
 	_, platformErr := parseBuildPlatform(c.BuildPlatform)
svc/ctrl/api/github_webhook.go (1)

209-210: Address TODO: Read context/dockerfile paths from project settings

These hardcoded defaults may not work for all repository structures.

Would you like me to open an issue to track reading ContextPath and DockerfilePath from project settings?

svc/ctrl/services/deployment/create_deployment.go (1)

238-245: trimLength may split multi-byte UTF-8 characters

The comment acknowledges this, but truncating in the middle of a UTF-8 sequence produces invalid strings. Consider using utf8.RuneCountInString or accepting the risk.

Optional: UTF-8 safe truncation
+import "unicode/utf8"
+
 // trimLength truncates s to the specified number of characters (runes).
-func trimLength(s string, characters int) string {
-	if len(s) > characters {
-		return s[:characters]
+func trimLength(s string, maxRunes int) string {
+	if utf8.RuneCountInString(s) <= maxRunes {
+		return s
+	}
+	runes := []rune(s)
+	if len(runes) > maxRunes {
+		return string(runes[:maxRunes])
 	}
 	return s
 }

@linear
Copy link

linear bot commented Feb 3, 2026

@Flo4604 Flo4604 requested review from Flo4604 February 3, 2026 12:53
@chronark chronark merged commit 3fcc1a4 into main Feb 3, 2026
17 checks passed
@chronark chronark deleted the github-webhook branch February 3, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants