Skip to content

Commit f1ea9ca

Browse files
committed
Add Docker container labels and automatic cleanup step
Label containers with build ID so orphaned containers can be identified and cleaned up. Add an always-running cleanup step to TeamCity builds that removes containers matching the build label. Also fix docker kill error on stopped containers and escape $ in Kotlin string literals.
1 parent d44c90a commit f1ea9ca

File tree

9 files changed

+165
-86
lines changed

9 files changed

+165
-86
lines changed

.teamcity/settings.kts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,17 @@ object PublicBuild : BuildType({
6464
path = "DockerBuild.ps1"
6565
}
6666
noProfile = false
67-
scriptArgs = "-Script Build.ps1 -ImageName postsharpengineering-2023.2 -NoBuildImage test --configuration Public --buildNumber %build.number% --buildType %system.teamcity.buildType.id% --timeout %Build.Timeout% %Build.Arguments%"
67+
scriptArgs = "-Script Build.ps1 -ImageName postsharpengineering-2023.2 -NoBuildImage -Label %system.teamcity.buildType.id%_%build.number% test --configuration Public --buildNumber %build.number% --buildType %system.teamcity.buildType.id% --timeout %Build.Timeout% %Build.Arguments%"
68+
}
69+
powerShell {
70+
name = "Cleanup Docker containers"
71+
id = "DockerCleanup"
72+
executionMode = BuildStep.ExecutionMode.ALWAYS
73+
edition = PowerShellStep.Edition.Core
74+
scriptMode = script {
75+
content = "${'$'}label = \"%system.teamcity.buildType.id%_%build.number%\"; ${'$'}ids = docker ps -a -q --filter \"label=postsharp.build=${'$'}label\"; if (${'$'}ids) { docker rm -f ${'$'}ids 2>&1 | Out-Null }"
76+
}
77+
noProfile = false
6878
}
6979
}
7080

@@ -144,7 +154,17 @@ object PublicDeployment : BuildType({
144154
path = "DockerBuild.ps1"
145155
}
146156
noProfile = false
147-
scriptArgs = "-Script Build.ps1 -ImageName postsharpengineering-2023.2 -NoBuildImage publish --configuration Public --timeout %Publish.Timeout% %Publish.Arguments%"
157+
scriptArgs = "-Script Build.ps1 -ImageName postsharpengineering-2023.2 -NoBuildImage -Label %system.teamcity.buildType.id%_%build.number% publish --configuration Public --timeout %Publish.Timeout% %Publish.Arguments%"
158+
}
159+
powerShell {
160+
name = "Cleanup Docker containers"
161+
id = "DockerCleanup"
162+
executionMode = BuildStep.ExecutionMode.ALWAYS
163+
edition = PowerShellStep.Edition.Core
164+
scriptMode = script {
165+
content = "${'$'}label = \"%system.teamcity.buildType.id%_%build.number%\"; ${'$'}ids = docker ps -a -q --filter \"label=postsharp.build=${'$'}label\"; if (${'$'}ids) { docker rm -f ${'$'}ids 2>&1 | Out-Null }"
166+
}
167+
noProfile = false
148168
}
149169
}
150170

