30 KiB
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
- CLI-First Operations: Every action possible via CLI, not just UI
- GitOps Native: Releases triggered by Git operations
- Developer Workflows: Integrate into existing CI/CD and development patterns
- Zero-Friction Onboarding: Quick start without extensive configuration
- Scriptable: All commands output machine-parseable formats
- 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