cli/tasks/plans/2026-01-29-sdk-release-design.md
Snider 77a7237d71 refactor: move plans to tasks/, framework docs to core-gui
- plans/ → tasks/plans/ (planning documents)
- framework/ → core-gui/docs/framework/ (GUI framework docs)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 15:01:46 +00:00

5.5 KiB

SDK Release Integration Design (S3.4)

Summary

Add core release --target sdk to generate SDKs as a separate release target. Runs breaking change detection before generating, uses release version for SDK versioning, outputs locally for manual publishing.

Design Decisions

  • Separate target: --target sdk runs ONLY SDK generation (no binary builds)
  • Local output: Generates to sdk/ directory, user handles publishing
  • Diff first: Run breaking change detection before generating
  • Match version: SDK version matches release version from git tags

CLI

core release --target sdk                    # Generate SDKs only
core release --target sdk --version v1.2.3   # Explicit version
core release --target sdk --dry-run          # Preview what would generate
core release                                 # Normal release (unchanged)

Config Schema

In .core/release.yaml:

sdk:
  spec: openapi.yaml           # or auto-detect
  languages: [typescript, python, go, php]
  output: sdk                  # output directory
  package:
    name: myapi-sdk
  diff:
    enabled: true
    fail_on_breaking: false    # warn but continue

Flow

core release --target sdk
    ↓
1. Load release config (.core/release.yaml)
    ↓
2. Check sdk config exists (error if not configured)
    ↓
3. Determine version (git tag or --version flag)
    ↓
4. If diff.enabled:
   - Get previous tag
   - Run oasdiff against current spec
   - If breaking && fail_on_breaking: abort
   - If breaking && !fail_on_breaking: warn, continue
    ↓
5. Generate SDKs for each language
   - Pass version to generators
   - Output to sdk/{language}/
    ↓
6. Print summary (languages generated, output paths)

Package Structure

pkg/release/
├── sdk.go          # RunSDK() orchestration + diff helper  ← NEW
├── release.go      # Existing Run() unchanged
└── config.go       # Existing SDKConfig unchanged

pkg/sdk/
└── sdk.go          # Add SetVersion() method  ← MODIFY

cmd/core/cmd/
└── release.go      # Add --target flag  ← MODIFY

RunSDK Implementation

// pkg/release/sdk.go

// RunSDK executes SDK-only release: diff check + generate.
func RunSDK(ctx context.Context, cfg *Config, dryRun bool) (*SDKRelease, error) {
    if cfg.SDK == nil {
        return nil, fmt.Errorf("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("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("breaking API changes detected")
            }
            fmt.Printf("Warning: breaking API changes detected\n")
        }
    }

    if dryRun {
        return &SDKRelease{
            Version:   version,
            Languages: cfg.SDK.Languages,
            Output:    cfg.SDK.Output,
        }, 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("sdk generation failed: %w", err)
    }

    return &SDKRelease{
        Version:   version,
        Languages: cfg.SDK.Languages,
        Output:    cfg.SDK.Output,
    }, nil
}

// SDKRelease holds the result of an SDK release.
type SDKRelease struct {
    Version   string
    Languages []string
    Output    string
}

CLI Integration

// cmd/core/cmd/release.go

var target string
releaseCmd.StringFlag("target", "Release target (sdk)", &target)

releaseCmd.Action(func() error {
    if target == "sdk" {
        return runReleaseSDK(dryRun, version)
    }
    return runRelease(dryRun, version, draft, prerelease)
})

func runReleaseSDK(dryRun bool, version string) error {
    ctx := context.Background()
    projectDir, _ := os.Getwd()

    cfg, err := release.LoadConfig(projectDir)
    if err != nil {
        return err
    }

    if version != "" {
        cfg.SetVersion(version)
    }

    fmt.Printf("%s Generating SDKs\n", releaseHeaderStyle.Render("SDK Release:"))
    if dryRun {
        fmt.Printf("  %s\n", releaseDimStyle.Render("(dry-run mode)"))
    }

    result, err := release.RunSDK(ctx, cfg, dryRun)
    if err != nil {
        fmt.Printf("%s %v\n", releaseErrorStyle.Render("Error:"), err)
        return err
    }

    fmt.Printf("%s SDK generation complete\n", releaseSuccessStyle.Render("Success:"))
    fmt.Printf("  Version:   %s\n", result.Version)
    fmt.Printf("  Languages: %v\n", result.Languages)
    fmt.Printf("  Output:    %s/\n", result.Output)

    return nil
}

Implementation Steps

  1. Add SetVersion() method to pkg/sdk/sdk.go
  2. Create pkg/release/sdk.go with RunSDK() and helpers
  3. Add --target flag to cmd/core/cmd/release.go
  4. Add runReleaseSDK() function to CLI
  5. Add tests for pkg/release/sdk_test.go
  6. Final verification and TODO update

Dependencies

  • oasdiff CLI (for breaking change detection)
  • Existing SDK generators (openapi-generator, etc.)