Skip to content

Commit 1ced9ca

Browse files
DockerBuild: rotate Claude image hash daily to refresh @latest packages
The image tag in -Claude mode is derived from a SHA256 of Dockerfile.claude plus eng/docker-context/ (excluding .g/), so it never changed and Docker served the cached image forever — the Claude CLI and marketplace plug-ins installed from @latest were effectively pinned to whatever was current the first time the image was built. Mix a shared $script:DayStamp into the hash only when -Claude is set, and have Get-TimestampFile persist the same string to update.timestamp so the outer image tag and the inner COPY layer invalidate on the same UTC-day boundary. Non-Claude image tags are unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent cd1e289 commit 1ced9ca

2 files changed

Lines changed: 106 additions & 64 deletions

File tree

DockerBuild.ps1

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -566,9 +566,10 @@ try
566566

567567
function Get-TimestampFile
568568
{
569-
param(
570-
[switch]$Update
571-
)
569+
# Persists $script:DayStamp (the single source of truth, also mixed
570+
# into the image tag by Get-ContentHash in Claude mode) to disk so
571+
# Dockerfile.claude can COPY it in and invalidate inner layers on
572+
# the same day boundary as the outer image tag.
572573

573574
$timestampDir = if ($IsUnix)
574575
{
@@ -586,34 +587,24 @@ try
586587
New-Item -ItemType Directory -Path $timestampDir -Force | Out-Null
587588
}
588589

589-
if ($Update)
590-
{
591-
# Force update with full timestamp (seconds precision) to invalidate cache
592-
$timestamp = [DateTime]::UtcNow.ToString("o") # ISO 8601 format
593-
Set-Content -Path $timestampFile -Value $timestamp -NoNewline -Force
594-
Write-Host "Timestamp file updated (forced): $timestamp" -ForegroundColor Cyan
595-
}
596-
else
590+
# Only rewrite the file if the content would actually change — avoids
591+
# bumping mtime on every run, which would pointlessly invalidate the
592+
# Docker COPY layer for the timestamp file.
593+
$needsUpdate = $true
594+
if (Test-Path $timestampFile)
597595
{
598-
# Daily timestamp - only update if file doesn't exist or date changed
599-
$todayTimestamp = [DateTime]::UtcNow.Date.ToString("yyyy-MM-dd")
600-
$needsUpdate = $true
601-
602-
if (Test-Path $timestampFile)
596+
$currentTimestamp = Get-Content $timestampFile -Raw -ErrorAction SilentlyContinue
597+
if ($currentTimestamp -eq $script:DayStamp)
603598
{
604-
$currentTimestamp = Get-Content $timestampFile -Raw
605-
# Check if current timestamp starts with today's date
606-
if ($currentTimestamp -and $currentTimestamp.StartsWith($todayTimestamp))
607-
{
608-
$needsUpdate = $false
609-
}
599+
$needsUpdate = $false
610600
}
601+
}
611602

612-
if ($needsUpdate)
613-
{
614-
Set-Content -Path $timestampFile -Value $todayTimestamp -NoNewline -Force
615-
Write-Host "Timestamp file updated (daily): $todayTimestamp" -ForegroundColor Cyan
616-
}
603+
if ($needsUpdate)
604+
{
605+
Set-Content -Path $timestampFile -Value $script:DayStamp -NoNewline -Force
606+
$label = if ($Update) { "forced" } else { "daily" }
607+
Write-Host "Timestamp file updated ($label): $script:DayStamp" -ForegroundColor Cyan
617608
}
618609

619610
return $timestampFile
@@ -623,7 +614,8 @@ try
623614
{
624615
param(
625616
[string]$DockerfilePath,
626-
[string]$ContextDirectory
617+
[string]$ContextDirectory,
618+
[string]$DayStamp # non-empty => mix into hash (used in -Claude mode)
627619
)
628620

629621
$hashInput = Get-Content $DockerfilePath -Raw -ErrorAction SilentlyContinue
@@ -632,7 +624,8 @@ try
632624
$hashInput = ""
633625
}
634626

635-
# Add context files (excluding generated .g/ directory)
627+
# Add context files (excluding generated .g/ directory, which holds
628+
# per-invocation files like env.g.json and Init.g.ps1).
636629
$contextFiles = Get-ChildItem $ContextDirectory -Recurse -File -ErrorAction SilentlyContinue |
637630
Where-Object { $_.FullName -notmatch '[/\\]\.g[/\\]' } |
638631
Sort-Object FullName
@@ -647,6 +640,14 @@ try
647640
}
648641
}
649642

