dank-ai 1.0.25 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/bin/dank +2 -0
- package/lib/cli/production-build.js +177 -31
- package/lib/docker/manager.js +81 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,6 +169,8 @@ dank build:prod --push # Build and push to registry (CLI only)
|
|
|
169
169
|
dank build:prod --tag v1.0.0 # Build with custom tag
|
|
170
170
|
dank build:prod --registry ghcr.io # Build for specific registry
|
|
171
171
|
dank build:prod --force # Force rebuild without cache
|
|
172
|
+
dank build:prod --output-metadata deployment.json # Generate deployment metadata
|
|
173
|
+
dank build:prod --json # Output JSON summary to stdout
|
|
172
174
|
```
|
|
173
175
|
|
|
174
176
|
> **💡 Push Control**: The `--push` option is the only way to push images to registries. Agent configuration defines naming, CLI controls pushing.
|
|
@@ -190,6 +192,8 @@ dank build:prod --registry ghcr.io # Build for GitHub Container Registry
|
|
|
190
192
|
dank build:prod --namespace mycompany # Build with custom namespace
|
|
191
193
|
dank build:prod --tag-by-agent # Use agent name as tag (common repo)
|
|
192
194
|
dank build:prod --force # Force rebuild without cache
|
|
195
|
+
dank build:prod --output-metadata <file> # Output deployment metadata JSON
|
|
196
|
+
dank build:prod --json # Output machine-readable JSON summary
|
|
193
197
|
```
|
|
194
198
|
|
|
195
199
|
## 🤖 Agent Configuration
|
|
@@ -719,6 +723,93 @@ dank build:prod --force --push
|
|
|
719
723
|
dank build:prod --tag release-2024.1 --push
|
|
720
724
|
```
|
|
721
725
|
|
|
726
|
+
**Deployment Metadata Output:**
|
|
727
|
+
```bash
|
|
728
|
+
# Generate deployment metadata JSON file
|
|
729
|
+
dank build:prod --output-metadata deployment.json
|
|
730
|
+
|
|
731
|
+
# Build, push, and generate metadata
|
|
732
|
+
dank build:prod --push --output-metadata deployment.json
|
|
733
|
+
|
|
734
|
+
# Use with custom configuration
|
|
735
|
+
dank build:prod --config production.config.js --output-metadata deployment.json
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
The `--output-metadata` option generates a JSON file containing all deployment information needed for your backend infrastructure:
|
|
739
|
+
- **Base image** used (`setBaseImage()` value)
|
|
740
|
+
- **Prompting server** configuration (protocol, port, authentication, maxConnections)
|
|
741
|
+
- **Resource limits** (memory, CPU, timeout)
|
|
742
|
+
- **Ports** that need to be opened
|
|
743
|
+
- **Features enabled** (direct prompting, HTTP API, event handlers)
|
|
744
|
+
- **HTTP server** configuration (if enabled)
|
|
745
|
+
- **LLM provider** and model information
|
|
746
|
+
- **Event handlers** registered
|
|
747
|
+
- **Environment variables** required
|
|
748
|
+
- **Build options** (registry, namespace, tag, image name)
|
|
749
|
+
|
|
750
|
+
This metadata file is perfect for CI/CD pipelines to automatically configure your deployment infrastructure, determine which ports to open, and which features to enable/disable.
|
|
751
|
+
|
|
752
|
+
**Example Metadata Output:**
|
|
753
|
+
```json
|
|
754
|
+
{
|
|
755
|
+
"project": "my-agent-project",
|
|
756
|
+
"buildTimestamp": "2024-01-15T10:30:00.000Z",
|
|
757
|
+
"agents": [
|
|
758
|
+
{
|
|
759
|
+
"name": "customer-service",
|
|
760
|
+
"imageName": "ghcr.io/mycompany/customer-service:v1.2.0",
|
|
761
|
+
"baseImage": {
|
|
762
|
+
"full": "deltadarkly/dank-agent-base:nodejs-20",
|
|
763
|
+
"tag": "nodejs-20"
|
|
764
|
+
},
|
|
765
|
+
"promptingServer": {
|
|
766
|
+
"protocol": "http",
|
|
767
|
+
"port": 3000,
|
|
768
|
+
"authentication": false,
|
|
769
|
+
"maxConnections": 50,
|
|
770
|
+
"timeout": 30000
|
|
771
|
+
},
|
|
772
|
+
"resources": {
|
|
773
|
+
"memory": "512m",
|
|
774
|
+
"cpu": 1,
|
|
775
|
+
"timeout": 30000
|
|
776
|
+
},
|
|
777
|
+
"ports": [
|
|
778
|
+
{
|
|
779
|
+
"port": 3000,
|
|
780
|
+
"protocol": "http",
|
|
781
|
+
"description": "Direct prompting server"
|
|
782
|
+
}
|
|
783
|
+
],
|
|
784
|
+
"features": {
|
|
785
|
+
"directPrompting": true,
|
|
786
|
+
"httpApi": false,
|
|
787
|
+
"eventHandlers": true
|
|
788
|
+
},
|
|
789
|
+
"llm": {
|
|
790
|
+
"provider": "openai",
|
|
791
|
+
"model": "gpt-3.5-turbo",
|
|
792
|
+
"temperature": 0.7,
|
|
793
|
+
"maxTokens": 1000
|
|
794
|
+
},
|
|
795
|
+
"handlers": ["request_output", "request_output:start"],
|
|
796
|
+
"buildOptions": {
|
|
797
|
+
"registry": "ghcr.io",
|
|
798
|
+
"namespace": "mycompany",
|
|
799
|
+
"tag": "v1.2.0",
|
|
800
|
+
"tagByAgent": false
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
],
|
|
804
|
+
"summary": {
|
|
805
|
+
"total": 1,
|
|
806
|
+
"successful": 1,
|
|
807
|
+
"failed": 0,
|
|
808
|
+
"pushed": 1
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
722
813
|
#### 🏷️ **Image Naming Convention**
|
|
723
814
|
|
|
724
815
|
**Default (Per-Agent Repository):**
|
package/bin/dank
CHANGED
|
@@ -90,6 +90,8 @@ program
|
|
|
90
90
|
.option('--registry <registry>', 'Docker registry URL (e.g., docker.io, ghcr.io)')
|
|
91
91
|
.option('--namespace <namespace>', 'Docker namespace/organization')
|
|
92
92
|
.option('--tag-by-agent', 'Use agent name as the image tag (common image name)')
|
|
93
|
+
.option('--json', 'Output machine-readable JSON summary to stdout')
|
|
94
|
+
.option('--output-metadata <file>', 'Output deployment metadata JSON file')
|
|
93
95
|
.option('--force', 'Force rebuild without cache')
|
|
94
96
|
.action(async (options) => {
|
|
95
97
|
const { productionBuildCommand } = require('../lib/cli/production-build');
|
|
@@ -8,6 +8,116 @@ const chalk = require('chalk');
|
|
|
8
8
|
const { DockerManager } = require('../docker/manager');
|
|
9
9
|
const analytics = require('../analytics');
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Extract deployment metadata from an agent configuration
|
|
13
|
+
*/
|
|
14
|
+
function extractAgentMetadata(agent, buildOptions, imageName) {
|
|
15
|
+
const config = agent.config || {};
|
|
16
|
+
const dockerConfig = config.docker || {};
|
|
17
|
+
const resources = config.resources || {};
|
|
18
|
+
const communication = config.communication || {};
|
|
19
|
+
const directPrompting = communication.directPrompting || {};
|
|
20
|
+
const httpConfig = config.http || {};
|
|
21
|
+
const llmConfig = config.llm || {};
|
|
22
|
+
|
|
23
|
+
// Extract base image tag (remove prefix)
|
|
24
|
+
// setBaseImage() sets docker.baseImage to "deltadarkly/dank-agent-base:tag"
|
|
25
|
+
const baseImage = dockerConfig.baseImage || '';
|
|
26
|
+
let baseImageTag = '';
|
|
27
|
+
if (baseImage.includes(':')) {
|
|
28
|
+
baseImageTag = baseImage.split(':').slice(1).join(':'); // Handle tags with colons
|
|
29
|
+
} else if (baseImage) {
|
|
30
|
+
baseImageTag = baseImage;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Extract prompting server configuration
|
|
34
|
+
const promptingServer = directPrompting.enabled ? {
|
|
35
|
+
protocol: directPrompting.protocol || 'http',
|
|
36
|
+
port: dockerConfig.port || 3000,
|
|
37
|
+
authentication: directPrompting.authentication || false,
|
|
38
|
+
maxConnections: directPrompting.maxConnections || 50,
|
|
39
|
+
timeout: directPrompting.timeout || 30000
|
|
40
|
+
} : null;
|
|
41
|
+
|
|
42
|
+
// Extract resources configuration
|
|
43
|
+
const resourcesConfig = {
|
|
44
|
+
memory: resources.memory || '512m',
|
|
45
|
+
cpu: resources.cpu || 1,
|
|
46
|
+
timeout: resources.timeout || 30000
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Extract HTTP server configuration if enabled
|
|
50
|
+
const httpServer = httpConfig.enabled ? {
|
|
51
|
+
port: httpConfig.port || 3000,
|
|
52
|
+
host: httpConfig.host || '0.0.0.0',
|
|
53
|
+
cors: httpConfig.cors !== false,
|
|
54
|
+
routes: httpConfig.routes ? Array.from(httpConfig.routes.keys()).map(routeKey => {
|
|
55
|
+
const [method, path] = routeKey.split(':');
|
|
56
|
+
return { method, path };
|
|
57
|
+
}) : []
|
|
58
|
+
} : null;
|
|
59
|
+
|
|
60
|
+
// Extract handler information
|
|
61
|
+
const handlers = agent.handlers ? Array.from(agent.handlers.keys()) : [];
|
|
62
|
+
|
|
63
|
+
// Extract LLM configuration (without sensitive data)
|
|
64
|
+
const llm = llmConfig.provider ? {
|
|
65
|
+
provider: llmConfig.provider,
|
|
66
|
+
model: llmConfig.model || 'gpt-3.5-turbo',
|
|
67
|
+
temperature: llmConfig.temperature || 0.7,
|
|
68
|
+
maxTokens: llmConfig.maxTokens || 1000,
|
|
69
|
+
baseURL: llmConfig.baseURL || null
|
|
70
|
+
} : null;
|
|
71
|
+
|
|
72
|
+
// Collect ports that need to be opened
|
|
73
|
+
const ports = [];
|
|
74
|
+
if (promptingServer) {
|
|
75
|
+
ports.push({
|
|
76
|
+
port: promptingServer.port,
|
|
77
|
+
protocol: promptingServer.protocol,
|
|
78
|
+
description: 'Direct prompting server'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (httpServer) {
|
|
82
|
+
ports.push({
|
|
83
|
+
port: httpServer.port,
|
|
84
|
+
protocol: 'http',
|
|
85
|
+
description: 'HTTP API server'
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Determine features enabled
|
|
90
|
+
const features = {
|
|
91
|
+
directPrompting: directPrompting.enabled || false,
|
|
92
|
+
httpApi: communication.httpApi?.enabled || httpConfig.enabled || false,
|
|
93
|
+
eventHandlers: communication.eventHandlers?.enabled || handlers.length > 0 || false
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
name: agent.name,
|
|
98
|
+
imageName: imageName,
|
|
99
|
+
baseImage: {
|
|
100
|
+
full: baseImage,
|
|
101
|
+
tag: baseImageTag
|
|
102
|
+
},
|
|
103
|
+
buildOptions: {
|
|
104
|
+
registry: buildOptions.registry || null,
|
|
105
|
+
namespace: buildOptions.namespace || null,
|
|
106
|
+
tag: buildOptions.tag || 'latest',
|
|
107
|
+
tagByAgent: buildOptions.tagByAgent || false
|
|
108
|
+
},
|
|
109
|
+
promptingServer: promptingServer,
|
|
110
|
+
resources: resourcesConfig,
|
|
111
|
+
httpServer: httpServer,
|
|
112
|
+
ports: ports,
|
|
113
|
+
features: features,
|
|
114
|
+
llm: llm,
|
|
115
|
+
handlers: handlers,
|
|
116
|
+
hasPrompt: !!config.prompt,
|
|
117
|
+
environment: config.environment || {}
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
11
121
|
async function productionBuildCommand(options) {
|
|
12
122
|
// Track production build command
|
|
13
123
|
await analytics.trackCommand('build:prod', true, {
|
|
@@ -35,6 +145,8 @@ async function productionBuildCommand(options) {
|
|
|
35
145
|
|
|
36
146
|
// Build production images for each agent
|
|
37
147
|
const buildResults = [];
|
|
148
|
+
const metadata = [];
|
|
149
|
+
|
|
38
150
|
for (const agent of config.agents) {
|
|
39
151
|
try {
|
|
40
152
|
console.log(chalk.blue(`📦 Building production image for agent: ${agent.name}`));
|
|
@@ -67,6 +179,10 @@ async function productionBuildCommand(options) {
|
|
|
67
179
|
pushed: result.pushed || false
|
|
68
180
|
});
|
|
69
181
|
|
|
182
|
+
// Extract deployment metadata for successfully built agents
|
|
183
|
+
const agentMetadata = extractAgentMetadata(agent, buildOptions, result.imageName);
|
|
184
|
+
metadata.push(agentMetadata);
|
|
185
|
+
|
|
70
186
|
console.log(chalk.green(`✅ Successfully built: ${result.imageName}`));
|
|
71
187
|
if (result.pushed) {
|
|
72
188
|
console.log(chalk.green(`🚀 Successfully pushed: ${result.imageName}`));
|
|
@@ -82,41 +198,71 @@ async function productionBuildCommand(options) {
|
|
|
82
198
|
}
|
|
83
199
|
}
|
|
84
200
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
201
|
+
// Output metadata file if requested
|
|
202
|
+
if (options.outputMetadata) {
|
|
203
|
+
const metadataPath = path.resolve(options.outputMetadata);
|
|
204
|
+
const metadataOutput = {
|
|
205
|
+
project: config.name,
|
|
206
|
+
buildTimestamp: new Date().toISOString(),
|
|
207
|
+
agents: metadata,
|
|
208
|
+
summary: {
|
|
209
|
+
total: metadata.length,
|
|
210
|
+
successful: buildResults.filter(r => r.success).length,
|
|
211
|
+
failed: buildResults.filter(r => !r.success).length,
|
|
212
|
+
pushed: buildResults.filter(r => r.pushed).length
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
await fs.writeJson(metadataPath, metadataOutput, { spaces: 2 });
|
|
217
|
+
console.log(chalk.cyan(`\n📄 Deployment metadata saved to: ${metadataPath}`));
|
|
99
218
|
}
|
|
100
219
|
|
|
101
|
-
//
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
220
|
+
// Output
|
|
221
|
+
if (options.json) {
|
|
222
|
+
const payload = {
|
|
223
|
+
success: buildResults.every(r => r.success),
|
|
224
|
+
results: buildResults
|
|
225
|
+
};
|
|
226
|
+
// Always print JSON to stdout for machine consumption
|
|
227
|
+
console.log(JSON.stringify(payload));
|
|
228
|
+
process.exit(payload.success ? 0 : 1);
|
|
229
|
+
} else {
|
|
230
|
+
// Human summary
|
|
231
|
+
console.log(chalk.yellow('\n📊 Build Summary:'));
|
|
232
|
+
console.log(chalk.gray('================'));
|
|
233
|
+
|
|
234
|
+
const successful = buildResults.filter(r => r.success);
|
|
235
|
+
const failed = buildResults.filter(r => !r.success);
|
|
236
|
+
const pushed = buildResults.filter(r => r.pushed);
|
|
108
237
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
238
|
+
console.log(chalk.green(`✅ Successful builds: ${successful.length}`));
|
|
239
|
+
if (pushed.length > 0) {
|
|
240
|
+
console.log(chalk.blue(`🚀 Pushed to registry: ${pushed.length}`));
|
|
241
|
+
}
|
|
242
|
+
if (failed.length > 0) {
|
|
243
|
+
console.log(chalk.red(`❌ Failed builds: ${failed.length}`));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// List built images
|
|
247
|
+
if (successful.length > 0) {
|
|
248
|
+
console.log(chalk.cyan('\n📦 Built Images:'));
|
|
249
|
+
successful.forEach(result => {
|
|
250
|
+
console.log(chalk.gray(` - ${result.imageName}`));
|
|
251
|
+
});
|
|
252
|
+
}
|
|
117
253
|
|
|
118
|
-
|
|
119
|
-
|
|
254
|
+
// List failed builds
|
|
255
|
+
if (failed.length > 0) {
|
|
256
|
+
console.log(chalk.red('\n❌ Failed Builds:'));
|
|
257
|
+
failed.forEach(result => {
|
|
258
|
+
console.log(chalk.gray(` - ${result.agent}: ${result.error}`));
|
|
259
|
+
});
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log(chalk.green('\n🎉 Production build completed successfully!'));
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
120
266
|
|
|
121
267
|
} catch (error) {
|
|
122
268
|
console.error(chalk.red('❌ Production build failed:'), error.message);
|
package/lib/docker/manager.js
CHANGED
|
@@ -23,6 +23,26 @@ const analytics = require("../analytics");
|
|
|
23
23
|
const execAsync = promisify(exec);
|
|
24
24
|
|
|
25
25
|
class DockerManager {
|
|
26
|
+
/**
|
|
27
|
+
* Resolve docker executable path
|
|
28
|
+
*/
|
|
29
|
+
async resolveDockerCommand() {
|
|
30
|
+
const dockerPaths = [
|
|
31
|
+
"/usr/local/bin/docker",
|
|
32
|
+
"/opt/homebrew/bin/docker",
|
|
33
|
+
"/usr/bin/docker",
|
|
34
|
+
"docker",
|
|
35
|
+
];
|
|
36
|
+
for (const path of dockerPaths) {
|
|
37
|
+
try {
|
|
38
|
+
await execAsync(`${path} --version`);
|
|
39
|
+
return path;
|
|
40
|
+
} catch (_) {
|
|
41
|
+
// continue
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw new Error("Docker executable not found in expected locations");
|
|
45
|
+
}
|
|
26
46
|
constructor(options = {}) {
|
|
27
47
|
this.docker = new Docker(options.dockerOptions || {});
|
|
28
48
|
this.logger =
|
|
@@ -1129,14 +1149,21 @@ class DockerManager {
|
|
|
1129
1149
|
|
|
1130
1150
|
try {
|
|
1131
1151
|
const buildContext = await this.createAgentBuildContext(agent);
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1152
|
+
const dockerCmd = await this.resolveDockerCommand();
|
|
1153
|
+
|
|
1154
|
+
const buildCommand = [
|
|
1155
|
+
dockerCmd,
|
|
1156
|
+
'buildx',
|
|
1157
|
+
'build',
|
|
1158
|
+
'--platform', 'linux/amd64,linux/arm64',
|
|
1159
|
+
'--tag', imageName,
|
|
1160
|
+
'--file', path.join(buildContext, 'Dockerfile'),
|
|
1161
|
+
'--load',
|
|
1162
|
+
...(options.rebuild || options.noCache ? ['--no-cache'] : []),
|
|
1163
|
+
buildContext
|
|
1164
|
+
].join(' ');
|
|
1165
|
+
|
|
1166
|
+
await this.runCommand(buildCommand, `Agent ${agent.name} build`);
|
|
1140
1167
|
|
|
1141
1168
|
this.logger.info(`Agent image '${imageName}' built successfully`);
|
|
1142
1169
|
|
|
@@ -1172,25 +1199,21 @@ class DockerManager {
|
|
|
1172
1199
|
return sanitized;
|
|
1173
1200
|
};
|
|
1174
1201
|
|
|
1175
|
-
//
|
|
1202
|
+
//construct full repo name
|
|
1176
1203
|
let repoName;
|
|
1177
|
-
if
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
// Default: per-agent repository name
|
|
1183
|
-
repoName = agent.name.toLowerCase();
|
|
1204
|
+
if(!tagByAgent){
|
|
1205
|
+
|
|
1206
|
+
repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}${repoName}`;
|
|
1207
|
+
}else{
|
|
1208
|
+
repoName = `${registry?`${registry}/`:''}${namespace?`${namespace}/`:''}`;
|
|
1184
1209
|
}
|
|
1210
|
+
|
|
1211
|
+
repoName = repoName.replace(/\/+$/, '');
|
|
1185
1212
|
|
|
1186
|
-
// Compose full repository path
|
|
1187
|
-
let fullRepo = repoName;
|
|
1188
|
-
if (namespace) fullRepo = `${namespace}/${fullRepo}`;
|
|
1189
|
-
if (registry) fullRepo = `${registry}/${fullRepo}`;
|
|
1190
1213
|
|
|
1191
1214
|
// Final tag selection
|
|
1192
1215
|
const finalTag = tagByAgent ? normalizeTag(agent.name) : tag;
|
|
1193
|
-
const imageName = `${
|
|
1216
|
+
const imageName = `${repoName}:${finalTag}`;
|
|
1194
1217
|
|
|
1195
1218
|
this.logger.info(
|
|
1196
1219
|
`Building production image for agent: ${agent.name} -> ${imageName}`
|
|
@@ -1198,17 +1221,21 @@ class DockerManager {
|
|
|
1198
1221
|
|
|
1199
1222
|
try {
|
|
1200
1223
|
const buildContext = await this.createAgentBuildContext(agent);
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1224
|
+
const dockerCmd = await this.resolveDockerCommand();
|
|
1225
|
+
|
|
1226
|
+
const buildCommand = [
|
|
1227
|
+
dockerCmd,
|
|
1228
|
+
'buildx',
|
|
1229
|
+
'build',
|
|
1230
|
+
'--platform', 'linux/amd64,linux/arm64',
|
|
1231
|
+
'--tag', imageName,
|
|
1232
|
+
'--file', path.join(buildContext, 'Dockerfile'),
|
|
1233
|
+
'--load',
|
|
1234
|
+
...(force ? ['--no-cache'] : []),
|
|
1235
|
+
buildContext
|
|
1236
|
+
].join(' ');
|
|
1237
|
+
|
|
1238
|
+
await this.runCommand(buildCommand, `Production build for ${agent.name}`);
|
|
1212
1239
|
|
|
1213
1240
|
this.logger.info(`Production image '${imageName}' built successfully`);
|
|
1214
1241
|
|
|
@@ -1217,19 +1244,29 @@ class DockerManager {
|
|
|
1217
1244
|
|
|
1218
1245
|
let pushed = false;
|
|
1219
1246
|
|
|
1220
|
-
// Push to registry if requested
|
|
1247
|
+
// Push to registry if requested (use docker CLI for reliability)
|
|
1221
1248
|
if (push) {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
)
|
|
1232
|
-
|
|
1249
|
+
const dockerCmd = await this.resolveDockerCommand();
|
|
1250
|
+
const maxAttempts = 3;
|
|
1251
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1252
|
+
try {
|
|
1253
|
+
this.logger.info(`Pushing image to registry (attempt ${attempt}/${maxAttempts}): ${imageName}`);
|
|
1254
|
+
await this.runCommand(`${dockerCmd} push ${imageName}`, `Docker push ${imageName}`);
|
|
1255
|
+
this.logger.info(`Successfully pushed image: ${imageName}`);
|
|
1256
|
+
pushed = true;
|
|
1257
|
+
break;
|
|
1258
|
+
} catch (pushError) {
|
|
1259
|
+
const msg = pushError?.message || '';
|
|
1260
|
+
this.logger.warn(`Push attempt ${attempt} failed: ${msg}`);
|
|
1261
|
+
if (msg.match(/denied|unauthorized|authentication required/i)) {
|
|
1262
|
+
this.logger.warn("Authentication issue detected. Please ensure you're logged in: docker login <registry>");
|
|
1263
|
+
break; // don't retry auth failures automatically
|
|
1264
|
+
}
|
|
1265
|
+
if (attempt < maxAttempts) {
|
|
1266
|
+
await this.sleep(2000 * attempt);
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1233
1270
|
}
|
|
1234
1271
|
}
|
|
1235
1272
|
|