@@ -163,15 +183,13 @@ object PublicDeployment : BuildType({
163183
}
164184

165185
dependencies {
166-
dependency(PublicBuild) {
167-
snapshot {
168-
onDependencyFailure = FailureAction.FAIL_TO_START
169-
}
186+
snapshot(PublicBuild) {
187+
onDependencyFailure = FailureAction.FAIL_TO_START
188+
}
170189

171-
artifacts {
172-
cleanDestination = true
173-
artifactRules = "+:artifacts/publish/public/**/*=>artifacts/publish/public\n+:artifacts/publish/private/**/*=>artifacts/publish/private"
174-
}
190+
artifacts(PublicBuild) {
191+
cleanDestination = true
192+
artifactRules = "+:artifacts/publish/public/**/*=>artifacts/publish/public\n+:artifacts/publish/private/**/*=>artifacts/publish/private"
175193
}
176194
}
177195

@@ -214,7 +232,17 @@ object VersionBump : BuildType({
214232
path = "DockerBuild.ps1"
215233
}
216234
noProfile = false
217-
scriptArgs = "-Script Build.ps1 -ImageName postsharpengineering-2023.2 -NoBuildImage bump --timeout %Bump.Timeout% %Bump.Arguments%"
235+
scriptArgs = "-Script Build.ps1 -ImageName postsharpengineering-2023.2 -NoBuildImage -Label %system.teamcity.buildType.id%_%build.number% bump --timeout %Bump.Timeout% %Bump.Arguments%"
236+
}
237+
powerShell {
238+
name = "Cleanup Docker containers"
239+
id = "DockerCleanup"
240+
executionMode = BuildStep.ExecutionMode.ALWAYS
241+
edition = PowerShellStep.Edition.Core
242+
scriptMode = script {
243+
content = "${'$'}label = \"%system.teamcity.buildType.id%_%build.number%\"; ${'$'}ids = docker ps -a -q --filter \"label=postsharp.build=${'$'}label\"; if (${'$'}ids) { docker rm -f ${'$'}ids 2>&1 | Out-Null }"
244+
}
245+
noProfile = false
218246
}
219247
}
220248

DockerBuild.ps1

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@
9494
.PARAMETER Ports
9595
Port mappings from host to container (e.g., "8080:80", "3000").
9696
97+
.PARAMETER Label
98+
Label to apply to the container for identification (e.g., for cleanup of orphaned build containers).
99+
The label is set as "postsharp.build=<value>" on the container.
100+
97101
.PARAMETER BuildArgs
98102
Arguments passed to Build.ps1 within the container (or Claude prompt if -Claude is specified).
99103
@@ -138,12 +142,13 @@ param(
138142
[string]$Dockerfile, # Path to custom Dockerfile (defaults to Dockerfile or Dockerfile.claude based on -Claude).
139143
[string]$RegistryImage, # Use a pre-built image from a registry, skipping Dockerfile build entirely.
140144
[switch]$NoInit, # Do not generate or call Init.g.ps1 (skips git config, safe.directory, etc).
141-
[string]$Isolation = 'process', # Docker isolation mode (process or hyperv). Memory/CPU limits only apply to hyperv.
142-
[string]$Memory, # Docker memory limit (e.g., "8g"). Only used with hyperv isolation.
145+
[string]$Isolation = 'hyperv', # Docker isolation mode (process or hyperv). Memory/CPU limits only apply to hyperv.
146+
[string]$Memory = '16g', # Docker memory limit (e.g., "8g"). Only used with hyperv isolation.
143147
[int]$Cpus = [Environment]::ProcessorCount, # Docker CPU limit. Only used with hyperv isolation.
144148
[string[]]$Mount, # Additional directories to mount from host (readonly by default, append :w for writable). Supports * and ** glob patterns.
145149
[string[]]$Env, # Additional environment variables to pass from host to container.
146150
[string[]]$Ports, # Port mappings from host to container (e.g., "8080:80", "3000").
151+
[string]$Label, # Label to apply to the container (e.g., for identifying build containers for cleanup).
147152
[Parameter(ValueFromRemainingArguments)]
148153
[string[]]$BuildArgs # Arguments passed to `Build.ps1` within the container (or Claude prompt if -Claude is specified).
149154
)
@@ -1413,12 +1418,12 @@ $envVarAssignments$gitConfigCommands$postInitCommands
14131418
}
14141419
}
14151420

1416-
# If no existing container, kill any stopped containers with same image to avoid conflicts
1421+
# If no existing container, remove any containers with same image to avoid conflicts
14171422
if (-not $existingContainerId)
14181423
{
1419-
docker ps -q --filter "ancestor=$ImageTag" | ForEach-Object {
1420-
Write-Host "Killing container $_"
1421-
docker kill $_
1424+
docker ps -a -q --filter "ancestor=$ImageTag" | ForEach-Object {
1425+
Write-Host "Removing container $_"
1426+
docker rm -f $_ 2>&1 | Out-Null
14221427
}
14231428
}
14241429

@@ -1807,6 +1812,12 @@ RUN if [ -n "`$MOUNTPOINTS" ]; then \
18071812
}
18081813
}
18091814

