Skip to content

Commit 4257c3d

Browse files
committed
Support for Claude under docker (WIP).
1 parent 3cf9d48 commit 4257c3d

File tree

12 files changed

+502
-96
lines changed

12 files changed

+502
-96
lines changed

.claude/settings.local.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
"allow": [
44
"Bash(git -C \"X:\\src\\PostSharp.Engineering\" diff)",
55
"Bash(git -C \"X:\\src\\PostSharp.Engineering\" log --oneline -10)",
6-
"Bash(git -C \"X:\\src\\PostSharp.Engineering\" status)"
6+
"Bash(git -C \"X:\\src\\PostSharp.Engineering\" status)",
7+
"Bash(code \"X:\\src\\PostSharp.Engineering\")",
8+
"Bash(pwsh:*)",
9+
"Bash(dotnet build:*)",
10+
"Bash(winget search:*)",
11+
"Bash(git -C \"X:\\src\\PostSharp.Engineering\" diff --stat)",
12+
"Bash(git -C \"X:\\src\\PostSharp.Engineering\" log --oneline -20)"
713
],
814
"deny": [],
915
"ask": []

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# PostSharp.Engineering
2+
3+
Build orchestration SDK for PostSharp/Metalama repositories.
4+
5+
## Discovering Plugin Skills
6+
7+
The `postsharp-engineering` plugin provides skills and slash commands. To discover them:
8+
9+
Before any work in this repo, read these skills:
10+
- `$env:USERPROFILE\.claude\plugins\cache\postsharp-engineering\**\skills\*.md` - Engineering workflows (git, builds, CI/CD)
11+

DockerBuild.ps1

Lines changed: 133 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ param(
99
[switch]$NoClean, # Does not clean up.
1010
[switch]$NoNuGetCache, # Does not mount the host nuget cache in the container.
1111
[switch]$KeepEnv, # Does not override the env.g.json file.
12+
[switch]$Claude, # Run Claude CLI instead of Build.ps1.
13+
[string]$ClaudePrompt, # Optional prompt for Claude (non-interactive mode).
1214
[string]$ImageName, # Image name (defaults to a name based on the directory).
1315
[string]$BuildAgentPath = 'C:\BuildAgent',
1416
[switch]$LoadEnvFromKeyVault, # Forces loading environment variables form the key vault.
@@ -93,6 +95,51 @@ function New-EnvJson
9395
return $jsonPath
9496
}
9597

