Skip to content

Commit a80342b

Browse files
committed
Use NPM for Claude CLI installation and simplify timestamp handling
- Switch Claude CLI installation from native installer to NPM (faster CDN) - Use cmd shell during installation to avoid HCS issues with PowerShell - Split plugin installation into separate ClaudeAddInsComponent - Unify timestamp logic: daily updates by default, forced with -Update - Set explicit base PATH to avoid ${PATH} expansion issues - Add VSLANG=1033 for consistent VS output language
1 parent 414d3a6 commit a80342b

File tree

10 files changed

+251
-233
lines changed

10 files changed

+251
-233
lines changed

DockerBuild.ps1

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@ param(
1111
[switch]$KeepEnv, # Does not override the env.g.json file.
1212
[switch]$Claude, # Run Claude CLI instead of Build.ps1. Use -Claude for interactive, -Claude "prompt" for non-interactive.
1313
[switch]$NoMcp, # Do not start the MCP approval server (for -Claude mode).
14-
[switch]$Update, # Update timestamp to invalidate Docker cache and force Claude/plugin updates (Claude mode only).
15-
# Timestamp value for Docker cache invalidation. Increase this to force updates of unpinned components like Claude CLI.
16-
# If not specified, defaults to the first day of the current week (so cache auto-invalidates weekly).
17-
[string]$Timestamp,
14+
[switch]$Update, # Force full timestamp update to invalidate Docker cache and force Claude/plugin updates.
1815
[string]$ImageName, # Image name (defaults to a name based on the directory).
1916
[string]$BuildAgentPath, # Path to build agent directory (defaults based on platform).
2017
[switch]$LoadEnvFromKeyVault, # Forces loading environment variables form the key vault.
@@ -311,46 +308,35 @@ function Get-TimestampFile
311308
New-Item -ItemType Directory -Path $timestampDir -Force | Out-Null
312309
}
313310

314-
# Create file if it doesn't exist OR update if -Update specified
315-
if (-not (Test-Path $timestampFile) -or $Update)
311+
if ($Update)
316312
{
313+
# Force update with full timestamp (seconds precision) to invalidate cache
317314
$timestamp = [DateTime]::UtcNow.ToString("o") # ISO 8601 format
318315
Set-Content -Path $timestampFile -Value $timestamp -NoNewline -Force
319-
Write-Host "Timestamp file updated: $timestamp" -ForegroundColor Cyan
316+
Write-Host "Timestamp file updated (forced): $timestamp" -ForegroundColor Cyan
320317
}
321-
322-
return $timestampFile
323-
}
324-
325-
# Creates a timestamp file for TeamCity builds to control Docker cache invalidation.
326-
# Uses the -Timestamp parameter if provided, otherwise defaults to first day of current week
327-
# for automatic weekly invalidation of unpinned components like Claude CLI.
328-
function New-TeamCityTimestampFile
329-
{
330-
param(
331-
[string]$TimestampValue,
332-
[string]$DockerContextDir
333-
)
334-
335-
if ([string]::IsNullOrEmpty($TimestampValue))
318+
else
336319
{
337-
# Default to first day of current week (Monday) for weekly cache invalidation
338-
$today = [DateTime]::UtcNow.Date
339-
$daysFromMonday = [int]$today.DayOfWeek - 1
340-
if ($daysFromMonday -lt 0) { $daysFromMonday = 6 } # Sunday = 6 days from Monday
341-
$firstDayOfWeek = $today.AddDays(-$daysFromMonday)
342-
$TimestampValue = $firstDayOfWeek.ToString("yyyy-MM-dd")
343-
}
320+
# Daily timestamp - only update if file doesn't exist or date changed
321+
$todayTimestamp = [DateTime]::UtcNow.Date.ToString("yyyy-MM-dd")
322+
$needsUpdate = $true
344323

345-
$gDirectory = Join-Path $DockerContextDir ".g"
346-
if (-not (Test-Path $gDirectory))
347-
{
348-
New-Item -ItemType Directory -Path $gDirectory -Force | Out-Null
349-
}
324+
if (Test-Path $timestampFile)
325+
{
326+
$currentTimestamp = Get-Content $timestampFile -Raw
327+
# Check if current timestamp starts with today's date
328+
if ($currentTimestamp -and $currentTimestamp.StartsWith($todayTimestamp))
329+
{
330+
$needsUpdate = $false
331+
}
332+
}
350333

351-
$timestampFile = Join-Path $gDirectory "update.timestamp"
352-
Set-Content -Path $timestampFile -Value $TimestampValue -NoNewline -Force
353-
Write-Host "TeamCity timestamp: $TimestampValue" -ForegroundColor Cyan
334+
if ($needsUpdate)
335+
{
336+
Set-Content -Path $timestampFile -Value $todayTimestamp -NoNewline -Force
337+
Write-Host "Timestamp file updated (daily): $todayTimestamp" -ForegroundColor Cyan
338+
}
339+
}
354340