1815+
# Add label for container identification (used for cleanup of orphaned containers)
1816+
if ($Label)
1817+
{
1818+
$dockerCmd += @('--label', "postsharp.build=$Label")
1819+
}
1820+
18101821
if ($pwshArgs)
18111822
{
18121823
$dockerCmd += @('-w', $ContainerCallingDir, $ImageTag, $pwshPath, $pwshArgs, '-Command', $inlineScript)

eng/RunClaude.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ if ($Prompt)
231231

232232
# Clean up prompt file
233233
Remove-Item $promptFile -ErrorAction SilentlyContinue
234+
235+
Write-Host "Claude exited with code $exitCode" -ForegroundColor $(if ($exitCode -eq 0) { "Green" } else { "Red" })
234236
exit $exitCode
235237
}
236238
else
Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,55 @@
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 PostSharp.Engineering.BuildTools.ContinuousIntegration.TeamCity.Arguments;
4-
using PostSharp.Engineering.BuildTools.Docker;
5-
using System;
6-
using System.Collections.Generic;
7-
using System.Linq;
8-
9-
namespace PostSharp.Engineering.BuildTools.ContinuousIntegration.TeamCity.BuildSteps;
10-
11-
internal abstract class BuildStep
12-
{
13-
private readonly DockerSpec? _dockerSpec;
14-
15-
private readonly List<BuildConfigurationParameter> _parameters = new();
16-
17-
protected BuildStep( DockerSpec? dockerSpec )
18-
{
19-
this._dockerSpec = dockerSpec;
20-
}
21-
22-
public IReadOnlyList<BuildConfigurationParameter> BuildConfigurationParameters => this._parameters;
23-
24-
protected void AddParameter( BuildConfigurationParameter parameter ) => this._parameters.Add( parameter );
25-
26-
public abstract string GenerateTeamCityCode();
27-
28-
public void InsertPrerequisites( IReadOnlyList<BuildStep> previousSteps, Action<BuildStep> addStep )
29-
{
30-
if ( this._dockerSpec != null )
31-
{
32-
var prepareImageStep = previousSteps
33-
.OfType<EngineeringPrepareImageBuildStep>()
34-
.SingleOrDefault( i => i.DockerSpec.ImageName == this._dockerSpec.ImageName );
35-
36-
if ( prepareImageStep == null )
37-
{
38-
addStep( new EngineeringPrepareImageBuildStep( "PrepareImage", this._dockerSpec ) );
39-
}
40-
}
41-
}
42-
43-
/// <summary>
44-
/// Gets a time that should be added to the complete build configuration timeout.
45-
/// </summary>
46-
public virtual TimeSpan AdditionalTimeout { get; init; } = TimeSpan.Zero;
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 PostSharp.Engineering.BuildTools.ContinuousIntegration.TeamCity.Arguments;
4+
using PostSharp.Engineering.BuildTools.Docker;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
9+
namespace PostSharp.Engineering.BuildTools.ContinuousIntegration.TeamCity.BuildSteps;
10+
11+
internal enum BuildStepExecutionMode
12+
{
13+
Default,
14+
Always
15+
}
16+
17+
internal abstract class BuildStep
18+
{
19+
private readonly DockerSpec? _dockerSpec;
20+
21+
private readonly List<BuildConfigurationParameter> _parameters = new();
22+
23+
protected BuildStep( DockerSpec? dockerSpec )
24+
{
25+
this._dockerSpec = dockerSpec;
26+
}
27+
28+
public IReadOnlyList<BuildConfigurationParameter> BuildConfigurationParameters => this._parameters;
29+
30+
protected void AddParameter( BuildConfigurationParameter parameter ) => this._parameters.Add( parameter );
31+
32+
public abstract string GenerateTeamCityCode();
33+
34+
public void InsertPrerequisites( IReadOnlyList<BuildStep> previousSteps, Action<BuildStep> addStep )
35+
{
36+
if ( this._dockerSpec != null )
37+
{
38+
var prepareImageStep = previousSteps
39+
.OfType<EngineeringPrepareImageBuildStep>()
40+
.SingleOrDefault( i => i.DockerSpec.ImageName == this._dockerSpec.ImageName );
41+
42+
if ( prepareImageStep == null )
43+
{
44+
addStep( new EngineeringPrepareImageBuildStep( "PrepareImage", this._dockerSpec ) );
45+
}
46+
}
47+
}
48+
49+
public BuildStepExecutionMode ExecutionMode { get; init; }
50+
51+
/// <summary>
52+
/// Gets a time that should be added to the complete build configuration timeout.
53+
/// </summary>
54+
public virtual TimeSpan AdditionalTimeout { get; init; } = TimeSpan.Zero;
4755
}

src/PostSharp.Engineering.BuildTools/ContinuousIntegration/TeamCity/BuildSteps/PowerShellCommandBuildStep.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@ public PowerShellCommandBuildStep(
3131

3232
public override string GenerateTeamCityCode()
3333
{
34+
var executionModeCode = this.ExecutionMode == BuildStepExecutionMode.Always
35+
? "\n executionMode = BuildStep.ExecutionMode.ALWAYS"
36+
: "";
37+
3438
return $@" powerShell {{
3539
name = ""{KotlinHelper.EscapeString( this.Name )}""
36-
id = ""{this.Id}""
40+
id = ""{this.Id}""{executionModeCode}
3741
edition = PowerShellStep.Edition.Core{(this.WorkingDirectory == null ? "" : $@"
3842
workingDir = ""{this.WorkingDirectory.Replace( Path.DirectorySeparatorChar, '/' )}""")}
3943
scriptMode = script {{

src/PostSharp.Engineering.BuildTools/ContinuousIntegration/TeamCity/BuildSteps/PowerShellScriptBuildStep.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public PowerShellScriptBuildStep(
4242
{
4343
this.ScriptPath = "DockerBuild.ps1";
4444
var dockerfileArg = dockerSpec.Dockerfile != null ? $" -Dockerfile {dockerSpec.Dockerfile}" : "";
45-
this.ScriptArguments = $"-Script {scriptPath} -ImageName {dockerSpec.ImageName}{dockerfileArg} -NoBuildImage {scriptArguments} {buildParameterValue}";
45+
this.ScriptArguments = $"-Script {scriptPath} -ImageName {dockerSpec.ImageName}{dockerfileArg} -NoBuildImage -Label %system.teamcity.buildType.id%_%build.number% {scriptArguments} {buildParameterValue}";
4646
}
4747

4848
if ( areCustomArgumentsAllowed )
Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
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-
5-
namespace PostSharp.Engineering.BuildTools.ContinuousIntegration.TeamCity;
6-
7-
internal static class KotlinHelper
8-
{
9-
public static string EscapeString( string value )
10-
{
11-
// Escape for Kotlin string: \ => \\, " => \"
12-
return value
13-
.Replace( "\\", "\\\\", StringComparison.Ordinal )
14-
.Replace( "\"", "\\\"", StringComparison.Ordinal );
15-
}
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+
5+
namespace PostSharp.Engineering.BuildTools.ContinuousIntegration.TeamCity;
6+
7+
internal static class KotlinHelper
8+
{
9+
public static string EscapeString( string value )
10+
{
11+
// Escape for Kotlin string: \ => \\, " => \", $ => ${'$'}
12+
return value
13+
.Replace( "\\", "\\\\", StringComparison.Ordinal )
14+
.Replace( "\"", "\\\"", StringComparison.Ordinal )
15+
.Replace( "$", "${'$'}", StringComparison.Ordinal );
16+
}
1617
}

src/PostSharp.Engineering.BuildTools/ContinuousIntegration/TeamCity/TeamCityBuildConfiguration.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,20 @@ void AddBuildStep( BuildStep newStep )
115115
}
116116
}
117117

118+
// If any step uses Docker, add a cleanup step that always runs to remove orphaned containers.
119+
if ( allBuildSteps.OfType<EngineeringPrepareImageBuildStep>().Any() )
120+
{
121+
allBuildSteps.Add(
122+
new PowerShellCommandBuildStep(
123+
"DockerCleanup",
124+
"Cleanup Docker containers",
125+
"$label = \"%system.teamcity.buildType.id%_%build.number%\"; $ids = docker ps -a -q --filter \"label=postsharp.build=$label\"; if ($ids) { docker rm -f $ids 2>&1 | Out-Null }",
126+
null )
127+
{
128+
ExecutionMode = BuildStepExecutionMode.Always
129+
} );
130+
}
131+
118132
var buildParameters = new List<BuildConfigurationParameter>();
119133

120134
buildParameters.AddRange( allBuildSteps.SelectMany( s => s.BuildConfigurationParameters ) );

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@
9494
.PARAMETER Ports
9595
Port mappings from host to container (e.g., "8080:80", "3000").
9696
97+
.PARAMETER Label
98+
Label to apply to the container for identification (e.g., for cleanup of orphaned build containers).
99+
The label is set as "postsharp.build=<value>" on the container.
100+
97101
.PARAMETER BuildArgs
98102
Arguments passed to Build.ps1 within the container (or Claude prompt if -Claude is specified).
99103
@@ -138,12 +142,13 @@ param(
138142
[string]$Dockerfile, # Path to custom Dockerfile (defaults to Dockerfile or Dockerfile.claude based on -Claude).
139143
[string]$RegistryImage, # Use a pre-built image from a registry, skipping Dockerfile build entirely.
140144
[switch]$NoInit, # Do not generate or call Init.g.ps1 (skips git config, safe.directory, etc).
141-
[string]$Isolation = 'process', # Docker isolation mode (process or hyperv). Memory/CPU limits only apply to hyperv.
142-
[string]$Memory, # Docker memory limit (e.g., "8g"). Only used with hyperv isolation.
145+
[string]$Isolation = 'hyperv', # Docker isolation mode (process or hyperv). Memory/CPU limits only apply to hyperv.
146+
[string]$Memory = '16g', # Docker memory limit (e.g., "8g"). Only used with hyperv isolation.
143147
[int]$Cpus = [Environment]::ProcessorCount, # Docker CPU limit. Only used with hyperv isolation.
144148
[string[]]$Mount, # Additional directories to mount from host (readonly by default, append :w for writable). Supports * and ** glob patterns.
145149
[string[]]$Env, # Additional environment variables to pass from host to container.
146150
[string[]]$Ports, # Port mappings from host to container (e.g., "8080:80", "3000").
151+
[string]$Label, # Label to apply to the container (e.g., for identifying build containers for cleanup).
147152
[Parameter(ValueFromRemainingArguments)]
148153
[string[]]$BuildArgs # Arguments passed to `Build.ps1` within the container (or Claude prompt if -Claude is specified).
149154
)
@@ -1413,12 +1418,12 @@ $envVarAssignments$gitConfigCommands$postInitCommands
14131418
}
14141419
}
14151420

1416-
# If no existing container, kill any stopped containers with same image to avoid conflicts
1421+
# If no existing container, remove any containers with same image to avoid conflicts
14171422
if (-not $existingContainerId)
14181423
{
1419-
docker ps -q --filter "ancestor=$ImageTag" | ForEach-Object {
1420-
Write-Host "Killing container $_"
1421-
docker kill $_
1424+
docker ps -a -q --filter "ancestor=$ImageTag" | ForEach-Object {
1425+
Write-Host "Removing container $_"
1426+
docker rm -f $_ 2>&1 | Out-Null
14221427
}
14231428
}
14241429

@@ -1807,6 +1812,12 @@ RUN if [ -n "`$MOUNTPOINTS" ]; then \
18071812
}
18081813
}
18091814

1815+
# Add label for container identification (used for cleanup of orphaned containers)
1816+
if ($Label)
1817+
{
1818+
$dockerCmd += @('--label', "postsharp.build=$Label")
1819+
}
1820+
18101821
if ($pwshArgs)
18111822
{
18121823
$dockerCmd += @('-w', $ContainerCallingDir, $ImageTag, $pwshPath, $pwshArgs, '-Command', $inlineScript)

0 commit comments

Comments
 (0)