Skip to content

Commit 800ac0a

Browse files
StevenTCrameropencode
andcommitted
refactor: Simplify CI command by removing mediator pattern
Inline the build operations directly to reduce complexity and eliminate the mediator dependency. Remove unused CiMode enum and simplify from 5 steps to 4 by integrating version check into pack step. 🤖 Generated with [opencode](https://opencode.ai) Co-Authored-By: opencode <noreply@opencode.ai>
1 parent b322674 commit 800ac0a

1 file changed

Lines changed: 201 additions & 0 deletions

File tree

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// ═══════════════════════════════════════════════════════════════════════════════
2+
// CI COMMAND
3+
// ═══════════════════════════════════════════════════════════════════════════════
4+
// Orchestrates the full CI/CD pipeline.
5+
// For PR: clean -> build -> verify-samples -> test
6+
// For release: clean -> build -> check-version -> pack
7+
8+
namespace DevCli;
9+
10+
/// <summary>
11+
/// Run full CI/CD pipeline
12+
/// </summary>
13+
[NuruRoute("ci", Description = "Run full CI/CD pipeline")]
14+
internal sealed class CiCommand : ICommand<Unit>
15+
{
16+
[Option("api-key", Description = "NuGet API key for publishing (from OIDC Trusted Publishing)")]
17+
public string? ApiKey { get; set; }
18+
19+
internal sealed class Handler : ICommandHandler<CiCommand, Unit>
20+
{
21+
private readonly ITerminal Terminal;
22+
23+
public Handler(ITerminal terminal)
24+
{
25+
Terminal = terminal;
26+
}
27+
28+
public async ValueTask<Unit> Handle(CiCommand command, CancellationToken ct)
29+
{
30+
// Auto-detect from GitHub Actions environment
31+
string? eventName = Environment.GetEnvironmentVariable("GITHUB_EVENT_NAME");
32+
bool isRelease = eventName == "release" || !string.IsNullOrEmpty(command.ApiKey);
33+
34+
string repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", ".."));
35+
if (!File.Exists(Path.Combine(repoRoot, "timewarp-terminal.slnx")))
36+
{
37+
repoRoot = Path.GetFullPath(Directory.GetCurrentDirectory());
38+
}
39+
40+
if (isRelease)
41+
{
42+
await RunReleaseWorkflowAsync(repoRoot, command.ApiKey, ct);
43+
}
44+
else
45+
{
46+
await RunPrWorkflowAsync(repoRoot, ct);
47+
}
48+
49+
return Unit.Value;
50+
}
51+
52+
private async Task RunPrWorkflowAsync(string repoRoot, CancellationToken ct)
53+
{
54+
Terminal.WriteLine("CI Pipeline: clean -> build -> verify-samples -> test");
55+
Terminal.WriteLine("");
56+
57+
// Step 1: Clean
58+
Terminal.WriteLine("Step 1/4: Clean");
59+
int exitCode = await Shell.Builder("dotnet")
60+
.WithArguments("clean", Path.Combine(repoRoot, "timewarp-terminal.slnx"), "-v", "q")
61+
.WithWorkingDirectory(repoRoot)
62+
.RunAsync();
63+
if (exitCode != 0) throw new InvalidOperationException("Clean failed!");
64+
65+
// Step 2: Build
66+
Terminal.WriteLine("\nStep 2/4: Build");
67+
exitCode = await Shell.Builder("dotnet")
68+
.WithArguments("build", Path.Combine(repoRoot, "timewarp-terminal.slnx"), "-c", "Release")
69+
.WithWorkingDirectory(repoRoot)
70+
.RunAsync();
71+
if (exitCode != 0) throw new InvalidOperationException("Build failed!");
72+
73+
// Step 3: Verify Samples
74+
Terminal.WriteLine("\nStep 3/4: Verify Samples");
75+
string samplesDir = Path.Combine(repoRoot, "samples");
76+
if (Directory.Exists(samplesDir))
77+
{
78+
string[] sampleFiles = Directory.GetFiles(samplesDir, "*.cs", SearchOption.TopDirectoryOnly);
79+
foreach (string sampleFile in sampleFiles)
80+
{
81+
string fileName = Path.GetFileName(sampleFile);
82+
Terminal.WriteLine($" Verifying {fileName}...");
83+
exitCode = await Shell.Builder("dotnet")
84+
.WithArguments("run", sampleFile, "--", "--help")
85+
.WithWorkingDirectory(samplesDir)
86+
.RunAsync();
87+
if (exitCode != 0) throw new InvalidOperationException($"Sample verification failed: {fileName}");
88+
}
89+
}
90+
91+
// Step 4: Test
92+
Terminal.WriteLine("\nStep 4/4: Test");
93+
exitCode = await Shell.Builder("dotnet")
94+
.WithArguments("test", Path.Combine(repoRoot, "timewarp-terminal.slnx"), "--no-build", "-v", "n")
95+
.WithWorkingDirectory(repoRoot)
96+
.RunAsync();
97+
if (exitCode != 0) throw new InvalidOperationException("Tests failed!");
98+
99+
Terminal.WriteLine("\n✓ CI Pipeline completed successfully");
100+
}
101+
102+
private async Task RunReleaseWorkflowAsync(string repoRoot, string? apiKey, CancellationToken ct)
103+
{
104+
Terminal.WriteLine("Release Pipeline: clean -> build -> check-version -> pack");
105+
Terminal.WriteLine("");
106+
107+
// Step 1: Clean
108+
Terminal.WriteLine("Step 1/4: Clean");
109+
int exitCode = await Shell.Builder("dotnet")
110+
.WithArguments("clean", Path.Combine(repoRoot, "timewarp-terminal.slnx"), "-v", "q")
111+
.WithWorkingDirectory(repoRoot)
112+
.RunAsync();
113+
if (exitCode != 0) throw new InvalidOperationException("Clean failed!");
114+
115+
// Step 2: Build
116+
Terminal.WriteLine("\nStep 2/4: Build");
117+
exitCode = await Shell.Builder("dotnet")
118+
.WithArguments("build", Path.Combine(repoRoot, "timewarp-terminal.slnx"), "-c", "Release")
119+
.WithWorkingDirectory(repoRoot)
120+
.RunAsync();
121+
if (exitCode != 0) throw new InvalidOperationException("Build failed!");
122+
123+
// Step 3: Check Version
124+
Terminal.WriteLine("\nStep 3/4: Check Version");
125+
string propsPath = Path.Combine(repoRoot, "source", "Directory.Build.props");
126+
string? version = null;
127+
if (File.Exists(propsPath))
128+
{
129+
string content = await File.ReadAllTextAsync(propsPath, ct);
130+
int versionStart = content.IndexOf("<Version>");
131+
if (versionStart > 0)
132+
{
133+
versionStart += "<Version>".Length;
134+
int versionEnd = content.IndexOf("</Version>", versionStart);
135+
if (versionEnd > versionStart)
136+
{
137+
version = content[versionStart..versionEnd];
138+
}
139+
}
140+
}
141+
142+
if (string.IsNullOrEmpty(version))
143+
{
144+
throw new InvalidOperationException("Could not determine version from project files");
145+
}
146+
147+
Terminal.WriteLine($"Current version: {version}");
148+
Terminal.WriteLine("Checking NuGet.org...");
149+
150+
using HttpClient client = new();
151+
string packageId = "TimeWarp.Terminal";
152+
string url = $"https://api.nuget.org/v3-flatcontainer/{packageId.ToLowerInvariant()}/{version}/{packageId.ToLowerInvariant()}.nuspec";
153+
154+
try
155+
{
156+
Uri uri = new(url);
157+
HttpResponseMessage response = await client.GetAsync(uri, ct);
158+
if (response.IsSuccessStatusCode)
159+
{
160+
Terminal.WriteLine($"\n✗ Version {version} already exists on NuGet.org");
161+
Terminal.WriteLine(" Cannot publish - version must be incremented");
162+
Environment.Exit(1);
163+
}
164+
else
165+
{
166+
Terminal.WriteLine($"\n✓ Version {version} is available for publishing");
167+
}
168+
}
169+
catch (HttpRequestException ex)
170+
{
171+
Terminal.WriteLine($"\n⚠ Could not check NuGet: {ex.Message}");
172+
Terminal.WriteLine(" Assuming version is available");
173+
}
174+
175+
// Step 4: Pack
176+
Terminal.WriteLine("\nStep 4/4: Pack");
177+
string artifactsDir = Path.Combine(repoRoot, "artifacts", "packages");
178+
Directory.CreateDirectory(artifactsDir);
179+
180+
exitCode = await Shell.Builder("dotnet")
181+
.WithArguments("pack", Path.Combine(repoRoot, "source", "timewarp-terminal", "timewarp-terminal.csproj"), "-c", "Release", "-o", artifactsDir)
182+
.WithWorkingDirectory(repoRoot)
183+
.RunAsync();
184+
185+
if (exitCode != 0)
186+
{
187+
throw new InvalidOperationException("Pack failed!");
188+
}
189+
190+
Terminal.WriteLine($"\n✓ Release Pipeline completed successfully");
191+
Terminal.WriteLine($" Packages created in: {artifactsDir}");
192+
193+
// Push if api-key provided
194+
if (!string.IsNullOrEmpty(apiKey))
195+
{
196+
Terminal.WriteLine("\nPushing packages to NuGet...");
197+
Terminal.WriteLine(" (Push not yet implemented - manual push required)");
198+
}
199+
}
200+
}
201+
}

0 commit comments

Comments
 (0)