355341
return $timestampFile
356342
}
@@ -462,14 +448,7 @@ if (-not $KeepEnv)
462448
# This is used by Dockerfile.claude but doesn't affect other Dockerfiles
463449
if (-not $NoBuildImage)
464450
{
465-
if ($env:IS_TEAMCITY_AGENT)
466-
{
467-
$timestampFile = New-TeamCityTimestampFile -TimestampValue $Timestamp -DockerContextDir $dockerContextDirectory
468-
}
469-
else
470-
{
471-
$timestampFile = Get-TimestampFile -Update:$Update
472-
}
451+
$timestampFile = Get-TimestampFile -Update:$Update
473452
}
474453

475454
if ($Claude)
@@ -1041,8 +1020,7 @@ foreach (`$dir in `$gitDirectories) {
10411020
}
10421021

10431022
# Copy timestamp file to docker context (for cache invalidation)
1044-
# Skip if running on TeamCity - we already created the file directly in docker-context
1045-
if ($timestampFile -and -not $env:IS_TEAMCITY_AGENT)
1023+
if ($timestampFile)
10461024
{
10471025
$gDirectory = Join-Path $dockerContextDirectory ".g"
10481026
if (-not (Test-Path $gDirectory))

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ ENV RUNNING_IN_DOCKER=TRUE
1818
ENV LANG=C.UTF-8
1919
ENV LC_ALL=C.UTF-8
2020
ENV DOTNET_CLI_UI_LANGUAGE=en
21+
ENV VSLANG=1033
2122

22-
# Add Windows PowerShell to PATH (pwsh added later by PowershellComponent)
23-
ENV PATH="C:\Windows\System32\WindowsPowerShell\v1.0;${PATH}"
23+
# Set base PATH explicitly to avoid issues with ${PATH} expansion
24+
ENV PATH="C:\Windows\System32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0"
2425

2526
# Enable long path support
2627
RUN Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1

Dockerfile.claude

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ ENV RUNNING_IN_DOCKER=TRUE
1818
ENV LANG=C.UTF-8
1919
ENV LC_ALL=C.UTF-8
2020
ENV DOTNET_CLI_UI_LANGUAGE=en
21+
ENV VSLANG=1033
2122

22-
# Add Windows PowerShell to PATH (pwsh added later by PowershellComponent)
23-
ENV PATH="C:\Windows\System32\WindowsPowerShell\v1.0;${PATH}"
23+
# Set base PATH explicitly to avoid issues with ${PATH} expansion
24+
ENV PATH="C:\Windows\System32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0"
2425

2526
# Enable long path support
2627
RUN Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
@@ -61,44 +62,38 @@ ENV PATH="C:\Program Files\dotnet;${PATH}"
6162
RUN & .\dotnet-install.ps1 -Version 9.0.305 -InstallDir 'C:\Program Files\dotnet'
6263

6364

64-
# Install GitHub CLI
65-
RUN Invoke-WebRequest -Uri https://github.com/cli/cli/releases/download/v2.63.2/gh_2.63.2_windows_amd64.msi -OutFile gh.msi; `
66-
$process = Start-Process msiexec.exe -Wait -PassThru -ArgumentList '/I gh.msi /quiet'; `
67-
if ($process.ExitCode -ne 0) { exit $process.ExitCode }; `
68-
Remove-Item gh.msi
65+
# Install Node.js
66+
RUN Invoke-WebRequest -Uri "https://nodejs.org/dist/v22.0.0/node-v22.0.0-win-x64.zip" -OutFile node.zip; `
67+
Expand-Archive node.zip -DestinationPath C:\; `
68+
Rename-Item "C:\node-v22.0.0-win-x64" "C:\nodejs"; `
69+
Remove-Item node.zip
6970

70-
ENV PATH="C:\Program Files\GitHub CLI;${PATH}"
71+
ENV NPM_CONFIG_PREFIX=C:\\npm
72+
ENV PATH="C:\nodejs;C:\\npm;${PATH}"
7173

7274

73-
# Timestamp
74-
# Cache invalidation layer - changes when -Update is used
75-
COPY .g/update.timestamp C:\docker-context\update.timestamp
76-
RUN Write-Host "PostSharp.Engineering build timestamp: $(Get-Content C:\docker-context\update.timestamp)"
75+
# Install Claude CLI
76+
# Set HOME/USERPROFILE so Claude CLI finds credentials during build
77+
ENV HOME=C:\\Users\\ContainerAdministrator
78+
ENV USERPROFILE=C:\\Users\\ContainerAdministrator
7779

80+
# Install Claude CLI and configure using cmd shell to avoid HCS issues with PowerShell
81+
SHELL ["cmd", "/S", "/C"]
82+
RUN C:\nodejs\npm.cmd install --global @anthropic-ai/claude-code@2.1.27
83+
RUN mkdir C:\Users\ContainerAdministrator\.claude && echo {"hasCompletedOnboarding": true} > C:\Users\ContainerAdministrator\.claude.json && echo {"alwaysThinkingEnabled": true} > C:\Users\ContainerAdministrator\.claude\settings.json
7884

79-
# Install Claude CLI
80-
ENV HOME=C:\Users\ContainerAdministrator
81-
ENV USERPROFILE=C:\Users\ContainerAdministrator
82-
ENV CLAUDE_CODE_SHELL=pwsh
83-
ENV PATH=$PATH;C:\Users\ContainerAdministrator\.local\bin
84-
85-
RUN irm https://claude.ai/install.ps1 | iex; `
86-
$claudeJsonPath = 'C:\Users\ContainerAdministrator\.claude.json'; `
87-
if (Test-Path $claudeJsonPath) { `
88-
$claudeConfig = Get-Content $claudeJsonPath -Raw | ConvertFrom-Json; `
89-
$claudeConfig | Add-Member -NotePropertyName 'hasCompletedOnboarding' -NotePropertyValue $true -Force; `
90-
$claudeConfig | ConvertTo-Json -Depth 10 | Set-Content $claudeJsonPath; `
91-
} else { `
92-
'{"hasCompletedOnboarding": true}' | Set-Content $claudeJsonPath; `
93-
}; `
94-
claude plugin marketplace add https://github.com/metalama/Metalama.AI.Skills; `
95-
claude plugin marketplace add https://github.com/postsharp/PostSharp.Engineering.AISkills; `
96-
claude plugin install metalama; `
97-
claude plugin install metalama-dev; `
98-
claude plugin install eng; `
99-
echo 'Claude CLI installation completed.'
85+
# Restore PowerShell shell using full path
86+
SHELL ["C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "-Command"]
10087

10188

89+
# Install Claude CLI Add-ins
90+
# Install Claude plugins using cmd shell to avoid HCS issues with PowerShell
91+
SHELL ["cmd", "/S", "/C"]
92+
RUN echo Installing Claude plugins && C:\npm\claude plugin marketplace add https://github.com/metalama/Metalama.AI.Skills && C:\npm\claude plugin marketplace add https://github.com/postsharp/PostSharp.Engineering.AISkills && C:\npm\claude plugin install metalama && C:\npm\claude plugin install metalama-dev && C:\npm\claude plugin install eng
93+
94+
# Restore PowerShell shell using full path
95+
SHELL ["C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "-Command"]
96+
10297

10398
# Epilogue
10499
# Create docker-context directory for build scripts
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
/// <summary>
11+
/// Installs Claude CLI plugins and marketplaces. This component is placed after the timestamp
12+
/// so that plugin updates cause a cache invalidation while the Claude CLI installation remains cached.
13+
/// </summary>
14+
public class ClaudeAddInsComponent : ContainerComponent
15+
{
16+
public override string Name => "Install Claude CLI Add-ins";
17+
18+
public override ContainerComponentKind Kind => ContainerComponentKind.ClaudeAddIns;
19+
20+
/// <summary>
21+
/// Gets the list of marketplace URLs to add in the container.
22+
/// These should be GitHub repository URLs (e.g., https://github.com/org/repo).
23+
/// Marketplaces are added first, then plugins can be installed from them.
24+
/// </summary>
25+
public string[] Marketplaces { get; init; } =
26+
[
27+
"https://github.com/metalama/Metalama.AI.Skills",
28+
"https://github.com/postsharp/PostSharp.Engineering.AISkills"
29+
];
30+
31+
/// <summary>
32+
/// Gets the list of plugin names to install from the added marketplaces.
33+
/// </summary>
34+
public string[] Plugins { get; init; } =
35+
[
36+
"metalama",
37+
"metalama-dev",
38+
"eng"
39+
];
40+
41+
public override void WriteDockerfile( TextWriter writer )
42+
{
43+
// Use cmd shell to avoid HCS issues with PowerShell
44+
writer.WriteLine(
45+
"""
46+
# Install Claude plugins using cmd shell to avoid HCS issues with PowerShell
47+
SHELL ["cmd", "/S", "/C"]
48+
""" );
49+
50+
writer.Write( "RUN echo Installing Claude plugins" );
51+
52+
// Add marketplaces if any are specified
53+
foreach ( var marketplace in this.Marketplaces )
54+
{
55+
writer.Write( $" && C:\\npm\\claude plugin marketplace add {marketplace}" );
56+
}
57+
58+
// Install plugins from the added marketplaces
59+
foreach ( var plugin in this.Plugins )
60+
{
61+
writer.Write( $" && C:\\npm\\claude plugin install {plugin}" );
62+
}
63+
64+
writer.WriteLine();
65+
66+
writer.WriteLine(
67+
"""
68+
69+
# Restore PowerShell shell using full path
70+
SHELL ["C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "-Command"]
71+
""" );
72+
}
73+
74+
public override void AddRequirements( IReadOnlyList<ContainerComponent> components, Action<ContainerComponent> add )
75+
{
76+
base.AddRequirements( components, add );
77+
78+
// Require Claude CLI to be installed first
79+
var existingClaude = components.OfType<ClaudeComponent>().FirstOrDefault();
80+
81+
if ( existingClaude == null )
82+
{
83+
add( new ClaudeComponent() );
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)