@opencode-cloud/core 3.0.15 → 3.1.3
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/Cargo.toml +2 -2
- package/README.md +33 -0
- package/package.json +1 -1
- package/src/config/schema.rs +14 -4
- package/src/docker/Dockerfile +316 -63
- package/src/docker/image.rs +415 -66
- package/src/docker/progress.rs +67 -67
package/src/docker/image.rs
CHANGED
|
@@ -7,21 +7,35 @@ use super::progress::ProgressReporter;
|
|
|
7
7
|
use super::{
|
|
8
8
|
DOCKERFILE, DockerClient, DockerError, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT,
|
|
9
9
|
};
|
|
10
|
-
use bollard::image::{BuildImageOptions, CreateImageOptions};
|
|
10
|
+
use bollard::image::{BuildImageOptions, BuilderVersion, CreateImageOptions};
|
|
11
|
+
use bollard::moby::buildkit::v1::StatusResponse as BuildkitStatusResponse;
|
|
11
12
|
use bollard::models::BuildInfoAux;
|
|
12
13
|
use bytes::Bytes;
|
|
13
14
|
use flate2::Compression;
|
|
14
15
|
use flate2::write::GzEncoder;
|
|
15
16
|
use futures_util::StreamExt;
|
|
16
|
-
use std::collections::VecDeque;
|
|
17
|
+
use std::collections::{HashMap, VecDeque};
|
|
18
|
+
use std::env;
|
|
19
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
17
20
|
use tar::Builder as TarBuilder;
|
|
18
21
|
use tracing::{debug, warn};
|
|
19
22
|
|
|
20
|
-
///
|
|
21
|
-
const
|
|
23
|
+
/// Default number of recent build log lines to capture for error context
|
|
24
|
+
const DEFAULT_BUILD_LOG_BUFFER_SIZE: usize = 20;
|
|
22
25
|
|
|
23
|
-
///
|
|
24
|
-
const
|
|
26
|
+
/// Default number of error lines to capture separately
|
|
27
|
+
const DEFAULT_ERROR_LOG_BUFFER_SIZE: usize = 10;
|
|
28
|
+
|
|
29
|
+
/// Read a log buffer size from env with bounds
|
|
30
|
+
fn read_log_buffer_size(var_name: &str, default: usize) -> usize {
|
|
31
|
+
let Ok(value) = env::var(var_name) else {
|
|
32
|
+
return default;
|
|
33
|
+
};
|
|
34
|
+
let Ok(parsed) = value.trim().parse::<usize>() else {
|
|
35
|
+
return default;
|
|
36
|
+
};
|
|
37
|
+
parsed.clamp(5, 500)
|
|
38
|
+
}
|
|
25
39
|
|
|
26
40
|
/// Check if a line looks like an error message
|
|
27
41
|
fn is_error_line(line: &str) -> bool {
|
|
@@ -77,9 +91,20 @@ pub async fn build_image(
|
|
|
77
91
|
.map_err(|e| DockerError::Build(format!("Failed to create build context: {e}")))?;
|
|
78
92
|
|
|
79
93
|
// Set up build options
|
|
94
|
+
// Explicitly use BuildKit builder to support cache mounts (--mount=type=cache)
|
|
95
|
+
// BuildKit requires a unique session ID for each build
|
|
96
|
+
let session_id = format!(
|
|
97
|
+
"opencode-cloud-build-{}",
|
|
98
|
+
SystemTime::now()
|
|
99
|
+
.duration_since(UNIX_EPOCH)
|
|
100
|
+
.unwrap_or_default()
|
|
101
|
+
.as_nanos()
|
|
102
|
+
);
|
|
80
103
|
let options = BuildImageOptions {
|
|
81
104
|
t: full_name.clone(),
|
|
82
105
|
dockerfile: "Dockerfile".to_string(),
|
|
106
|
+
version: BuilderVersion::BuilderBuildKit,
|
|
107
|
+
session: Some(session_id),
|
|
83
108
|
rm: true,
|
|
84
109
|
nocache: no_cache,
|
|
85
110
|
..Default::default()
|
|
@@ -95,67 +120,42 @@ pub async fn build_image(
|
|
|
95
120
|
progress.add_spinner("build", "Initializing...");
|
|
96
121
|
|
|
97
122
|
let mut maybe_image_id = None;
|
|
98
|
-
let mut
|
|
99
|
-
let mut error_logs: VecDeque<String> = VecDeque::with_capacity(ERROR_LOG_BUFFER_SIZE);
|
|
123
|
+
let mut log_state = BuildLogState::new();
|
|
100
124
|
|
|
101
125
|
while let Some(result) = stream.next().await {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
126
|
+
let Ok(info) = result else {
|
|
127
|
+
return Err(handle_stream_error(
|
|
128
|
+
"Build failed",
|
|
129
|
+
result.expect_err("checked error").to_string(),
|
|
130
|
+
&log_state,
|
|
131
|
+
progress,
|
|
132
|
+
));
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
handle_stream_message(&info, progress, &mut log_state);
|
|
136
|
+
|
|
137
|
+
if let Some(error_msg) = info.error {
|
|
138
|
+
progress.abandon_all(&error_msg);
|
|
139
|
+
let context = format_build_error_with_context(
|
|
140
|
+
&error_msg,
|
|
141
|
+
&log_state.recent_logs,
|
|
142
|
+
&log_state.error_logs,
|
|
143
|
+
&log_state.recent_buildkit_logs,
|
|
144
|
+
);
|
|
145
|
+
return Err(DockerError::Build(context));
|
|
146
|
+
}
|
|
123
147
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
148
|
+
if let Some(aux) = info.aux {
|
|
149
|
+
match aux {
|
|
150
|
+
BuildInfoAux::Default(image_id) => {
|
|
151
|
+
if let Some(id) = image_id.id {
|
|
152
|
+
maybe_image_id = Some(id);
|
|
128
153
|
}
|
|
129
154
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if let Some(error_msg) = info.error {
|
|
133
|
-
progress.abandon_all(&error_msg);
|
|
134
|
-
let context =
|
|
135
|
-
format_build_error_with_context(&error_msg, &recent_logs, &error_logs);
|
|
136
|
-
return Err(DockerError::Build(context));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Capture the image ID from aux field
|
|
140
|
-
if let Some(aux) = info.aux {
|
|
141
|
-
match aux {
|
|
142
|
-
BuildInfoAux::Default(image_id) => {
|
|
143
|
-
if let Some(id) = image_id.id {
|
|
144
|
-
maybe_image_id = Some(id);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
BuildInfoAux::BuildKit(_) => {
|
|
148
|
-
// BuildKit responses are handled via stream messages
|
|
149
|
-
}
|
|
150
|
-
}
|
|
155
|
+
BuildInfoAux::BuildKit(status) => {
|
|
156
|
+
handle_buildkit_status(&status, progress, &mut log_state);
|
|
151
157
|
}
|
|
152
158
|
}
|
|
153
|
-
Err(e) => {
|
|
154
|
-
progress.abandon_all("Build failed");
|
|
155
|
-
let context =
|
|
156
|
-
format_build_error_with_context(&e.to_string(), &recent_logs, &error_logs);
|
|
157
|
-
return Err(DockerError::Build(context));
|
|
158
|
-
}
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -166,6 +166,317 @@ pub async fn build_image(
|
|
|
166
166
|
Ok(full_name)
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
struct BuildLogState {
|
|
170
|
+
recent_logs: VecDeque<String>,
|
|
171
|
+
error_logs: VecDeque<String>,
|
|
172
|
+
recent_buildkit_logs: VecDeque<String>,
|
|
173
|
+
build_log_buffer_size: usize,
|
|
174
|
+
error_log_buffer_size: usize,
|
|
175
|
+
last_buildkit_vertex: Option<String>,
|
|
176
|
+
last_buildkit_vertex_id: Option<String>,
|
|
177
|
+
buildkit_logs_by_vertex_id: HashMap<String, String>,
|
|
178
|
+
vertex_name_by_vertex_id: HashMap<String, String>,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
impl BuildLogState {
|
|
182
|
+
fn new() -> Self {
|
|
183
|
+
let build_log_buffer_size = read_log_buffer_size(
|
|
184
|
+
"OPENCODE_DOCKER_BUILD_LOG_TAIL",
|
|
185
|
+
DEFAULT_BUILD_LOG_BUFFER_SIZE,
|
|
186
|
+
);
|
|
187
|
+
let error_log_buffer_size = read_log_buffer_size(
|
|
188
|
+
"OPENCODE_DOCKER_BUILD_ERROR_TAIL",
|
|
189
|
+
DEFAULT_ERROR_LOG_BUFFER_SIZE,
|
|
190
|
+
);
|
|
191
|
+
Self {
|
|
192
|
+
recent_logs: VecDeque::with_capacity(build_log_buffer_size),
|
|
193
|
+
error_logs: VecDeque::with_capacity(error_log_buffer_size),
|
|
194
|
+
recent_buildkit_logs: VecDeque::with_capacity(build_log_buffer_size),
|
|
195
|
+
build_log_buffer_size,
|
|
196
|
+
error_log_buffer_size,
|
|
197
|
+
last_buildkit_vertex: None,
|
|
198
|
+
last_buildkit_vertex_id: None,
|
|
199
|
+
buildkit_logs_by_vertex_id: HashMap::new(),
|
|
200
|
+
vertex_name_by_vertex_id: HashMap::new(),
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn handle_stream_message(
|
|
206
|
+
info: &bollard::models::BuildInfo,
|
|
207
|
+
progress: &mut ProgressReporter,
|
|
208
|
+
state: &mut BuildLogState,
|
|
209
|
+
) {
|
|
210
|
+
let Some(stream_msg) = info.stream.as_deref() else {
|
|
211
|
+
return;
|
|
212
|
+
};
|
|
213
|
+
let msg = stream_msg.trim();
|
|
214
|
+
if msg.is_empty() {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if progress.is_plain_output() {
|
|
219
|
+
eprint!("{stream_msg}");
|
|
220
|
+
} else {
|
|
221
|
+
let has_runtime_vertex = state
|
|
222
|
+
.last_buildkit_vertex
|
|
223
|
+
.as_deref()
|
|
224
|
+
.is_some_and(|name| name.starts_with("[runtime "));
|
|
225
|
+
let is_internal_msg = msg.contains("[internal]");
|
|
226
|
+
if !(has_runtime_vertex && is_internal_msg) {
|
|
227
|
+
progress.update_spinner("build", stream_msg);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if state.recent_logs.len() >= state.build_log_buffer_size {
|
|
232
|
+
state.recent_logs.pop_front();
|
|
233
|
+
}
|
|
234
|
+
state.recent_logs.push_back(msg.to_string());
|
|
235
|
+
|
|
236
|
+
if is_error_line(msg) {
|
|
237
|
+
if state.error_logs.len() >= state.error_log_buffer_size {
|
|
238
|
+
state.error_logs.pop_front();
|
|
239
|
+
}
|
|
240
|
+
state.error_logs.push_back(msg.to_string());
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if msg.starts_with("Step ") {
|
|
244
|
+
debug!("Build step: {}", msg);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fn handle_buildkit_status(
|
|
249
|
+
status: &BuildkitStatusResponse,
|
|
250
|
+
progress: &mut ProgressReporter,
|
|
251
|
+
state: &mut BuildLogState,
|
|
252
|
+
) {
|
|
253
|
+
let latest_logs = append_buildkit_logs(&mut state.buildkit_logs_by_vertex_id, status);
|
|
254
|
+
update_buildkit_vertex_names(&mut state.vertex_name_by_vertex_id, status);
|
|
255
|
+
let (vertex_id, vertex_name) =
|
|
256
|
+
match select_latest_buildkit_vertex(status, &state.vertex_name_by_vertex_id) {
|
|
257
|
+
Some((vertex_id, vertex_name)) => (vertex_id, vertex_name),
|
|
258
|
+
None => {
|
|
259
|
+
let Some(log_entry) = latest_logs.last() else {
|
|
260
|
+
return;
|
|
261
|
+
};
|
|
262
|
+
let name = state
|
|
263
|
+
.vertex_name_by_vertex_id
|
|
264
|
+
.get(&log_entry.vertex_id)
|
|
265
|
+
.cloned()
|
|
266
|
+
.or_else(|| state.last_buildkit_vertex.clone())
|
|
267
|
+
.unwrap_or_else(|| format_vertex_fallback_label(&log_entry.vertex_id));
|
|
268
|
+
(log_entry.vertex_id.clone(), name)
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
record_buildkit_logs(state, &latest_logs, &vertex_id, &vertex_name);
|
|
272
|
+
state.last_buildkit_vertex_id = Some(vertex_id);
|
|
273
|
+
if state.last_buildkit_vertex.as_deref() != Some(&vertex_name) {
|
|
274
|
+
state.last_buildkit_vertex = Some(vertex_name.clone());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let message = if progress.is_plain_output() {
|
|
278
|
+
vertex_name
|
|
279
|
+
} else if let Some(log_entry) = latest_logs.last() {
|
|
280
|
+
format!("{vertex_name} · {}", log_entry.message)
|
|
281
|
+
} else {
|
|
282
|
+
vertex_name
|
|
283
|
+
};
|
|
284
|
+
progress.update_spinner("build", &message);
|
|
285
|
+
|
|
286
|
+
if progress.is_plain_output() {
|
|
287
|
+
for log_entry in latest_logs {
|
|
288
|
+
eprintln!("[{}] {}", log_entry.vertex_id, log_entry.message);
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let (Some(current_id), Some(current_name)) = (
|
|
294
|
+
state.last_buildkit_vertex_id.as_ref(),
|
|
295
|
+
state.last_buildkit_vertex.as_ref(),
|
|
296
|
+
) else {
|
|
297
|
+
return;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
let name = state
|
|
301
|
+
.vertex_name_by_vertex_id
|
|
302
|
+
.get(current_id)
|
|
303
|
+
.unwrap_or(current_name);
|
|
304
|
+
// Keep non-verbose output on the spinner line only.
|
|
305
|
+
let _ = name;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fn handle_stream_error(
|
|
309
|
+
prefix: &str,
|
|
310
|
+
error_str: String,
|
|
311
|
+
state: &BuildLogState,
|
|
312
|
+
progress: &mut ProgressReporter,
|
|
313
|
+
) -> DockerError {
|
|
314
|
+
progress.abandon_all(prefix);
|
|
315
|
+
|
|
316
|
+
let buildkit_hint = if error_str.contains("mount")
|
|
317
|
+
|| error_str.contains("--mount")
|
|
318
|
+
|| state
|
|
319
|
+
.recent_logs
|
|
320
|
+
.iter()
|
|
321
|
+
.any(|log| log.contains("--mount") && log.contains("cache"))
|
|
322
|
+
{
|
|
323
|
+
"\n\nNote: This Dockerfile uses BuildKit cache mounts (--mount=type=cache).\n\
|
|
324
|
+
The build is configured to use BuildKit, but the Docker daemon may not support it.\n\
|
|
325
|
+
Ensure BuildKit is enabled in Docker Desktop settings and the daemon is restarted."
|
|
326
|
+
} else {
|
|
327
|
+
""
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
let context = format!(
|
|
331
|
+
"{}{}",
|
|
332
|
+
format_build_error_with_context(
|
|
333
|
+
&error_str,
|
|
334
|
+
&state.recent_logs,
|
|
335
|
+
&state.error_logs,
|
|
336
|
+
&state.recent_buildkit_logs,
|
|
337
|
+
),
|
|
338
|
+
buildkit_hint
|
|
339
|
+
);
|
|
340
|
+
DockerError::Build(context)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
fn update_buildkit_vertex_names(
|
|
344
|
+
vertex_name_by_vertex_id: &mut HashMap<String, String>,
|
|
345
|
+
status: &BuildkitStatusResponse,
|
|
346
|
+
) {
|
|
347
|
+
for vertex in &status.vertexes {
|
|
348
|
+
if vertex.name.is_empty() {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
vertex_name_by_vertex_id
|
|
352
|
+
.entry(vertex.digest.clone())
|
|
353
|
+
.or_insert_with(|| vertex.name.clone());
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
fn select_latest_buildkit_vertex(
|
|
358
|
+
status: &BuildkitStatusResponse,
|
|
359
|
+
vertex_name_by_vertex_id: &HashMap<String, String>,
|
|
360
|
+
) -> Option<(String, String)> {
|
|
361
|
+
let mut best_runtime: Option<(u32, String, String)> = None;
|
|
362
|
+
let mut fallback: Option<(String, String)> = None;
|
|
363
|
+
|
|
364
|
+
for vertex in &status.vertexes {
|
|
365
|
+
let name = if vertex.name.is_empty() {
|
|
366
|
+
vertex_name_by_vertex_id.get(&vertex.digest).cloned()
|
|
367
|
+
} else {
|
|
368
|
+
Some(vertex.name.clone())
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
let Some(name) = name else {
|
|
372
|
+
continue;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
if fallback.is_none() && !name.starts_with("[internal]") {
|
|
376
|
+
fallback = Some((vertex.digest.clone(), name.clone()));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if let Some(step) = parse_runtime_step(&name) {
|
|
380
|
+
match &best_runtime {
|
|
381
|
+
Some((best_step, _, _)) if *best_step >= step => {}
|
|
382
|
+
_ => {
|
|
383
|
+
best_runtime = Some((step, vertex.digest.clone(), name.clone()));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if let Some((_, digest, name)) = best_runtime {
|
|
390
|
+
Some((digest, name))
|
|
391
|
+
} else {
|
|
392
|
+
fallback.or_else(|| {
|
|
393
|
+
status.vertexes.iter().find_map(|vertex| {
|
|
394
|
+
let name = if vertex.name.is_empty() {
|
|
395
|
+
vertex_name_by_vertex_id.get(&vertex.digest).cloned()
|
|
396
|
+
} else {
|
|
397
|
+
Some(vertex.name.clone())
|
|
398
|
+
};
|
|
399
|
+
name.map(|resolved| (vertex.digest.clone(), resolved))
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
fn parse_runtime_step(name: &str) -> Option<u32> {
|
|
406
|
+
let prefix = "[runtime ";
|
|
407
|
+
let start = name.find(prefix)? + prefix.len();
|
|
408
|
+
let rest = &name[start..];
|
|
409
|
+
let end = rest.find('/')?;
|
|
410
|
+
rest[..end].trim().parse::<u32>().ok()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
fn format_vertex_fallback_label(vertex_id: &str) -> String {
|
|
414
|
+
let short = vertex_id
|
|
415
|
+
.strip_prefix("sha256:")
|
|
416
|
+
.unwrap_or(vertex_id)
|
|
417
|
+
.chars()
|
|
418
|
+
.take(12)
|
|
419
|
+
.collect::<String>();
|
|
420
|
+
format!("vertex {short}")
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
fn record_buildkit_logs(
|
|
424
|
+
state: &mut BuildLogState,
|
|
425
|
+
latest_logs: &[BuildkitLogEntry],
|
|
426
|
+
current_vertex_id: &str,
|
|
427
|
+
current_vertex_name: &str,
|
|
428
|
+
) {
|
|
429
|
+
for log_entry in latest_logs {
|
|
430
|
+
let name = state
|
|
431
|
+
.vertex_name_by_vertex_id
|
|
432
|
+
.get(&log_entry.vertex_id)
|
|
433
|
+
.cloned()
|
|
434
|
+
.or_else(|| {
|
|
435
|
+
if log_entry.vertex_id == current_vertex_id {
|
|
436
|
+
Some(current_vertex_name.to_string())
|
|
437
|
+
} else {
|
|
438
|
+
None
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
.unwrap_or_else(|| format_vertex_fallback_label(&log_entry.vertex_id));
|
|
442
|
+
|
|
443
|
+
let message = log_entry.message.replace('\r', "").trim_end().to_string();
|
|
444
|
+
if message.is_empty() {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if state.recent_buildkit_logs.len() >= state.build_log_buffer_size {
|
|
449
|
+
state.recent_buildkit_logs.pop_front();
|
|
450
|
+
}
|
|
451
|
+
state
|
|
452
|
+
.recent_buildkit_logs
|
|
453
|
+
.push_back(format!("[{name}] {message}"));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#[derive(Debug, Clone)]
|
|
458
|
+
struct BuildkitLogEntry {
|
|
459
|
+
vertex_id: String,
|
|
460
|
+
message: String,
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
fn append_buildkit_logs(
|
|
464
|
+
logs: &mut HashMap<String, String>,
|
|
465
|
+
status: &BuildkitStatusResponse,
|
|
466
|
+
) -> Vec<BuildkitLogEntry> {
|
|
467
|
+
let mut latest: Vec<BuildkitLogEntry> = Vec::new();
|
|
468
|
+
|
|
469
|
+
for log in &status.logs {
|
|
470
|
+
let vertex_id = log.vertex.clone();
|
|
471
|
+
let message = String::from_utf8_lossy(&log.msg).to_string();
|
|
472
|
+
let entry = logs.entry(vertex_id.clone()).or_default();
|
|
473
|
+
entry.push_str(&message);
|
|
474
|
+
latest.push(BuildkitLogEntry { vertex_id, message });
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
latest
|
|
478
|
+
}
|
|
479
|
+
|
|
169
480
|
/// Pull the opencode image from registry with automatic fallback
|
|
170
481
|
///
|
|
171
482
|
/// Tries GHCR first, falls back to Docker Hub on failure.
|
|
@@ -326,6 +637,7 @@ fn format_build_error_with_context(
|
|
|
326
637
|
error: &str,
|
|
327
638
|
recent_logs: &VecDeque<String>,
|
|
328
639
|
error_logs: &VecDeque<String>,
|
|
640
|
+
recent_buildkit_logs: &VecDeque<String>,
|
|
329
641
|
) -> String {
|
|
330
642
|
let mut message = String::new();
|
|
331
643
|
|
|
@@ -351,6 +663,15 @@ fn format_build_error_with_context(
|
|
|
351
663
|
}
|
|
352
664
|
}
|
|
353
665
|
|
|
666
|
+
// Add recent BuildKit log context if available
|
|
667
|
+
if !recent_buildkit_logs.is_empty() {
|
|
668
|
+
message.push_str("\n\nRecent BuildKit output:");
|
|
669
|
+
for line in recent_buildkit_logs {
|
|
670
|
+
message.push_str("\n ");
|
|
671
|
+
message.push_str(line);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
354
675
|
// Add recent log context if available
|
|
355
676
|
if !recent_logs.is_empty() {
|
|
356
677
|
message.push_str("\n\nRecent build output:");
|
|
@@ -358,6 +679,9 @@ fn format_build_error_with_context(
|
|
|
358
679
|
message.push_str("\n ");
|
|
359
680
|
message.push_str(line);
|
|
360
681
|
}
|
|
682
|
+
} else if recent_buildkit_logs.is_empty() {
|
|
683
|
+
message.push_str("\n\nNo build output was received from the Docker daemon.");
|
|
684
|
+
message.push_str("\nThis usually means the build failed before any logs were streamed.");
|
|
361
685
|
}
|
|
362
686
|
|
|
363
687
|
// Add actionable suggestions based on common error patterns
|
|
@@ -432,9 +756,14 @@ mod tests {
|
|
|
432
756
|
logs.push_back("Step 2/5 : RUN apt-get update".to_string());
|
|
433
757
|
logs.push_back("E: Unable to fetch some archives".to_string());
|
|
434
758
|
let error_logs = VecDeque::new();
|
|
759
|
+
let buildkit_logs = VecDeque::new();
|
|
435
760
|
|
|
436
|
-
let result =
|
|
437
|
-
|
|
761
|
+
let result = format_build_error_with_context(
|
|
762
|
+
"Build failed: exit code 1",
|
|
763
|
+
&logs,
|
|
764
|
+
&error_logs,
|
|
765
|
+
&buildkit_logs,
|
|
766
|
+
);
|
|
438
767
|
|
|
439
768
|
assert!(result.contains("Build failed: exit code 1"));
|
|
440
769
|
assert!(result.contains("Recent build output:"));
|
|
@@ -446,7 +775,9 @@ mod tests {
|
|
|
446
775
|
fn format_build_error_handles_empty_logs() {
|
|
447
776
|
let logs = VecDeque::new();
|
|
448
777
|
let error_logs = VecDeque::new();
|
|
449
|
-
let
|
|
778
|
+
let buildkit_logs = VecDeque::new();
|
|
779
|
+
let result =
|
|
780
|
+
format_build_error_with_context("Stream error", &logs, &error_logs, &buildkit_logs);
|
|
450
781
|
|
|
451
782
|
assert!(result.contains("Stream error"));
|
|
452
783
|
assert!(!result.contains("Recent build output:"));
|
|
@@ -456,7 +787,13 @@ mod tests {
|
|
|
456
787
|
fn format_build_error_adds_network_suggestion() {
|
|
457
788
|
let logs = VecDeque::new();
|
|
458
789
|
let error_logs = VecDeque::new();
|
|
459
|
-
let
|
|
790
|
+
let buildkit_logs = VecDeque::new();
|
|
791
|
+
let result = format_build_error_with_context(
|
|
792
|
+
"connection timeout",
|
|
793
|
+
&logs,
|
|
794
|
+
&error_logs,
|
|
795
|
+
&buildkit_logs,
|
|
796
|
+
);
|
|
460
797
|
|
|
461
798
|
assert!(result.contains("Check your network connection"));
|
|
462
799
|
}
|
|
@@ -465,7 +802,13 @@ mod tests {
|
|
|
465
802
|
fn format_build_error_adds_disk_suggestion() {
|
|
466
803
|
let logs = VecDeque::new();
|
|
467
804
|
let error_logs = VecDeque::new();
|
|
468
|
-
let
|
|
805
|
+
let buildkit_logs = VecDeque::new();
|
|
806
|
+
let result = format_build_error_with_context(
|
|
807
|
+
"no space left on device",
|
|
808
|
+
&logs,
|
|
809
|
+
&error_logs,
|
|
810
|
+
&buildkit_logs,
|
|
811
|
+
);
|
|
469
812
|
|
|
470
813
|
assert!(result.contains("Free up disk space"));
|
|
471
814
|
}
|
|
@@ -480,7 +823,13 @@ mod tests {
|
|
|
480
823
|
error_logs.push_back("error: failed to compile dust".to_string());
|
|
481
824
|
error_logs.push_back("error: failed to compile glow".to_string());
|
|
482
825
|
|
|
483
|
-
let
|
|
826
|
+
let buildkit_logs = VecDeque::new();
|
|
827
|
+
let result = format_build_error_with_context(
|
|
828
|
+
"Build failed",
|
|
829
|
+
&recent_logs,
|
|
830
|
+
&error_logs,
|
|
831
|
+
&buildkit_logs,
|
|
832
|
+
);
|
|
484
833
|
|
|
485
834
|
assert!(result.contains("Potential errors detected during build:"));
|
|
486
835
|
assert!(result.contains("failed to compile dust"));
|