643+
# When a day stamp is supplied (Claude mode), rotate the image tag once
644+
# per UTC day so @latest npm installs of the Claude CLI and marketplace
645+
# plug-ins actually get refreshed. Same string as update.timestamp.
646+
if ($DayStamp)
647+
{
648+
$hashInput += "`n--- day-stamp ---`n$DayStamp"
649+
}
650+
650651
$hashBytes = [System.Security.Cryptography.SHA256]::Create().ComputeHash(
651652
[System.Text.Encoding]::UTF8.GetBytes($hashInput)
652653
)
@@ -704,6 +705,20 @@ try
704705
}
705706
else
706707
{
708+
# Single source of truth for today's cache-busting stamp, shared by
709+
# Get-ContentHash (image tag, Claude mode only) and Get-TimestampFile
710+
# (update.timestamp file baked into the image). Computing it once here
711+
# guarantees both consumers see the same value even if the wall clock
712+
# crosses UTC midnight mid-run.
713+
$script:DayStamp = if ($Update)
714+
{
715+
[DateTime]::UtcNow.ToString("o") # full ISO 8601, seconds precision
716+
}
717+
else
718+
{
719+
[DateTime]::UtcNow.Date.ToString("yyyy-MM-dd")
720+
}
721+
707722
# Determine which Dockerfile will be used (needed for ImageName generation)
708723
$DockerfilesDir = "$EngPath/docker"
709724

@@ -744,8 +759,14 @@ try
744759
$dockerfileFullPath = Join-Path $PSScriptRoot $Dockerfile
745760
}
746761

