From 525579b1b6aefd3903a184364d7427687a3ca8c4 Mon Sep 17 00:00:00 2001 From: Edward J Date: Mon, 5 Jan 2026 20:43:24 -0800 Subject: [PATCH 1/6] Run GCP cd in gcp cloud build --- src/pkg/cli/client/byoc/gcp/byoc.go | 94 ++++----- src/pkg/cli/client/byoc/gcp/byoc_test.go | 4 +- src/pkg/cli/client/byoc/gcp/stream.go | 104 ++++++---- src/pkg/cli/client/byoc/gcp/stream_test.go | 6 +- src/pkg/clouds/gcp/cloudbuild.go | 213 ++++++++++++++++++--- 5 files changed, 300 insertions(+), 121 deletions(-) diff --git a/src/pkg/cli/client/byoc/gcp/byoc.go b/src/pkg/cli/client/byoc/gcp/byoc.go index 798a5ac71..2513b68f2 100644 --- a/src/pkg/cli/client/byoc/gcp/byoc.go +++ b/src/pkg/cli/client/byoc/gcp/byoc.go @@ -14,15 +14,11 @@ import ( "time" "cloud.google.com/go/logging/apiv2/loggingpb" - run "cloud.google.com/go/run/apiv2" - "cloud.google.com/go/run/apiv2/runpb" "cloud.google.com/go/storage" "github.com/DefangLabs/defang/src/pkg" "github.com/DefangLabs/defang/src/pkg/cli/client" "github.com/DefangLabs/defang/src/pkg/cli/client/byoc" "github.com/DefangLabs/defang/src/pkg/cli/compose" - "github.com/DefangLabs/defang/src/pkg/clouds" - "github.com/DefangLabs/defang/src/pkg/clouds/aws/ecs" "github.com/DefangLabs/defang/src/pkg/clouds/gcp" "github.com/DefangLabs/defang/src/pkg/dns" "github.com/DefangLabs/defang/src/pkg/http" @@ -30,13 +26,13 @@ import ( "github.com/DefangLabs/defang/src/pkg/term" "github.com/DefangLabs/defang/src/pkg/types" defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" - "github.com/aws/smithy-go/ptr" "github.com/bufbuild/connect-go" "google.golang.org/api/googleapi" auditpb "google.golang.org/genproto/googleapis/cloud/audit" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" + "gopkg.in/yaml.v3" ) var _ client.Provider = (*ByocGcp)(nil) @@ -186,6 +182,7 @@ func (b *ByocGcp) SetUpCD(ctx context.Context) error { "roles/certificatemanager.owner", // For creating certificates "roles/serviceusage.serviceUsageAdmin", // For allowing cd to Enable APIs "roles/datastore.owner", // For creating firestore database + "roles/logging.logWriter", // For allowing cloudbuild to write logs }); err != nil { return err } @@ -232,20 +229,6 @@ func (b *ByocGcp) SetUpCD(ctx context.Context) error { // 5. Setup Cloud Run Job term.Debugf("Using CD image: %q", b.CDImage) - serviceAccount := path.Base(b.cdServiceAccount) - if err := b.driver.SetupJob(ctx, "defang-cd", serviceAccount, []clouds.Container{ - { - Image: b.CDImage, - Name: ecs.CdContainerName, - Cpus: 2.0, - Memory: 2048_000_000, // 2G - Essential: ptr.Bool(true), - WorkDir: "/app", - }, - }); err != nil { - return err - } - b.setupDone = true return nil } @@ -344,11 +327,18 @@ func (b *ByocGcp) BootstrapCommand(ctx context.Context, req client.BootstrapComm type cdCommand struct { command []string delegateDomain string - envOverride map[string]string + etag types.ETag mode defangv1.DeploymentMode project string } +type CloudBuildStep struct { + Name string `yaml:"name,omitempty"` + Entrypoint string `yaml:"entrypoint,omitempty"` + Args []string `yaml:"args,omitempty"` + Env []string `yaml:"env,omitempty"` +} + func (b *ByocGcp) runCdCommand(ctx context.Context, cmd cdCommand) (string, error) { defangStateUrl := `gs://` + b.bucket pulumiBackendKey, pulumiBackendValue, err := byoc.GetPulumiBackend(defangStateUrl) @@ -384,8 +374,8 @@ func (b *ByocGcp) runCdCommand(ctx context.Context, cmd cdCommand) (string, erro env["DOMAIN"] = "dummy.domain" } - for k, v := range cmd.envOverride { - env[k] = v + if cmd.etag != "" { + env["DEFANG_ETAG"] = cmd.etag } if os.Getenv("DEFANG_PULUMI_DIR") != "" { @@ -401,12 +391,35 @@ func (b *ByocGcp) runCdCommand(ctx context.Context, cmd cdCommand) (string, erro } } - execution, err := b.driver.Run(ctx, gcp.JobNameCD, env, cmd.command...) + var envs []string + for k, v := range env { + envs = append(envs, fmt.Sprintf("%s=%s", k, v)) + } + + steps, err := yaml.Marshal([]CloudBuildStep{ + { + Name: b.CDImage, + Args: cmd.command, + Env: envs, + }, + }) + if err != nil { + return "", err + } + execution, err := b.driver.RunCloudBuild(ctx, gcp.CloudBuildArgs{ + Steps: string(steps), + ServiceAccount: &b.cdServiceAccount, + Tags: []string{ + fmt.Sprintf("%v_%v_%v_%v", b.PulumiStack, cmd.project, "cd", cmd.etag), // For cd logs, consistent with cloud build tagging + "defang-cd", // To indicate this is the actual cd service + }, + }) if err != nil { return "", err } b.cdExecution = execution // term.Printf("CD Execution: %s\n", execution) + return execution, nil } @@ -433,38 +446,7 @@ func (b *ByocGcp) Preview(ctx context.Context, req *defangv1.DeployRequest) (*de } func (b *ByocGcp) GetDeploymentStatus(ctx context.Context) error { - execClient, err := run.NewExecutionsClient(ctx) - if err != nil { - return err - } - defer execClient.Close() - - execution, err := execClient.GetExecution(ctx, &runpb.GetExecutionRequest{Name: b.cdExecution}) - if err != nil { - return err - } - - var completionTime = execution.GetCompletionTime() - if completionTime != nil { - // cd is done - var failedTasks = execution.GetFailedCount() - if failedTasks > 0 { - var msgs []string - for _, condition := range execution.GetConditions() { - if condition.GetType() == "Completed" && condition.GetMessage() != "" { - msgs = append(msgs, condition.GetMessage()) - } - } - - return client.ErrDeploymentFailed{Message: strings.Join(msgs, ",")} - } - - // completed successfully - return io.EOF - } - - // still running - return nil + return b.driver.GetBuildStatus(ctx, b.cdExecution) } func (b *ByocGcp) deploy(ctx context.Context, req *defangv1.DeployRequest, command string) (*defangv1.DeployResponse, error) { @@ -521,7 +503,7 @@ func (b *ByocGcp) deploy(ctx context.Context, req *defangv1.DeployRequest, comma cdCmd := cdCommand{ command: []string{command, payload}, delegateDomain: req.DelegateDomain, - envOverride: map[string]string{"DEFANG_ETAG": etag}, + etag: etag, mode: req.Mode, project: project.Name, } diff --git a/src/pkg/cli/client/byoc/gcp/byoc_test.go b/src/pkg/cli/client/byoc/gcp/byoc_test.go index 671016cd6..38a886b0a 100644 --- a/src/pkg/cli/client/byoc/gcp/byoc_test.go +++ b/src/pkg/cli/client/byoc/gcp/byoc_test.go @@ -74,12 +74,12 @@ func (m MockGcpLogsClient) GetBuildInfo(ctx context.Context, buildId string) (*g } type MockGcpLoggingLister struct { - logEntries []loggingpb.LogEntry + logEntries []*loggingpb.LogEntry } func (m *MockGcpLoggingLister) Next() (*loggingpb.LogEntry, error) { if len(m.logEntries) > 0 { - entry := &m.logEntries[0] + entry := m.logEntries[0] m.logEntries = m.logEntries[1:] return entry, nil } diff --git a/src/pkg/cli/client/byoc/gcp/stream.go b/src/pkg/cli/client/byoc/gcp/stream.go index c64d769c7..aff0657a5 100644 --- a/src/pkg/cli/client/byoc/gcp/stream.go +++ b/src/pkg/cli/client/byoc/gcp/stream.go @@ -3,6 +3,7 @@ package gcp import ( "context" "errors" + "fmt" "io" "path" "regexp" @@ -378,6 +379,7 @@ var cdExecutionNamePattern = regexp.MustCompile(`^defang-cd-[a-z0-9]{5}$`) func getLogEntryParser(ctx context.Context, gcpClient GcpLogsClient) func(entry *loggingpb.LogEntry) ([]*defangv1.TailResponse, error) { envCache := make(map[string]map[string]string) + cdStarted := false return func(entry *loggingpb.LogEntry) ([]*defangv1.TailResponse, error) { if entry == nil { return nil, nil @@ -398,9 +400,12 @@ func getLogEntryParser(ctx context.Context, gcpClient GcpLogsClient) func(entry } var serviceName, etag, host string + var buildTags []string serviceName = entry.Labels["defang-service"] executionName := entry.Labels["run.googleapis.com/execution_name"] - buildTags := entry.Labels["build_tags"] + if entry.Labels["build_tags"] != "" { + buildTags = strings.Split(entry.Labels["build_tags"], ",") + } // Log from service if serviceName != "" { etag = entry.Labels["defang-etag"] @@ -421,7 +426,7 @@ func getLogEntryParser(ctx context.Context, gcpClient GcpLogsClient) func(entry var err error env, err = gcpClient.GetExecutionEnv(ctx, executionName) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get execution environment variables: %w", err) } envCache[executionName] = env } @@ -435,7 +440,7 @@ func getLogEntryParser(ctx context.Context, gcpClient GcpLogsClient) func(entry // use kaniko build job environment to get etag etag = env["DEFANG_ETAG"] host = "pulumi" // Hardcoded to match end condition detector in cmd/cli/command/compose.go - } else if buildTags != "" { + } else if len(buildTags) > 0 { var bt gcp.BuildTag if err := bt.Parse(buildTags); err != nil { return nil, err @@ -443,6 +448,16 @@ func getLogEntryParser(ctx context.Context, gcpClient GcpLogsClient) func(entry serviceName = bt.Service etag = bt.Etag host = "cloudbuild" + if bt.IsDefangCD { + host = "pulumi" + } + // HACK: Detect cd start from cloudbuild logs to skip the cloud build image pulling logs + if strings.HasPrefix(msg, " ** Update started") { + cdStarted = true + } + if !cdStarted { + return nil, nil // Skip cloudbuild logs (like pulling cd image) before cd started + } } else { var err error _, msg, err = LogEntryToString(entry) @@ -478,6 +493,23 @@ func getActivityParser(ctx context.Context, gcpLogsClient GcpLogsClient, waitFor computeEngineRootTriggers := make(map[string]string) + getReadyServicesCompletedResps := func(status string) []*defangv1.SubscribeResponse { + resps := make([]*defangv1.SubscribeResponse, 0, len(readyServices)) + for serviceName, status := range readyServices { + resps = append(resps, &defangv1.SubscribeResponse{ + Name: serviceName, + State: defangv1.ServiceState_DEPLOYMENT_COMPLETED, + Status: status, + }) + } + resps = append(resps, &defangv1.SubscribeResponse{ + Name: defangCD, + State: defangv1.ServiceState_DEPLOYMENT_COMPLETED, + Status: status, + }) + return resps + } + return func(entry *loggingpb.LogEntry) ([]*defangv1.SubscribeResponse, error) { if entry == nil { return nil, nil @@ -575,20 +607,7 @@ func getActivityParser(ctx context.Context, gcpLogsClient GcpLogsClient, waitFor } cdSuccess = true // Report all ready services when CD is successful, prevents cli deploy stop before cd is done - resps := make([]*defangv1.SubscribeResponse, 0, len(readyServices)) - for serviceName, status := range readyServices { - resps = append(resps, &defangv1.SubscribeResponse{ - Name: serviceName, - State: defangv1.ServiceState_DEPLOYMENT_COMPLETED, - Status: status, - }) - } - resps = append(resps, &defangv1.SubscribeResponse{ - Name: defangCD, - State: defangv1.ServiceState_DEPLOYMENT_COMPLETED, - Status: auditLog.GetStatus().GetMessage(), - }) - return resps, nil // Ignore success cd status when we are waiting for service status + return getReadyServicesCompletedResps(auditLog.GetStatus().GetMessage()), nil // Ignore success cd status when we are waiting for service status } else { term.Warnf("unexpected execution name in audit log : %v", executionName) return nil, nil @@ -672,30 +691,43 @@ func getActivityParser(ctx context.Context, gcpLogsClient GcpLogsClient, waitFor return nil, nil } - var state defangv1.ServiceState - status := "" - if entry.Operation.First { - state = defangv1.ServiceState_BUILD_ACTIVATING - } else if entry.Operation.Last { + if bt.IsDefangCD { + if !entry.Operation.Last { // Ignore non-final cloud build event for CD + return nil, nil + } + // When cloud build fails, the last log message is an error message if entry.Severity == logtype.LogSeverity_ERROR { - state = defangv1.ServiceState_BUILD_FAILED - if auditLog.GetStatus() != nil { - status = auditLog.GetStatus().String() + return nil, client.ErrDeploymentFailed{Message: auditLog.GetStatus().GetMessage()} + } + + cdSuccess = true + return getReadyServicesCompletedResps(auditLog.GetStatus().String()), nil + } else { + var state defangv1.ServiceState + status := "" + if entry.Operation.First { + state = defangv1.ServiceState_BUILD_ACTIVATING + } else if entry.Operation.Last { + if entry.Severity == logtype.LogSeverity_ERROR { + state = defangv1.ServiceState_BUILD_FAILED + if auditLog.GetStatus() != nil { + status = auditLog.GetStatus().String() + } + } else { + state = defangv1.ServiceState_BUILD_STOPPING } } else { - state = defangv1.ServiceState_BUILD_STOPPING + state = defangv1.ServiceState_BUILD_RUNNING } - } else { - state = defangv1.ServiceState_BUILD_RUNNING - } - if status == "" { - status = state.String() + if status == "" { + status = state.String() + } + return []*defangv1.SubscribeResponse{{ + Name: bt.Service, + State: state, + Status: status, + }}, nil } - return []*defangv1.SubscribeResponse{{ - Name: bt.Service, - State: state, - Status: status, - }}, nil default: term.Warnf("unexpected resource type : %v", entry.Resource.Type) return nil, nil diff --git a/src/pkg/cli/client/byoc/gcp/stream_test.go b/src/pkg/cli/client/byoc/gcp/stream_test.go index 7308d740f..92468effc 100644 --- a/src/pkg/cli/client/byoc/gcp/stream_test.go +++ b/src/pkg/cli/client/byoc/gcp/stream_test.go @@ -53,10 +53,10 @@ func TestServiceNameRestorer(t *testing.T) { }) } } -func makeMockLogEntries(n int) []loggingpb.LogEntry { - logEntries := make([]loggingpb.LogEntry, n) +func makeMockLogEntries(n int) []*loggingpb.LogEntry { + logEntries := make([]*loggingpb.LogEntry, n) for i := range logEntries { - logEntries[i] = loggingpb.LogEntry{ + logEntries[i] = &loggingpb.LogEntry{ Payload: &loggingpb.LogEntry_TextPayload{ TextPayload: "Log entry number " + strconv.Itoa(i), }, diff --git a/src/pkg/clouds/gcp/cloudbuild.go b/src/pkg/clouds/gcp/cloudbuild.go index c965c278c..f6ad1065f 100644 --- a/src/pkg/clouds/gcp/cloudbuild.go +++ b/src/pkg/clouds/gcp/cloudbuild.go @@ -4,17 +4,51 @@ import ( "context" "errors" "fmt" + "io" + "net/url" "strings" + "time" cloudbuild "cloud.google.com/go/cloudbuild/apiv1/v2" - "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb" + cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb" + "github.com/DefangLabs/defang/src/pkg/cli/client" + "google.golang.org/protobuf/types/known/durationpb" + "gopkg.in/yaml.v3" ) +type MachineType = cloudbuildpb.BuildOptions_MachineType + +const ( + UNSPECIFIED MachineType = cloudbuildpb.BuildOptions_UNSPECIFIED + N1_HIGHCPU_8 MachineType = cloudbuildpb.BuildOptions_N1_HIGHCPU_8 + N1_HIGHCPU_32 MachineType = cloudbuildpb.BuildOptions_N1_HIGHCPU_32 + E2_HIGHCPU_8 MachineType = cloudbuildpb.BuildOptions_E2_HIGHCPU_8 + E2_HIGHCPU_32 MachineType = cloudbuildpb.BuildOptions_E2_HIGHCPU_32 + E2_MEDIUM MachineType = cloudbuildpb.BuildOptions_E2_MEDIUM +) + +const DefangCDBuildTag = "defang-cd" + +type CloudBuildArgs struct { + // Required fields + Source string + Steps string + + // TODO: We should be able to use ETAG from object metadata as ditest in Diff func to determine a new build is necessary + Images []string `pulumi:"images,optional" provider:"replaceOnChanges"` + ServiceAccount *string `pulumi:"serviceAccount,optional" provider:"replaceOnChanges"` + Tags []string `pulumi:"tags,optional"` + MachineType *string `pulumi:"machineType,optional"` + DiskSizeGb *int64 `pulumi:"diskSizeGb,optional"` + Substitutions map[string]string `pulumi:"substitutions,optional"` +} + type BuildTag struct { - Stack string - Project string - Service string - Etag string + Stack string + Project string + Service string + Etag string + IsDefangCD bool } func (bt BuildTag) String() string { @@ -25,22 +59,28 @@ func (bt BuildTag) String() string { } } -func (bt *BuildTag) Parse(tag string) error { - parts := strings.Split(tag, "_") - if len(parts) < 3 || len(parts) > 4 { - return fmt.Errorf("invalid cloudbuild build tags value: %q", tag) - } +func (bt *BuildTag) Parse(tags []string) error { + for _, tag := range tags { + if tag == DefangCDBuildTag { + bt.IsDefangCD = true + continue + } + parts := strings.Split(tag, "_") + if len(parts) < 3 || len(parts) > 4 { + return fmt.Errorf("invalid cloudbuild build tags value: %q", tag) + } - if len(parts) == 3 { // Backward compatibility - bt.Stack = "" - bt.Project = parts[0] - bt.Service = parts[1] - bt.Etag = parts[2] - } else { - bt.Stack = parts[0] - bt.Project = parts[1] - bt.Service = parts[2] - bt.Etag = parts[3] + if len(parts) == 3 { // Backward compatibility + bt.Stack = "" + bt.Project = parts[0] + bt.Service = parts[1] + bt.Etag = parts[2] + } else { + bt.Stack = parts[0] + bt.Project = parts[1] + bt.Service = parts[2] + bt.Etag = parts[3] + } } return nil } @@ -63,10 +103,135 @@ func (gcp Gcp) GetBuildInfo(ctx context.Context, buildId string) (*BuildTag, err return nil, errors.New("build not found") } var bt BuildTag - for _, tag := range build.Tags { - if err := bt.Parse(tag); err == nil { - return &bt, nil - } + bt.Parse(build.Tags) + if bt.Project != "" || bt.Service != "" || bt.Etag != "" { + return &bt, nil } return nil, fmt.Errorf("cannot find build tag containing build info: %v", build.Tags) } + +func (gcp Gcp) RunCloudBuild(ctx context.Context, args CloudBuildArgs) (string, error) { + client, err := cloudbuild.NewClient(ctx) + if err != nil { + return "", fmt.Errorf("failed to create Cloud Build client: %w", err) + } + defer client.Close() + + var steps []*cloudbuildpb.BuildStep + if err := yaml.Unmarshal([]byte(args.Steps), &steps); err != nil { + return "", fmt.Errorf("failed to parse cloudbuild steps: %w, steps are:\n%v\n", err, args.Steps) + } + + // TODO: Implement secrets with a global `availableSecrets` and per-step `secretEnv` + // See: https://cloud.google.com/build/docs/securing-builds/use-secrets + // TODO: Use inline secret for environment variables since there is no other way to pass env vars to build steps + // var secrets *cloudbuildpb.Secrets + + // Create a build request + build := &cloudbuildpb.Build{ + Substitutions: args.Substitutions, + Steps: steps, + // TODO: Support NPM or Python packages using Artifacts field + // AvailableSecrets: secrets, + Options: &cloudbuildpb.BuildOptions{ + MachineType: GetMachineType(args.MachineType), + DiskSizeGb: GetDiskSize(args.DiskSizeGb), + Logging: cloudbuildpb.BuildOptions_CLOUD_LOGGING_ONLY, + }, + Timeout: durationpb.New(time.Hour), + Tags: args.Tags, + } + + if args.Source != "" { + // Extract bucket and object from the source + bucket, object, err := parseGCSURI(args.Source) + if err != nil { + return "", fmt.Errorf("failed to parse source URI: %w", err) + } + build.Source = &cloudbuildpb.Source{ + Source: &cloudbuildpb.Source_StorageSource{ + StorageSource: &cloudbuildpb.StorageSource{ + Bucket: bucket, + Object: object, + }, + }, + } + } + + if args.ServiceAccount != nil { + build.ServiceAccount = fmt.Sprintf("projects/%s/serviceAccounts/%s", gcp.ProjectId, *args.ServiceAccount) + } + + if args.Images != nil { + build.Images = args.Images + } + + // Trigger the build + op, err := client.CreateBuild(ctx, &cloudbuildpb.CreateBuildRequest{ + ProjectId: gcp.ProjectId, // Replace with your GCP project ID + // Current API endpoint does not support location + // Parent: fmt.Sprintf("projects/%s/locations/%s", args.ProjectId, args.Location), + Build: build, + }) + if err != nil { + return "", fmt.Errorf("failed to create build: %w", err) + } + + return op.Name(), nil +} + +func (gcp Gcp) GetBuildStatus(ctx context.Context, startBuildOpName string) error { + svc, err := cloudbuild.NewClient(ctx) + if err != nil { + return fmt.Errorf("failed to create Cloud Build client: %w", err) + } + defer svc.Close() + + op := svc.CreateBuildOperation(startBuildOpName) + build, err := op.Poll(ctx) + if err != nil { + return fmt.Errorf("failed to poll build operation: %w", err) + } + if build != nil { + if build.Status == cloudbuildpb.Build_SUCCESS { + return io.EOF + } + return client.ErrDeploymentFailed{Message: fmt.Sprintf("build failed with status: %v", build.Status)} + } + return nil +} + +func GetMachineType(machineType *string) MachineType { + if machineType == nil { + return UNSPECIFIED + } + m, ok := cloudbuildpb.BuildOptions_MachineType_value[*machineType] + if !ok { + return UNSPECIFIED + } + return MachineType(m) +} + +func GetDiskSize(diskSizeGb *int64) int64 { + if diskSizeGb == nil { + return 0 + } + return *diskSizeGb +} + +func parseGCSURI(uri string) (bucket string, object string, err error) { + if !strings.HasPrefix(uri, "gs://") { + return "", "", errors.New("URI must start with 'gs://' prefix") + } + + parts := strings.SplitN(uri[5:], "/", 2) + if len(parts) < 2 { + return "", "", errors.New("URI must contain a bucket and an object path") + } + obj, err := url.QueryUnescape(parts[1]) // Because the base 64 encoding may contain '=' + if err != nil { + return "", "", fmt.Errorf("failed to unescape object path: %w", err) + } + + return parts[0], obj, nil +} From a079f7dd2a89f8d813d35365c2d67b8257844d16 Mon Sep 17 00:00:00 2001 From: Edward J Date: Tue, 6 Jan 2026 18:53:41 -0800 Subject: [PATCH 2/6] Switch to gopkg.in/yaml.v3 --- src/pkg/clouds/gcp/cloudbuild.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pkg/clouds/gcp/cloudbuild.go b/src/pkg/clouds/gcp/cloudbuild.go index f6ad1065f..c39721870 100644 --- a/src/pkg/clouds/gcp/cloudbuild.go +++ b/src/pkg/clouds/gcp/cloudbuild.go @@ -12,8 +12,8 @@ import ( cloudbuild "cloud.google.com/go/cloudbuild/apiv1/v2" cloudbuildpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb" "github.com/DefangLabs/defang/src/pkg/cli/client" + "github.com/goccy/go-yaml" "google.golang.org/protobuf/types/known/durationpb" - "gopkg.in/yaml.v3" ) type MachineType = cloudbuildpb.BuildOptions_MachineType From 7da2a4e672c92625d03f5a552200b21a0282a067 Mon Sep 17 00:00:00 2001 From: Edward J Date: Tue, 6 Jan 2026 19:07:40 -0800 Subject: [PATCH 3/6] Check bt.Parse error --- src/go.mod | 2 +- src/pkg/clouds/gcp/cloudbuild.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/go.mod b/src/go.mod index f25a4b4df..16c6d7d75 100644 --- a/src/go.mod +++ b/src/go.mod @@ -39,6 +39,7 @@ require ( github.com/docker/cli v27.3.1+incompatible github.com/docker/docker v27.3.1+incompatible github.com/firebase/genkit/go v1.2.0 + github.com/goccy/go-yaml v1.17.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 github.com/googleapis/gax-go/v2 v2.14.2 @@ -99,7 +100,6 @@ require ( github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/go-jose/go-jose/v4 v4.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/goccy/go-yaml v1.17.1 // indirect github.com/google/dotprompt/go v0.0.0-20251014011017-8d056e027254 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect diff --git a/src/pkg/clouds/gcp/cloudbuild.go b/src/pkg/clouds/gcp/cloudbuild.go index c39721870..e15be8733 100644 --- a/src/pkg/clouds/gcp/cloudbuild.go +++ b/src/pkg/clouds/gcp/cloudbuild.go @@ -103,7 +103,9 @@ func (gcp Gcp) GetBuildInfo(ctx context.Context, buildId string) (*BuildTag, err return nil, errors.New("build not found") } var bt BuildTag - bt.Parse(build.Tags) + if err := bt.Parse(build.Tags); err != nil { + return nil, fmt.Errorf("failed to parse build tags: %w", err) + } if bt.Project != "" || bt.Service != "" || bt.Etag != "" { return &bt, nil } From 50118a23ca7fc463611defc02b7e7a29e04fcb8a Mon Sep 17 00:00:00 2001 From: Edward J Date: Tue, 6 Jan 2026 19:10:47 -0800 Subject: [PATCH 4/6] Switch to gopkg.in/yaml.v3 for byoc.go --- src/pkg/cli/client/byoc/gcp/byoc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pkg/cli/client/byoc/gcp/byoc.go b/src/pkg/cli/client/byoc/gcp/byoc.go index 8362008d7..53e7f7ec0 100644 --- a/src/pkg/cli/client/byoc/gcp/byoc.go +++ b/src/pkg/cli/client/byoc/gcp/byoc.go @@ -27,12 +27,12 @@ import ( "github.com/DefangLabs/defang/src/pkg/types" defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" "github.com/bufbuild/connect-go" + "github.com/goccy/go-yaml" "google.golang.org/api/googleapi" auditpb "google.golang.org/genproto/googleapis/cloud/audit" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" - "gopkg.in/yaml.v3" ) var _ client.Provider = (*ByocGcp)(nil) From b53db5540d170a5a59501eef634afb4356ed46c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 00:03:18 +0000 Subject: [PATCH 5/6] Initial plan From c19effd5299b20e929cfe8c82ecb0bb6b3472f80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 00:09:43 +0000 Subject: [PATCH 6/6] Switch from goccy/go-yaml to official gopkg.in/yaml.v3 Co-authored-by: lionello <591860+lionello@users.noreply.github.com> --- src/pkg/cli/client/byoc/gcp/byoc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pkg/cli/client/byoc/gcp/byoc.go b/src/pkg/cli/client/byoc/gcp/byoc.go index 53e7f7ec0..8362008d7 100644 --- a/src/pkg/cli/client/byoc/gcp/byoc.go +++ b/src/pkg/cli/client/byoc/gcp/byoc.go @@ -27,12 +27,12 @@ import ( "github.com/DefangLabs/defang/src/pkg/types" defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1" "github.com/bufbuild/connect-go" - "github.com/goccy/go-yaml" "google.golang.org/api/googleapi" auditpb "google.golang.org/genproto/googleapis/cloud/audit" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" + "gopkg.in/yaml.v3" ) var _ client.Provider = (*ByocGcp)(nil)