98+
# Function to create Claude-specific env.g.json with filtered/renamed variables
99+
function New-ClaudeEnvJson
100+
{
101+
$claudeEnv = @{ }
102+
103+
# CLAUDE_GITHUB_TOKEN -> GITHUB_TOKEN (renamed)
104+
if ($env:CLAUDE_GITHUB_TOKEN)
105+
{
106+
$claudeEnv["GITHUB_TOKEN"] = $env:CLAUDE_GITHUB_TOKEN
107+
}
108+
109+
# Preserved variables
110+
if ($env:ANTHROPIC_API_KEY)
111+
{
112+
$claudeEnv["ANTHROPIC_API_KEY"] = $env:ANTHROPIC_API_KEY
113+
}
114+
if ($env:IS_POSTSHARP_OWNED)
115+
{
116+
$claudeEnv["IS_POSTSHARP_OWNED"] = $env:IS_POSTSHARP_OWNED
117+
}
118+
if ($env:IS_TEAMCITY_AGENT)
119+
{
120+
$claudeEnv["IS_TEAMCITY_AGENT"] = $env:IS_TEAMCITY_AGENT
121+
}
122+
123+
# Convert to JSON and save
124+
$jsonPath = Join-Path $dockerContextDirectory "env.g.json"
125+
126+
# Write a test JSON file with GUID first
127+
@{ guid = [System.Guid]::NewGuid().ToString() } | ConvertTo-Json | Set-Content -Path $jsonPath -Encoding UTF8
128+
129+
# Check if secrets file is tracked by git
130+
$gitStatus = git status --porcelain $jsonPath 2> $null
131+
if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($gitStatus))
132+
{
133+
Write-Error "Secrets file '$jsonPath' is tracked by git. Please add it to .gitignore first."
134+
exit 1
135+
}
136+
137+
$claudeEnv | ConvertTo-Json -Depth 10 | Set-Content -Path $jsonPath -Encoding UTF8
138+
Write-Host "Created Claude secrets file: $jsonPath" -ForegroundColor Cyan
139+
140+
return $jsonPath
141+
}
142+
96143
if ($env:RUNNING_IN_DOCKER)
97144
{
98145
Write-Error "Already running in Docker."
@@ -139,29 +186,38 @@ Write-Host "Preparing context and mounts." -ForegroundColor Green
139186
# Create secrets JSON file.
140187
if (-not $KeepEnv)
141188
{
142-
if (-not $env:ENG_USERNAME)
189+
if ($Claude)
143190
{
144-
$env:ENG_USERNAME = $env:USERNAME
191+
# Use Claude-specific environment variables (filtered and renamed)
192+
New-ClaudeEnvJson
145193
}
146-
147-
# Add git identity to environment
148-
if ($env:IS_TEAMCITY_AGENT)
194+
else
149195
{
150-
# On TeamCity agents, check if the environment variables are set.
151-
if (-not $env:GIT_USER_EMAIL -or -not $env:GIT_USER_NAME)
196+
# Use standard build environment variables
197+
if (-not $env:ENG_USERNAME)
152198
{
153-
Write-Error "On TeamCity agents, the GIT_USER_EMAIL and GIT_USER_NAME environment variables must be set."
154-
exit 1
199+
$env:ENG_USERNAME = $env:USERNAME
200+
}
201+
202+
# Add git identity to environment
203+
if ($env:IS_TEAMCITY_AGENT)
204+
{
205+
# On TeamCity agents, check if the environment variables are set.
206+
if (-not $env:GIT_USER_EMAIL -or -not $env:GIT_USER_NAME)
207+
{
208+
Write-Error "On TeamCity agents, the GIT_USER_EMAIL and GIT_USER_NAME environment variables must be set."
209+
exit 1
210+
}
211+
}
212+
else
213+
{
214+
# On developer machines, use the current git user.
215+
$env:GIT_USER_EMAIL = git config --global user.email
216+
$env:GIT_USER_NAME = git config --global user.name
155217
}
156-
}
157-
else
158-
{
159-
# On developer machines, use the current git user.
160-
$env:GIT_USER_EMAIL = git config --global user.email
161-
$env:GIT_USER_NAME = git config --global user.name
162-
}
163218

164-
New-EnvJson -EnvironmentVariableList $EnvironmentVariables
219+
New-EnvJson -EnvironmentVariableList $EnvironmentVariables
220+
}
165221
}
166222

167223
# Get the source directory name from $PSScriptRoot
@@ -179,9 +235,9 @@ if (-not (Test-Path $dockerContextDirectory))
179235

180236

181237
# Prepare volume mappings
182-
$VolumeMappings = @("-v", "${SourceDirName}:${SourceDirName}")
183-
$MountPoints = @($SourceDirName, "c:\packages")
184-
$GitDirectories = @($SourceDirName)
238+
$VolumeMappings = @("-v", "${SourceDirName}:${SourceDirName}")
239+
$MountPoints = @($SourceDirName, "c:\packages")
240+
$GitDirectories = @($SourceDirName)
185241