747-
# Generate content-based hash for image tag
748-
$contentHash = Get-ContentHash -DockerfilePath $dockerfileFullPath -ContextDirectory $dockerContextDirectory
762+
# Generate content-based hash for image tag.
763+
# In Claude mode, mix in $script:DayStamp so the tag rotates daily
764+
# and picks up fresh @latest npm installs.
765+
$hashDayStamp = if ($Claude) { $script:DayStamp } else { $null }
766+
$contentHash = Get-ContentHash `
767+
-DockerfilePath $dockerfileFullPath `
768+
-ContextDirectory $dockerContextDirectory `
769+
-DayStamp $hashDayStamp
749770
$dockerRegistry = $env:DOCKER_REGISTRY
750771

751772
if ($dockerRegistry)
@@ -801,7 +822,7 @@ try
801822
# This is used by Dockerfile.claude but doesn't affect other Dockerfiles
802823
if (-not $NoBuildImage)
803824
{
804-
$timestampFile = Get-TimestampFile -Update:$Update
825+
$timestampFile = Get-TimestampFile
805826
}
806827

807828
if ($Claude)

src/PostSharp.Engineering.BuildTools/Resources/DockerBuild.ps1

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -566,9 +566,10 @@ try
566566

567567
function Get-TimestampFile
568568
{
569-
param(
570-
[switch]$Update
571-
)
569+
# Persists $script:DayStamp (the single source of truth, also mixed
570+
# into the image tag by Get-ContentHash in Claude mode) to disk so
571+
# Dockerfile.claude can COPY it in and invalidate inner layers on
572+
# the same day boundary as the outer image tag.
572573

573574
$timestampDir = if ($IsUnix)
574575
{
@@ -586,34 +587,24 @@ try
586587
New-Item -ItemType Directory -Path $timestampDir -Force | Out-Null
587588
}
588589

589-
if ($Update)
590-
{
591-
# Force update with full timestamp (seconds precision) to invalidate cache
592-
$timestamp = [DateTime]::UtcNow.ToString("o") # ISO 8601 format
593-
Set-Content -Path $timestampFile -Value $timestamp -NoNewline -Force
594-
Write-Host "Timestamp file updated (forced): $timestamp" -ForegroundColor Cyan
595-
}
596-
else
590+
# Only rewrite the file if the content would actually change — avoids
591+
# bumping mtime on every run, which would pointlessly invalidate the
592+
# Docker COPY layer for the timestamp file.
593+
$needsUpdate = $true
594+
if (Test-Path $timestampFile)
597595
{
598-
# Daily timestamp - only update if file doesn't exist or date changed
599-
$todayTimestamp = [DateTime]::UtcNow.Date.ToString("yyyy-MM-dd")
600-
$needsUpdate = $true
601-
602-
if (Test-Path $timestampFile)
596+
$currentTimestamp = Get-Content $timestampFile -Raw -ErrorAction SilentlyContinue
597+
if ($currentTimestamp -eq $script:DayStamp)
603598
{
604-
$currentTimestamp = Get-Content $timestampFile -Raw
605-
# Check if current timestamp starts with today's date
606-
if ($currentTimestamp -and $currentTimestamp.StartsWith($todayTimestamp))
607-
{
608-
$needsUpdate = $false
609-
}
599+
$needsUpdate = $false
610600
}
601+
}
611602

612-
if ($needsUpdate)
613-
{
614-
Set-Content -Path $timestampFile -Value $todayTimestamp -NoNewline -Force
615-
Write-Host "Timestamp file updated (daily): $todayTimestamp" -ForegroundColor Cyan
616-
}
603+
if ($needsUpdate)
604+
{
605+
Set-Content -Path $timestampFile -Value $script:DayStamp -NoNewline -Force
606+
$label = if ($Update) { "forced" } else { "daily" }
607+
Write-Host "Timestamp file updated ($label): $script:DayStamp" -ForegroundColor Cyan
617608
}
618609

619610
return $timestampFile
@@ -623,7 +614,8 @@ try
623614
{
624615
param(
625616
[string]$DockerfilePath,
626-
[string]$ContextDirectory
617+
[string]$ContextDirectory,
618+
[string]$DayStamp # non-empty => mix into hash (used in -Claude mode)
627619
)
628620

629621
$hashInput = Get-Content $DockerfilePath -Raw -ErrorAction SilentlyContinue
@@ -632,7 +624,8 @@ try
632624
$hashInput = ""
633625
}
634626

635-
# Add context files (excluding generated .g/ directory)
627+
# Add context files (excluding generated .g/ directory, which holds
628+
# per-invocation files like env.g.json and Init.g.ps1).
636629
$contextFiles = Get-ChildItem $ContextDirectory -Recurse -File -ErrorAction SilentlyContinue |
637630
Where-Object { $_.FullName -notmatch '[/\\]\.g[/\\]' } |
638631
Sort-Object FullName
@@ -647,6 +640,14 @@ try
647640
}
648641
}
649642

643+
# When a day stamp is supplied (Claude mode), rotate the image tag once
644+
# per UTC day so @latest npm installs of the Claude CLI and marketplace
645+
# plug-ins actually get refreshed. Same string as update.timestamp.
646+
if ($DayStamp)
647+
{
648+
$hashInput += "`n--- day-stamp ---`n$DayStamp"
649+
}
650+
650651
$hashBytes = [System.Security.Cryptography.SHA256]::Create().ComputeHash(
651652
[System.Text.Encoding]::UTF8.GetBytes($hashInput)
652653
)
@@ -704,6 +705,20 @@ try
704705
}
705706
else
706707
{
708+
# Single source of truth for today's cache-busting stamp, shared by
709+
# Get-ContentHash (image tag, Claude mode only) and Get-TimestampFile
710+
# (update.timestamp file baked into the image). Computing it once here
711+
# guarantees both consumers see the same value even if the wall clock
712+
# crosses UTC midnight mid-run.
713+
$script:DayStamp = if ($Update)
714+
{
715+
[DateTime]::UtcNow.ToString("o") # full ISO 8601, seconds precision
716+
}
717+
else
718+
{
719+
[DateTime]::UtcNow.Date.ToString("yyyy-MM-dd")
720+
}
721+
707722
# Determine which Dockerfile will be used (needed for ImageName generation)
708723
$DockerfilesDir = "$EngPath/docker"
709724

@@ -744,8 +759,14 @@ try
744759
$dockerfileFullPath = Join-Path $PSScriptRoot $Dockerfile
745760
}
746761

747-
# Generate content-based hash for image tag
748-
$contentHash = Get-ContentHash -DockerfilePath $dockerfileFullPath -ContextDirectory $dockerContextDirectory
762+
# Generate content-based hash for image tag.
763+
# In Claude mode, mix in $script:DayStamp so the tag rotates daily
764+
# and picks up fresh @latest npm installs.
765+
$hashDayStamp = if ($Claude) { $script:DayStamp } else { $null }
766+
$contentHash = Get-ContentHash `
767+
-DockerfilePath $dockerfileFullPath `
768+
-ContextDirectory $dockerContextDirectory `
769+
-DayStamp $hashDayStamp
749770
$dockerRegistry = $env:DOCKER_REGISTRY
750771

751772
if ($dockerRegistry)
@@ -801,7 +822,7 @@ try
801822
# This is used by Dockerfile.claude but doesn't affect other Dockerfiles
802823
if (-not $NoBuildImage)
803824
{
804-
$timestampFile = Get-TimestampFile -Update:$Update
825+
$timestampFile = Get-TimestampFile
805826
}
806827

807828
if ($Claude)

0 commit comments

Comments
 (0)