Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion go/Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: install fmt test-unit test-integration test-integration-long test-stress test build generate pull up clean
.PHONY: install tools fmt test-unit test-integration test-integration-long test-stress test build generate pull up clean

# Detect OS and set GOMAXPROCS accordingly
UNAME_S := $(shell uname -s)
Expand All @@ -16,6 +16,11 @@ PARALLEL_PROCS := $(shell if [ $(GOMAXPROCS_VAL) -gt 1 ]; then expr $(GOMAXPROCS
install:
go mod tidy

tools:
go install github.com/bufbuild/buf/cmd/buf@v1.57.0
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0
go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.28.0

fmt:
go fmt ./...
golangci-lint run
Expand Down
71 changes: 58 additions & 13 deletions go/apps/ctrl/services/deployment/create_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"strings"
"time"

"connectrpc.com/connect"
Expand All @@ -13,6 +14,20 @@ import (
"github.com/unkeyed/unkey/go/pkg/uid"
)

// limitString truncates a string to the specified maximum number of runes
func limitString(s string, maxRunes int) string {
runes := []rune(s)
if len(runes) > maxRunes {
return string(runes[:maxRunes])
}
return s
}

// CreateVersion creates a new deployment version record and kicks off the deployment workflow.
// It validates workspace/project, normalizes git metadata (branch fallback, commit fields),
// and persists the deployment in "pending" state. Workflow failures are logged but do not
// fail creation to allow retries.

func (s *Service) CreateVersion(
ctx context.Context,
req *connect.Request[ctrlv1.CreateVersionRequest],
Expand Down Expand Up @@ -53,6 +68,23 @@ func (s *Service) CreateVersion(
}
}

// Validate git commit timestamp if provided (must be Unix epoch milliseconds)
if req.Msg.GetGitCommitTimestamp() != 0 {
timestamp := req.Msg.GetGitCommitTimestamp()
// Reject timestamps that are clearly in seconds format (< 1_000_000_000_000)
// This corresponds to January 1, 2001 in milliseconds
if timestamp < 1_000_000_000_000 {
return nil, connect.NewError(connect.CodeInvalidArgument,
fmt.Errorf("git_commit_timestamp must be Unix epoch milliseconds, got %d (appears to be seconds format)", timestamp))
}
// Also reject future timestamps more than 1 hour ahead (likely invalid)
maxValidTimestamp := time.Now().Add(1 * time.Hour).UnixMilli()
if timestamp > maxValidTimestamp {
return nil, connect.NewError(connect.CodeInvalidArgument,
fmt.Errorf("git_commit_timestamp %d is too far in the future (must be Unix epoch milliseconds)", timestamp))
}
}

// Determine environment (default to preview)
// TODO: Add environment field to CreateVersionRequest proto
environment := db.DeploymentsEnvironmentPreview
Expand All @@ -61,21 +93,34 @@ func (s *Service) CreateVersion(
deploymentID := uid.New("deployment")
now := time.Now().UnixMilli()

// Sanitize input values before persisting
gitCommitSha := req.Msg.GetGitCommitSha()
gitCommitMessage := limitString(req.Msg.GetGitCommitMessage(), 10240)
gitCommitAuthorName := limitString(strings.TrimSpace(req.Msg.GetGitCommitAuthorName()), 256)
gitCommitAuthorUsername := limitString(strings.TrimSpace(req.Msg.GetGitCommitAuthorUsername()), 256)
gitCommitAuthorAvatarUrl := limitString(strings.TrimSpace(req.Msg.GetGitCommitAuthorAvatarUrl()), 512)

// Insert deployment into database
err = db.Query.InsertDeployment(ctx, s.db.RW(), db.InsertDeploymentParams{
ID: deploymentID,
WorkspaceID: req.Msg.GetWorkspaceId(),
ProjectID: req.Msg.GetProjectId(),
Environment: environment,
BuildID: sql.NullString{String: "", Valid: false}, // Build creation handled separately
RootfsImageID: "", // Image handling not implemented yet
GitCommitSha: sql.NullString{String: req.Msg.GetGitCommitSha(), Valid: req.Msg.GetGitCommitSha() != ""},
GitBranch: sql.NullString{String: gitBranch, Valid: true},
ConfigSnapshot: []byte("{}"), // Configuration snapshot placeholder
OpenapiSpec: sql.NullString{String: "", Valid: false},
Status: "pending",
CreatedAt: now,
UpdatedAt: sql.NullInt64{Int64: now, Valid: true},
ID: deploymentID,
WorkspaceID: req.Msg.GetWorkspaceId(),
ProjectID: req.Msg.GetProjectId(),
Environment: environment,
BuildID: sql.NullString{String: "", Valid: false}, // Build creation handled separately
RootfsImageID: "", // Image handling not implemented yet
GitCommitSha: sql.NullString{String: gitCommitSha, Valid: gitCommitSha != ""},
GitBranch: sql.NullString{String: gitBranch, Valid: true},
GitCommitMessage: sql.NullString{String: gitCommitMessage, Valid: req.Msg.GetGitCommitMessage() != ""},
GitCommitAuthorName: sql.NullString{String: gitCommitAuthorName, Valid: req.Msg.GetGitCommitAuthorName() != ""},
// TODO: Use email to lookup GitHub username/avatar via GitHub API instead of persisting PII
GitCommitAuthorUsername: sql.NullString{String: gitCommitAuthorUsername, Valid: req.Msg.GetGitCommitAuthorUsername() != ""},
GitCommitAuthorAvatarUrl: sql.NullString{String: gitCommitAuthorAvatarUrl, Valid: req.Msg.GetGitCommitAuthorAvatarUrl() != ""},
GitCommitTimestamp: sql.NullInt64{Int64: req.Msg.GetGitCommitTimestamp(), Valid: req.Msg.GetGitCommitTimestamp() != 0},
ConfigSnapshot: []byte("{}"), // Configuration snapshot placeholder
OpenapiSpec: sql.NullString{String: "", Valid: false},
Status: db.DeploymentsStatusPending,
CreatedAt: now,
UpdatedAt: sql.NullInt64{Int64: now, Valid: true},
})
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
Expand Down
Loading