186242
# Define static Git system directory for mapping. This used by Teamcity as an LFS parent repo.
187243
$gitSystemDir = "$BuildAgentPath\system\git"
@@ -281,60 +337,89 @@ docker ps -q --filter "ancestor=$ImageTag" | ForEach-Object {
281337
# Building the image.
282338
if (-not $NoBuildImage)
283339
{
284-
Write-Host "Building the image with tag: $ImageTag" -ForegroundColor Green
340+
Write-Host "Building the base image with tag: $ImageTag" -ForegroundColor Green
285341
Get-Content -Raw Dockerfile | docker build -t $ImageTag --build-arg GITDIRS="$gitDirectoriesAsString" --build-arg MOUNTPOINTS="$mountPointsAsString" -f - $dockerContextDirectory
286342
if ($LASTEXITCODE -ne 0)
287343
{
288344
Write-Host "Docker build failed with exit code $LASTEXITCODE" -ForegroundColor Red
289345
exit $LASTEXITCODE
290346
}
347+
348+
# Build Claude image if requested
349+
if ($Claude)
350+
{
351+
$ClaudeImageTag = "$ImageTag-claude"
352+
Write-Host "Building the Claude image with tag: $ClaudeImageTag" -ForegroundColor Green
353+
354+
if (-not (Test-Path "Dockerfile.claude"))
355+
{
356+
Write-Error "Dockerfile.claude not found. Make sure generate-scripts was run with a NodeJs component."
357+
exit 1
358+
}
359+
360+
Get-Content -Raw Dockerfile.claude | docker build -t $ClaudeImageTag --build-arg BASE_IMAGE="$ImageTag" -f - $dockerContextDirectory
361+
if ($LASTEXITCODE -ne 0)
362+
{
363+
Write-Host "Docker build (Claude) failed with exit code $LASTEXITCODE" -ForegroundColor Red
364+
exit $LASTEXITCODE
365+
}
366+
367+
# Use Claude image for the run
368+
$ImageTag = $ClaudeImageTag
369+
}
291370
}
292371
else
293372
{
294373
Write-Host "Skipping image build (-NoBuildImage specified)." -ForegroundColor Yellow
374+
375+
# If Claude mode and skipping build, use the Claude image tag
376+
if ($Claude)
377+
{
378+
$ImageTag = "$ImageTag-claude"
379+
}
295380
}
296381

297382

298383
# Run the build within the container
299384
if (-not $BuildImage)
300385
{
301386

302-
# Delete now and not in the container because it's much faster and lock error messages are more relevant.
303-
Write-Host "Building the product in the container." -ForegroundColor Green
387+
# Delete now and not in the container because it's much faster and lock error messages are more relevant.
388+
Write-Host "Building the product in the container." -ForegroundColor Green
304389

305-
# Prepare Build.ps1 arguments
306-
if ($StartVsmon)
307-
{
308-
$BuildArgs = @("-StartVsmon") + $BuildArgs
309-
}
390+
# Prepare Build.ps1 arguments
391+
if ($StartVsmon)
392+
{
393+
$BuildArgs = @("-StartVsmon") + $BuildArgs
394+
}
310395

311-
if ($Interactive)
312-
{
313-
$pwshArgs = "-NoExit"
314-
$BuildArgs = @("-Interactive") + $BuildArgs
315-
$dockerArgs = @("-it")
316-
$pwshExitCommand = ""
317-
}
318-
else
319-
{
320-
$pwshArgs = "-NonInteractive"
321-
$dockerArgs = @()
396+
if ($Interactive)
397+
{
398+
$pwshArgs = "-NoExit"
399+
$BuildArgs = @("-Interactive") + $BuildArgs
400+
$dockerArgs = @("-it")
401+
$pwshExitCommand = ""
402+
}
403+
else
404+
{
405+
$pwshArgs = "-NonInteractive"
406+
$dockerArgs = @()
322407
$pwshExitCommand = "exit `$LASTEXITCODE`;"
323-
}
408+
}
324409

325-
$buildArgsString = $BuildArgs -join " "
326-
$VolumeMappingsAsString = $VolumeMappings -join " "
327-
$dockerArgsAsString = $dockerArgs -join " "
410+
$buildArgsString = $BuildArgs -join " "
411+
$VolumeMappingsAsString = $VolumeMappings -join " "
412+
$dockerArgsAsString = $dockerArgs -join " "
328413

329414

330415
Write-Host "Executing: ``docker run --rm --memory=12g $dockerArgsAsString $VolumeMappingsAsString -w $SourceDirName $ImageTag pwsh $pwshArgs -Command `"& .\$Script $buildArgsString`; $pwshExitCommand`"" -ForegroundColor Cyan
331416

332417
docker run --rm --memory=12g $dockerArgs @VolumeMappings -w $SourceDirName @dockerArgs $ImageTag pwsh $pwshArgs -Command "& .\$Script $buildArgsString`; $pwshExitCommand; "
333-
if ($LASTEXITCODE -ne 0)
334-
{
335-
Write-Host "Docker run (build) failed with exit code $LASTEXITCODE" -ForegroundColor Red
336-
exit $LASTEXITCODE
337-
}
418+
if ($LASTEXITCODE -ne 0)
419+
{
420+
Write-Host "Docker run (build) failed with exit code $LASTEXITCODE" -ForegroundColor Red
421+
exit $LASTEXITCODE
422+
}
338423

339424
}
340425
else

Dockerfile.claude

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# escape=`
2+
3+
# This file is auto-generated by PostSharp.Engineering.
4+
# This Dockerfile builds a Claude-enabled image on top of the base product image.
5+
6+
ARG BASE_IMAGE
7+
FROM ${BASE_IMAGE}
8+
9+
10+
# Install Chocolatey
11+
RUN powershell -c 'irm https://community.chocolatey.org/install.ps1|iex'; `
12+
$pathsToAdd = @('C:\ProgramData\chocolatey\bin'); `
13+
$newPath = [Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + ($pathsToAdd -join ';'); `
14+
[Environment]::SetEnvironmentVariable('PATH', $newPath, 'Machine'); `
15+
& C:\ProgramData\chocolatey\bin\choco.exe feature enable -n allowGlobalConfirmation
16+
17+
# Install Node.js
18+
RUN choco install nodejs --version="22.0.0"
19+
20+
# Install Claude CLI
21+
RUN npm install --global @anthropic-ai/claude-code
22+
23+
# Add PostSharp.Engineering.AISkills as a plugin marketplace and install plugins
24+
RUN claude plugin marketplace add postsharp/PostSharp.Engineering.AISkills && `
25+
claude plugin marketplace update postsharp-engineering-aiskills

eng/RunClaude.ps1

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The original of this file is in the PostSharp.Engineering repo.
2+
# You can generate this file using `./Build.ps1 generate-scripts`.
3+
4+
param(
5+
[string]$Prompt
6+
)
7+
8+
$ErrorActionPreference = "Stop"
9+
10+
Write-Host "Starting Claude CLI..." -ForegroundColor Green
11+
12+
# Run Claude
13+
if ($Prompt)
14+
{
15+
Write-Host "Running Claude with prompt: $Prompt" -ForegroundColor Cyan
16+
claude --dangerously-skip-permissions -p $Prompt
17+
}
18+
else
19+
{
20+
Write-Host "Running Claude in interactive mode" -ForegroundColor Cyan
21+
claude
22+
}
23+
24+
exit $LASTEXITCODE

src/PostSharp.Engineering.BuildTools/ContinuousIntegration/GenerateScriptsCommand.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,16 @@ public static bool Execute( BuildContext context, CommonCommandSettings settings
3232
if ( product.UseDocker )
3333
{
3434
EmbeddedResourceHelper.ExtractScript( context, "DockerBuild.ps1", "" );
35+
EmbeddedResourceHelper.ExtractScript( context, "RunClaude.ps1", "eng" );
3536
var image = (ContainerRequirements) product.OverriddenBuildAgentRequirements!;
3637

37-
if ( !image.Prepare( context ) )
38+
if ( !image.WriteDockerfile( context ) )
39+
{
40+
return false;
41+
}
42+
43+
// Generate Claude Dockerfile (will auto-add NodeJs if not present)
44+
if ( !image.WriteClaudeDockerfile( context ) )
3845
{
3946
return false;
4047
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) SharpCrafters s.r.o. See the LICENSE.md file in the root directory of this repository root for details.
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
8+
namespace PostSharp.Engineering.BuildTools.Docker;
9+
10+
public class ClaudeComponent : ContainerComponent
11+
{
12+
private const string _minNodeVersion = "22.0.0";
13+
14+
public override string Name => "Install Claude CLI";
15+
16+
public override ContainerComponentKind Kind => ContainerComponentKind.Claude;
17+
18+
public override void WriteDockerfile( TextWriter writer )
19+
{
20+
writer.WriteLine(
21+
"""
22+
RUN npm install --global @anthropic-ai/claude-code
23+
24+
# Add PostSharp.Engineering.AISkills as a plugin marketplace and install plugins
25+
RUN claude plugin marketplace add postsharp/PostSharp.Engineering.AISkills && `
26+
claude plugin marketplace update postsharp-engineering-aiskills
27+
""" );
28+
}
29+
30+
public override void AddRequirements( IReadOnlyList<ContainerComponent> components, Action<ContainerComponent> add )
31+
{
32+
base.AddRequirements( components, add );
33+
34+
var existingNodeJs = components.OfType<NodeJsComponent>().FirstOrDefault();
35+
36+
if ( existingNodeJs == null )
37+
{
38+
// Auto-add NodeJsComponent with minimum required version
39+
add( new NodeJsComponent( _minNodeVersion ) );
40+
}
41+
else if ( Version.Parse( existingNodeJs.Version ) < Version.Parse( _minNodeVersion ) )
42+
{
43+
throw new InvalidOperationException(
44+
$"Claude CLI requires Node.js >= {_minNodeVersion}, but {existingNodeJs.Version} is configured." );
45+
}
46+
}
47+
}

src/PostSharp.Engineering.BuildTools/Docker/ContainerComponentKind.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ public enum ContainerComponentKind
1717
NodeJs,
1818
Python,
1919
Gulp,
20+
Claude,
2021
Epilogue
2122
}

0 commit comments

Comments
 (0)