feat(release): implement RunSDK for SDK-only releases
Add RunSDK function that orchestrates SDK-only releases by: - Validating config and SDK configuration - Determining version from git tags or config override - Running optional breaking change detection via oasdiff - Generating SDKs for configured languages (unless dry run) The function supports dry run mode for previewing what would be done without actually generating SDKs. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e3d9aa2b90
commit
d9c4af07c2
2 changed files with 231 additions and 0 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
134
pkg/release/sdk_test.go
Normal file
134
pkg/release/sdk_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue