Files
git.stella-ops.org/docs/modules/release-orchestrator/enhancements/developer-experience.md
2026-01-17 21:32:08 +02:00

30 KiB

Developer Experience

Overview

Developer Experience transforms the Release Orchestrator from a web-first platform into a complete developer toolkit. This enhancement provides a powerful CLI for release operations, GitOps-native workflows, IDE integrations, and streamlined development workflows that integrate seamlessly with existing developer toolchains.

This is a best-in-class implementation inspired by tools like GitHub CLI, Vercel CLI, and Argo CD CLI, tailored for release orchestration workflows.


Design Principles

  1. CLI-First Operations: Every action possible via CLI, not just UI
  2. GitOps Native: Releases triggered by Git operations
  3. Developer Workflows: Integrate into existing CI/CD and development patterns
  4. Zero-Friction Onboarding: Quick start without extensive configuration
  5. Scriptable: All commands output machine-parseable formats
  6. Offline Capable: Local validation and preview without server

Architecture

Component Overview

┌────────────────────────────────────────────────────────────────────────┐
│                    Developer Experience System                         │
├────────────────────────────────────────────────────────────────────────┤
│                                                                        │
│  ┌──────────────────┐    ┌───────────────────┐    ┌─────────────────┐ │
│  │ CLI Application  │───▶│ API Client        │───▶│ Server API      │ │
│  │ (stella)         │    │                   │    │                 │ │
│  └──────────────────┘    └───────────────────┘    └─────────────────┘ │
│           │                       │                        │          │
│           ▼                       ▼                        ▼          │
│  ┌──────────────────┐    ┌───────────────────┐    ┌─────────────────┐ │
│  │ GitOps Controller│    │ IDE Extensions    │    │ Webhook Handler │ │
│  │                  │    │                   │    │                 │ │
│  └──────────────────┘    └───────────────────┘    └─────────────────┘ │
│           │                       │                        │          │
│           ▼                       ▼                        ▼          │
│  ┌──────────────────┐    ┌───────────────────┐    ┌─────────────────┐ │
│  │ Template Engine  │    │ Local Validator   │    │ Config Sync     │ │
│  │                  │    │                   │    │                 │ │
│  └──────────────────┘    └───────────────────┘    └─────────────────┘ │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

Key Components

1. CLI Application (stella)

Full-featured command-line interface:

// CLI structure
public sealed class StellaCli
{
    // Root command
    // stella --version
    // stella --help

    // Auth commands
    // stella auth login [--token] [--sso]
    // stella auth logout
    // stella auth status
    // stella auth switch-context <context>

    // Release commands
    // stella release create <name> --version <ver> [--component <img>]...
    // stella release list [--env <env>] [--status <status>]
    // stella release get <id>
    // stella release diff <id1> <id2>
    // stella release history <component>

    // Promotion commands
    // stella promote <release> --to <env> [--approve] [--wait]
    // stella promote status <promotion-id>
    // stella promote approve <promotion-id>
    // stella promote reject <promotion-id> --reason <reason>

    // Deployment commands
    // stella deploy <release> --env <env> [--strategy <strategy>]
    // stella deploy status <deployment-id>
    // stella deploy logs <deployment-id> [--follow]
    // stella rollback <env> [--to <release>]

    // Environment commands
    // stella env list
    // stella env get <env>
    // stella env freeze <env> --until <time>
    // stella env unfreeze <env>
    // stella env diff <env1> <env2>

    // Workflow commands
    // stella workflow list
    // stella workflow run <template> [--var <key>=<value>]...
    // stella workflow status <run-id>
    // stella workflow logs <run-id> [--step <step>]

    // Agent commands
    // stella agent list [--env <env>]
    // stella agent status <agent-id>
    // stella agent drain <agent-id>

    // Config commands
    // stella config init
    // stella config validate
    // stella config apply
    // stella config diff
}
Command Implementation Example
public sealed class ReleaseCreateCommand : ICommand
{
    public async Task<int> ExecuteAsync(
        ReleaseCreateOptions options,
        CancellationToken ct)
    {
        var console = _consoleFactory.Create();

        // Validate options
        var validation = await ValidateOptionsAsync(options, ct);
        if (!validation.IsValid)
        {
            console.WriteError(validation.Error);
            return 1;
        }

        // Show what we're about to do
        console.WriteLine($"Creating release '{options.Name}' v{options.Version}");
        console.WriteLine();

        // Resolve components
        var components = new List<ReleaseComponent>();
        foreach (var componentSpec in options.Components)
        {
            var (image, tag) = ParseComponentSpec(componentSpec);

            console.WriteSpinner($"Resolving {image}:{tag}...");
            var digest = await _registryClient.ResolveDigestAsync(image, tag, ct);
            console.WriteSuccess($"{image}@{digest[..19]}");

            components.Add(new ReleaseComponent
            {
                Name = ExtractComponentName(image),
                Image = image,
                Tag = tag,
                Digest = digest
            });
        }

        // Create release
        console.WriteLine();
        console.WriteSpinner("Creating release...");

        var release = await _apiClient.CreateReleaseAsync(new CreateReleaseRequest
        {
            Name = options.Name,
            Version = options.Version,
            Components = components.ToImmutableArray(),
            SourceRef = options.SourceRef ?? await GetGitRefAsync(ct),
            Labels = options.Labels?.ToImmutableDictionary()
        }, ct);

        console.WriteSuccess($"Release created: {release.Id}");
        console.WriteLine();

        // Output
        if (options.OutputFormat == OutputFormat.Json)
        {
            console.WriteJson(release);
        }
        else
        {
            WriteReleaseTable(console, release);
        }

        // Next steps
        console.WriteLine();
        console.WriteHint($"Promote with: stella promote {release.Id} --to <environment>");

        return 0;
    }
}
Interactive Prompts
public sealed class PromoteCommand : ICommand
{
    public async Task<int> ExecuteAsync(
        PromoteOptions options,
        CancellationToken ct)
    {
        var console = _consoleFactory.Create();

        // If no release specified, prompt
        if (string.IsNullOrEmpty(options.ReleaseId))
        {
            var releases = await _apiClient.ListReleasesAsync(new ListReleasesRequest
            {
                Status = ReleaseStatus.Ready,
                Limit = 10
            }, ct);

            options.ReleaseId = console.Prompt(
                "Select release to promote",
                releases.Select(r => new Choice($"{r.Name} v{r.Version}", r.Id)));
        }

        // If no target specified, prompt
        if (string.IsNullOrEmpty(options.TargetEnvironment))
        {
            var environments = await _apiClient.ListEnvironmentsAsync(ct);
            var release = await _apiClient.GetReleaseAsync(options.ReleaseId, ct);

            // Filter to valid promotion targets
            var validTargets = environments
                .Where(e => e.PromotionOrder > release.CurrentEnvironmentOrder)
                .OrderBy(e => e.PromotionOrder);

            options.TargetEnvironment = console.Prompt(
                "Select target environment",
                validTargets.Select(e => new Choice(e.Name, e.Id)));
        }

        // Confirm
        var confirmation = await ShowPromotionPreviewAsync(
            options.ReleaseId, options.TargetEnvironment, ct);

        if (!options.AutoApprove)
        {
            var proceed = console.Confirm(
                $"Promote to {options.TargetEnvironment}?", defaultValue: false);

            if (!proceed)
            {
                console.WriteWarning("Promotion cancelled");
                return 0;
            }
        }

        // Execute promotion
        return await ExecutePromotionAsync(options, ct);
    }
}

2. GitOps Controller

Enables Git-driven releases:

public sealed class GitOpsController
{
    public async Task ProcessGitEventAsync(
        GitEvent @event,
        CancellationToken ct)
    {
        var config = await LoadGitOpsConfigAsync(@event.Repository, ct);
        if (config == null)
        {
            _logger.LogDebug("No GitOps config found for {Repo}", @event.Repository);
            return;
        }

        switch (@event)
        {
            case TagCreatedEvent tag:
                await HandleTagCreatedAsync(tag, config, ct);
                break;

            case BranchPushedEvent push:
                await HandleBranchPushAsync(push, config, ct);
                break;

            case PullRequestMergedEvent pr:
                await HandlePRMergedAsync(pr, config, ct);
                break;
        }
    }

    private async Task HandleTagCreatedAsync(
        TagCreatedEvent tag,
        GitOpsConfig config,
        CancellationToken ct)
    {
        // Check if tag matches release pattern
        if (!MatchesPattern(tag.TagName, config.ReleaseTagPattern))
            return;

        _logger.LogInformation(
            "Processing release tag {Tag} for {Repo}",
            tag.TagName, tag.Repository);

        // Extract version from tag
        var version = ExtractVersion(tag.TagName, config.ReleaseTagPattern);

        // Resolve components from config
        var components = await ResolveComponentsAsync(tag.Commit, config, ct);

        // Create release
        var release = await _releaseService.CreateReleaseAsync(new CreateReleaseRequest
        {
            Name = config.ReleaseName ?? tag.Repository,
            Version = version,
            Components = components,
            SourceRef = tag.Commit,
            Labels = new Dictionary<string, string>
            {
                ["git.tag"] = tag.TagName,
                ["git.repo"] = tag.Repository,
                ["gitops"] = "true"
            }.ToImmutableDictionary()
        }, ct);

        _logger.LogInformation("Created release {ReleaseId} from tag {Tag}", release.Id, tag.TagName);

        // Auto-promote if configured
        if (config.AutoPromote?.Enabled == true)
        {
            await AutoPromoteAsync(release, config.AutoPromote, ct);
        }
    }

    private async Task AutoPromoteAsync(
        Release release,
        AutoPromoteConfig config,
        CancellationToken ct)
    {
        foreach (var targetEnv in config.Environments)
        {
            // Check conditions
            if (targetEnv.RequireTests)
            {
                var testsPassed = await CheckTestsAsync(release.SourceRef, ct);
                if (!testsPassed)
                {
                    _logger.LogWarning("Tests not passed, skipping auto-promote to {Env}", targetEnv.Name);
                    continue;
                }
            }

            // Create promotion
            await _promotionService.CreatePromotionAsync(new CreatePromotionRequest
            {
                ReleaseId = release.Id,
                TargetEnvironmentId = targetEnv.Id,
                Requester = "gitops-controller",
                AutoApprove = targetEnv.AutoApprove,
                Labels = new Dictionary<string, string>
                {
                    ["gitops.auto_promote"] = "true"
                }.ToImmutableDictionary()
            }, ct);
        }
    }
}

public sealed record GitOpsConfig
{
    public string ReleaseTagPattern { get; init; }      // e.g., "v*" or "release-*"
    public string? ReleaseName { get; init; }
    public ImmutableArray<ComponentMapping> Components { get; init; }
    public AutoPromoteConfig? AutoPromote { get; init; }
    public ImmutableArray<string> IgnorePaths { get; init; }
}
GitOps Configuration File (.stella.yaml)
# .stella.yaml - GitOps configuration

# Release configuration
release:
  name: "my-service"
  tag_pattern: "v*"              # Create release on v* tags
  version_pattern: "v{version}"  # Extract version from tag

# Component mappings
components:
  - name: api
    image: registry.example.com/my-service/api
    dockerfile: ./api/Dockerfile
    build_context: ./api

  - name: worker
    image: registry.example.com/my-service/worker
    dockerfile: ./worker/Dockerfile
    build_context: ./worker

# Auto-promotion rules
auto_promote:
  enabled: true
  environments:
    - name: development
      auto_approve: true
      require_tests: false

    - name: staging
      auto_approve: true
      require_tests: true
      test_workflow: ".github/workflows/integration-tests.yml"

    - name: production
      auto_approve: false
      require_tests: true
      require_review: true

# Branch mappings (optional)
branches:
  main:
    environment: staging
    auto_deploy: true

  "release/*":
    environment: production
    auto_deploy: false

# Ignore paths (changes to these don't trigger releases)
ignore_paths:
  - "*.md"
  - "docs/**"
  - ".github/**"

3. IDE Extensions

VS Code Extension
// VS Code extension for Stella

import * as vscode from 'vscode';
import { StellaClient } from './client';

export function activate(context: vscode.ExtensionContext) {
  const client = new StellaClient();

  // Release explorer tree view
  const releaseProvider = new ReleaseTreeDataProvider(client);
  vscode.window.registerTreeDataProvider('stella.releases', releaseProvider);

  // Status bar item
  const statusBar = vscode.window.createStatusBarItem(
    vscode.StatusBarAlignment.Left
  );
  statusBar.command = 'stella.showReleases';
  statusBar.show();
  updateStatusBar(statusBar, client);

  // Commands
  context.subscriptions.push(
    vscode.commands.registerCommand('stella.createRelease', async () => {
      const name = await vscode.window.showInputBox({
        prompt: 'Release name',
        placeHolder: 'my-release'
      });
      if (!name) return;

      const version = await vscode.window.showInputBox({
        prompt: 'Version',
        placeHolder: '1.0.0'
      });
      if (!version) return;

      try {
        const release = await client.createRelease({ name, version });
        vscode.window.showInformationMessage(
          `Release created: ${release.id}`
        );
        releaseProvider.refresh();
      } catch (err) {
        vscode.window.showErrorMessage(`Failed to create release: ${err}`);
      }
    }),

    vscode.commands.registerCommand('stella.promote', async (releaseId: string) => {
      const environments = await client.listEnvironments();
      const selected = await vscode.window.showQuickPick(
        environments.map(e => ({ label: e.name, id: e.id })),
        { placeHolder: 'Select target environment' }
      );
      if (!selected) return;

      try {
        await client.createPromotion({
          releaseId,
          targetEnvironmentId: selected.id
        });
        vscode.window.showInformationMessage('Promotion created');
      } catch (err) {
        vscode.window.showErrorMessage(`Promotion failed: ${err}`);
      }
    }),

    vscode.commands.registerCommand('stella.viewLogs', async (deploymentId: string) => {
      const panel = vscode.window.createWebviewPanel(
        'stellaLogs',
        'Deployment Logs',
        vscode.ViewColumn.Two,
        { enableScripts: true }
      );

      // Stream logs to webview
      const stream = client.streamDeploymentLogs(deploymentId);
      stream.on('log', (log) => {
        panel.webview.postMessage({ type: 'log', data: log });
      });
    })
  );

  // Code lens for .stella.yaml
  context.subscriptions.push(
    vscode.languages.registerCodeLensProvider(
      { pattern: '**/.stella.yaml' },
      new StellaConfigCodeLensProvider(client)
    )
  );

  // Diagnostics for config validation
  const diagnostics = vscode.languages.createDiagnosticCollection('stella');
  context.subscriptions.push(diagnostics);

  vscode.workspace.onDidSaveTextDocument(async (doc) => {
    if (doc.fileName.endsWith('.stella.yaml')) {
      const validation = await client.validateConfig(doc.getText());
      if (validation.errors.length > 0) {
        diagnostics.set(doc.uri, validation.errors.map(e => ({
          message: e.message,
          range: new vscode.Range(e.line, 0, e.line, 100),
          severity: vscode.DiagnosticSeverity.Error
        })));
      } else {
        diagnostics.clear();
      }
    }
  });
}

class ReleaseTreeDataProvider implements vscode.TreeDataProvider<ReleaseItem> {
  private _onDidChangeTreeData = new vscode.EventEmitter<ReleaseItem | undefined>();
  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  constructor(private client: StellaClient) {}

  refresh(): void {
    this._onDidChangeTreeData.fire(undefined);
  }

  async getChildren(element?: ReleaseItem): Promise<ReleaseItem[]> {
    if (!element) {
      const releases = await this.client.listReleases({ limit: 20 });
      return releases.map(r => new ReleaseItem(r));
    }
    return [];
  }

  getTreeItem(element: ReleaseItem): vscode.TreeItem {
    return element;
  }
}
JetBrains Plugin
// JetBrains plugin for Stella

class StellaToolWindowFactory : ToolWindowFactory {
    override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
        val stellaClient = StellaClient(project)
        val contentFactory = ContentFactory.getInstance()

        // Releases panel
        val releasesPanel = ReleasesPanel(stellaClient)
        val releasesContent = contentFactory.createContent(
            releasesPanel,
            "Releases",
            false
        )
        toolWindow.contentManager.addContent(releasesContent)

        // Deployments panel
        val deploymentsPanel = DeploymentsPanel(stellaClient)
        val deploymentsContent = contentFactory.createContent(
            deploymentsPanel,
            "Deployments",
            false
        )
        toolWindow.contentManager.addContent(deploymentsContent)
    }
}

class StellaConfigAnnotator : Annotator {
    override fun annotate(element: PsiElement, holder: AnnotationHolder) {
        if (element.containingFile?.name != ".stella.yaml") return

        // Validate configuration
        val validation = StellaConfigValidator.validate(element.text)
        for (error in validation.errors) {
            holder.newAnnotation(HighlightSeverity.ERROR, error.message)
                .range(error.textRange)
                .create()
        }
    }
}

class StellaLineMarkerProvider : LineMarkerProvider {
    override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? {
        // Add gutter icons for promote/deploy actions
        if (element is YAMLKeyValue && element.keyText == "environment") {
            return LineMarkerInfo(
                element,
                element.textRange,
                StellaIcons.PROMOTE,
                { "Promote to ${element.valueText}" },
                { _, _ -> promoteToEnvironment(element.valueText) },
                GutterIconRenderer.Alignment.CENTER
            )
        }
        return null
    }
}

4. Local Validator

Validates configurations without server:

public sealed class LocalValidator
{
    public async Task<ValidationResult> ValidateAsync(
        ValidationRequest request,
        CancellationToken ct)
    {
        var result = new ValidationResult();

        // Parse configuration
        var config = await ParseConfigAsync(request.ConfigPath, ct);
        if (config == null)
        {
            result.AddError("Invalid YAML syntax", request.ConfigPath, 0);
            return result;
        }

        // Schema validation
        var schemaErrors = ValidateAgainstSchema(config);
        result.AddErrors(schemaErrors);

        // Semantic validation
        var semanticErrors = await ValidateSemanticsAsync(config, ct);
        result.AddErrors(semanticErrors);

        // Component validation
        foreach (var component in config.Components)
        {
            var componentErrors = await ValidateComponentAsync(component, ct);
            result.AddErrors(componentErrors);
        }

        // Workflow validation
        if (config.Workflows != null)
        {
            foreach (var workflow in config.Workflows)
            {
                var workflowErrors = ValidateWorkflow(workflow);
                result.AddErrors(workflowErrors);
            }
        }

        return result;
    }

    public async Task<DiffResult> DiffAsync(
        string localPath,
        string serverEndpoint,
        CancellationToken ct)
    {
        var localConfig = await ParseConfigAsync(localPath, ct);
        var serverConfig = await FetchServerConfigAsync(serverEndpoint, ct);

        return new DiffResult
        {
            AddedComponents = FindAdded(localConfig.Components, serverConfig.Components),
            RemovedComponents = FindRemoved(localConfig.Components, serverConfig.Components),
            ModifiedComponents = FindModified(localConfig.Components, serverConfig.Components),
            AddedWorkflows = FindAdded(localConfig.Workflows, serverConfig.Workflows),
            RemovedWorkflows = FindRemoved(localConfig.Workflows, serverConfig.Workflows),
            ModifiedWorkflows = FindModified(localConfig.Workflows, serverConfig.Workflows)
        };
    }
}

CLI Command Reference

Authentication

# Login with interactive flow
stella auth login

# Login with token
stella auth login --token <token>

# Login with SSO
stella auth login --sso

# Check auth status
stella auth status
# Output:
# Logged in as: john@example.com
# Organization: acme-corp
# Context: production
# Token expires: 2024-02-15 14:30:00

# Switch context
stella auth switch-context staging

# Logout
stella auth logout

Releases

# Create release
stella release create my-release \
  --version 1.2.0 \
  --component api=registry.example.com/api:v1.2.0 \
  --component worker=registry.example.com/worker:v1.2.0 \
  --label team=platform \
  --label sprint=42

# List releases
stella release list
stella release list --env production --status deployed
stella release list --format json | jq '.[] | .version'

# Get release details
stella release get rel-abc123

# Compare releases
stella release diff rel-abc123 rel-def456
# Output:
# Component   | rel-abc123      | rel-def456
# ------------|-----------------|------------------
# api         | sha256:abc...   | sha256:def... (changed)
# worker      | sha256:xyz...   | sha256:xyz... (unchanged)
# cache       | -               | sha256:new... (added)

# Release history for component
stella release history api --env production

Promotions

# Promote release
stella promote rel-abc123 --to staging

# Promote with auto-approval
stella promote rel-abc123 --to staging --approve

# Promote and wait for completion
stella promote rel-abc123 --to production --wait --timeout 30m

# Check promotion status
stella promote status promo-xyz789

# Approve pending promotion
stella promote approve promo-xyz789

# Reject promotion
stella promote reject promo-xyz789 --reason "Security review pending"

Deployments

# Deploy release
stella deploy rel-abc123 --env staging

# Deploy with strategy
stella deploy rel-abc123 --env production --strategy canary

# Check deployment status
stella deploy status deploy-abc123

# Stream deployment logs
stella deploy logs deploy-abc123 --follow

# Rollback
stella rollback production
stella rollback production --to rel-previous123

Environments

# List environments
stella env list
# Output:
# NAME         | STATUS  | RELEASES | LAST DEPLOYMENT
# -------------|---------|----------|------------------
# development  | active  | 45       | 2 hours ago
# staging      | active  | 32       | 1 hour ago
# production   | frozen  | 28       | 3 days ago

# Get environment details
stella env get production

# Freeze environment
stella env freeze production --until "2024-02-15 18:00:00" --reason "Feature freeze"

# Unfreeze environment
stella env unfreeze production

# Compare environments
stella env diff staging production

Workflows

# List workflow templates
stella workflow list

# Run workflow
stella workflow run deploy-workflow \
  --var release_id=rel-abc123 \
  --var environment=staging

# Check workflow status
stella workflow status run-xyz789

# View workflow logs
stella workflow logs run-xyz789
stella workflow logs run-xyz789 --step approval-gate

# Cancel workflow
stella workflow cancel run-xyz789

Configuration

# Initialize config in current directory
stella config init

# Validate configuration
stella config validate
# Output:
# ✓ Configuration valid
# - 3 components defined
# - 2 workflows defined
# - Auto-promote enabled for: development, staging

# Show what would change
stella config diff

# Apply configuration
stella config apply

# Apply with preview
stella config apply --dry-run

Output Formats

Human-Readable (Default)

$ stella release list

