diff --git a/pkg/release/sdk.go b/pkg/release/sdk.go index 761fc10..420e02f 100644 --- a/pkg/release/sdk.go +++ b/pkg/release/sdk.go @@ -2,6 +2,9 @@ package release import ( + "context" + "fmt" + "github.com/host-uk/core/pkg/sdk" ) @@ -15,6 +18,100 @@ type SDKRelease struct { Output string } +// RunSDK executes SDK-only release: diff check + generate. +// If dryRun is true, it shows what would be done without generating. +func RunSDK(ctx context.Context, cfg *Config, dryRun bool) (*SDKRelease, error) { + if cfg == nil { + return nil, fmt.Errorf("release.RunSDK: config is nil") + } + if cfg.SDK == nil { + return nil, fmt.Errorf("release.RunSDK: sdk not configured in .core/release.yaml") + } + + projectDir := cfg.projectDir + if projectDir == "" { + projectDir = "." + } + + // Determine version + version := cfg.version + if version == "" { + var err error + version, err = DetermineVersion(projectDir) + if err != nil { + return nil, fmt.Errorf("release.RunSDK: failed to determine version: %w", err) + } + } + + // Run diff check if enabled + if cfg.SDK.Diff.Enabled { + breaking, err := checkBreakingChanges(projectDir, cfg.SDK) + if err != nil { + // Non-fatal: warn and continue + fmt.Printf("Warning: diff check failed: %v\n", err) + } else if breaking { + if cfg.SDK.Diff.FailOnBreaking { + return nil, fmt.Errorf("release.RunSDK: breaking API changes detected") + } + fmt.Printf("Warning: breaking API changes detected\n") + } + } + + // Prepare result + output := cfg.SDK.Output + if output == "" { + output = "sdk" + } + + result := &SDKRelease{ + Version: version, + Languages: cfg.SDK.Languages, + Output: output, + } + + if dryRun { + return result, nil + } + + // Generate SDKs + sdkCfg := toSDKConfig(cfg.SDK) + s := sdk.New(projectDir, sdkCfg) + s.SetVersion(version) + + if err := s.Generate(ctx); err != nil { + return nil, fmt.Errorf("release.RunSDK: generation failed: %w", err) + } + + return result, nil +} + +// checkBreakingChanges runs oasdiff to detect breaking changes. +func checkBreakingChanges(projectDir string, cfg *SDKConfig) (bool, error) { + // Get previous tag for comparison (uses getPreviousTag from changelog.go) + prevTag, err := getPreviousTag(projectDir, "HEAD") + if err != nil { + return false, fmt.Errorf("no previous tag found: %w", err) + } + + // Detect spec path + specPath := cfg.Spec + if specPath == "" { + s := sdk.New(projectDir, nil) + specPath, err = s.DetectSpec() + if err != nil { + return false, err + } + } + + // Run diff + result, err := sdk.Diff(prevTag, specPath) + if err != nil { + return false, err + } + + return result.Breaking, nil +} + // toSDKConfig converts release.SDKConfig to sdk.Config. func toSDKConfig(cfg *SDKConfig) *sdk.Config { if cfg == nil { diff --git a/pkg/release/sdk_test.go b/pkg/release/sdk_test.go new file mode 100644 index 0000000..9d757f6 --- /dev/null +++ b/pkg/release/sdk_test.go @@ -0,0 +1,134 @@ +package release + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRunSDK_Bad_NilConfig(t *testing.T) { + _, err := RunSDK(context.Background(), nil, true) + assert.Error(t, err) + assert.Contains(t, err.Error(), "config is nil") +} + +func TestRunSDK_Bad_NoSDKConfig(t *testing.T) { + cfg := &Config{ + SDK: nil, + } + cfg.projectDir = "/tmp" + + _, err := RunSDK(context.Background(), cfg, true) + assert.Error(t, err) + assert.Contains(t, err.Error(), "sdk not configured") +} + +func TestRunSDK_Good_DryRun(t *testing.T) { + cfg := &Config{ + SDK: &SDKConfig{ + Languages: []string{"typescript", "python"}, + Output: "sdk", + }, + } + cfg.projectDir = "/tmp" + cfg.version = "v1.0.0" + + result, err := RunSDK(context.Background(), cfg, true) + require.NoError(t, err) + + assert.Equal(t, "v1.0.0", result.Version) + assert.Len(t, result.Languages, 2) + assert.Contains(t, result.Languages, "typescript") + assert.Contains(t, result.Languages, "python") + assert.Equal(t, "sdk", result.Output) +} + +func TestRunSDK_Good_DryRunDefaultOutput(t *testing.T) { + cfg := &Config{ + SDK: &SDKConfig{ + Languages: []string{"go"}, + Output: "", // Empty output, should default to "sdk" + }, + } + cfg.projectDir = "/tmp" + cfg.version = "v2.0.0" + + result, err := RunSDK(context.Background(), cfg, true) + require.NoError(t, err) + + assert.Equal(t, "sdk", result.Output) +} + +func TestRunSDK_Good_DryRunDefaultProjectDir(t *testing.T) { + cfg := &Config{ + SDK: &SDKConfig{ + Languages: []string{"typescript"}, + Output: "out", + }, + } + // projectDir is empty, should default to "." + cfg.version = "v1.0.0" + + result, err := RunSDK(context.Background(), cfg, true) + require.NoError(t, err) + + assert.Equal(t, "v1.0.0", result.Version) +} + +func TestRunSDK_Bad_BreakingChangesFailOnBreaking(t *testing.T) { + // This test verifies that when diff.FailOnBreaking is true and breaking changes + // are detected, RunSDK returns an error. However, since we can't easily mock + // the diff check, this test verifies the config is correctly processed. + // The actual breaking change detection is tested in pkg/sdk/diff_test.go. + cfg := &Config{ + SDK: &SDKConfig{ + Languages: []string{"typescript"}, + Output: "sdk", + Diff: SDKDiffConfig{ + Enabled: true, + FailOnBreaking: true, + }, + }, + } + cfg.projectDir = "/tmp" + cfg.version = "v1.0.0" + + // In dry run mode with no git repo, diff check will fail gracefully + // (non-fatal warning), so this should succeed + result, err := RunSDK(context.Background(), cfg, true) + require.NoError(t, err) + assert.Equal(t, "v1.0.0", result.Version) +} + +func TestToSDKConfig_Good(t *testing.T) { + sdkCfg := &SDKConfig{ + Spec: "api/openapi.yaml", + Languages: []string{"typescript", "go"}, + Output: "sdk", + Package: SDKPackageConfig{ + Name: "myapi", + Version: "v1.0.0", + }, + Diff: SDKDiffConfig{ + Enabled: true, + FailOnBreaking: true, + }, + } + + result := toSDKConfig(sdkCfg) + + assert.Equal(t, "api/openapi.yaml", result.Spec) + assert.Equal(t, []string{"typescript", "go"}, result.Languages) + assert.Equal(t, "sdk", result.Output) + assert.Equal(t, "myapi", result.Package.Name) + assert.Equal(t, "v1.0.0", result.Package.Version) + assert.True(t, result.Diff.Enabled) + assert.True(t, result.Diff.FailOnBreaking) +} + +func TestToSDKConfig_Good_NilInput(t *testing.T) { + result := toSDKConfig(nil) + assert.Nil(t, result) +}