RELEASES
────────────────────────────────────────────────────────────────────
ID           NAME          VERSION    STATUS      CREATED
rel-abc123   my-service    1.2.0      deployed    2 hours ago
rel-def456   my-service    1.1.0      deployed    1 day ago
rel-ghi789   my-service    1.0.0      archived    1 week ago

Showing 3 of 45 releases. Use --limit to show more.

JSON

$ stella release list --format json
[
  {
    "id": "rel-abc123",
    "name": "my-service",
    "version": "1.2.0",
    "status": "deployed",
    "created_at": "2024-02-10T14:30:00Z",
    "components": [
      {
        "name": "api",
        "image": "registry.example.com/api",
        "digest": "sha256:abc..."
      }
    ]
  }
]

YAML

$ stella release get rel-abc123 --format yaml
id: rel-abc123
name: my-service
version: "1.2.0"
status: deployed
created_at: "2024-02-10T14:30:00Z"
components:
  - name: api
    image: registry.example.com/api
    digest: sha256:abc...

Table (for scripts)

$ stella release list --format table --columns id,version,status
rel-abc123	1.2.0	deployed
rel-def456	1.1.0	deployed
rel-ghi789	1.0.0	archived

Configuration Files

Global Config (~/.stella/config.yaml)

# Default server
server: https://stella.example.com

# Current context
current_context: production

# Contexts
contexts:
  production:
    server: https://stella.example.com
    organization: acme-corp
    environment: production

  staging:
    server: https://stella-staging.example.com
    organization: acme-corp
    environment: staging

# Defaults
defaults:
  output_format: human
  timeout: 5m
  auto_approve: false

# Aliases
aliases:
  p: promote
  d: deploy
  r: release

# Plugins
plugins:
  - name: stella-plugin-slack
    config:
      webhook_url: https://hooks.slack.com/...

API Design

REST Endpoints (CLI-Optimized)

# Batch operations
POST   /api/v1/batch                    # Execute multiple operations

# CLI-specific
GET    /api/v1/cli/completions          # Shell completions data
GET    /api/v1/cli/version              # CLI version check
POST   /api/v1/cli/feedback             # Submit feedback

# Config sync
GET    /api/v1/config/export            # Export current config
POST   /api/v1/config/import            # Import config
POST   /api/v1/config/validate          # Validate config
GET    /api/v1/config/diff              # Show pending changes

Metrics & Observability

CLI Telemetry (Opt-in)

# Command usage
stella_cli_commands_total{command, subcommand, status}
stella_cli_command_duration_seconds{command}

# Errors
stella_cli_errors_total{command, error_type}

# GitOps
stella_gitops_events_total{event_type, repository}
stella_gitops_releases_created_total{repository}
stella_gitops_auto_promotes_total{environment, status}

Test Strategy

Unit Tests

  • Command parsing
  • Output formatting
  • Config validation
  • GitOps pattern matching

Integration Tests

  • Full CLI flows
  • Server interaction
  • GitOps webhook handling
  • IDE extension commands

E2E Tests

  • Release lifecycle via CLI
  • GitOps trigger to deployment
  • Multi-context operations

Migration Path

Phase 1: CLI Foundation (Week 1-2)

  • Core CLI structure
  • Auth commands
  • Release commands
  • Output formatting

Phase 2: Operations (Week 3-4)

  • Promotion commands
  • Deployment commands
  • Workflow commands
  • Environment commands

Phase 3: GitOps (Week 5-6)

  • GitOps controller
  • Webhook handlers
  • Auto-promote logic
  • Branch mappings

Phase 4: IDE Extensions (Week 7-8)

  • VS Code extension
  • JetBrains plugin
  • Config validation
  • Code lens/annotations

Phase 5: Local Tools (Week 9-10)

  • Local validator
  • Offline mode
  • Config sync
  • Diff tools

Phase 6: Polish (Week 11-12)

  • Shell completions
  • Documentation
  • Tutorials
  • Plugin system