bitfab 0.64.2 → 0.64.4
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/cloudReplay.py +229 -44
- package/dist/{chunk-GLWAET3G.js → chunk-I6B5R2FP.js} +2 -2
- package/dist/{chunk-FX4X7LOW.js → chunk-OEHQPSOB.js} +3 -3
- package/dist/{chunk-ENORQO5K.js → chunk-UMZDRAAK.js} +6 -6
- package/dist/{chunk-O632VPWV.js → chunk-XASGXGQN.js} +2 -2
- package/dist/{chunk-O632VPWV.js.map → chunk-XASGXGQN.js.map} +1 -1
- package/dist/{chunk-XHUW3NY6.js → chunk-ZPVUSLIU.js} +2 -2
- package/dist/{chunk-XHUW3NY6.js.map → chunk-ZPVUSLIU.js.map} +1 -1
- package/dist/{http-MFXRJ6RS.js → http-53RWIBXJ.js} +2 -2
- package/dist/{http-IOT2GDD2.js → http-Y6T6T2AP.js} +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/node.cjs +1 -1
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +3 -3
- package/dist/{replay-QZQYRTAO.js → replay-SIKHFPKA.js} +3 -3
- package/dist/replayCli.js +2 -2
- package/dist/seedCli.js +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-GLWAET3G.js.map → chunk-I6B5R2FP.js.map} +0 -0
- /package/dist/{chunk-FX4X7LOW.js.map → chunk-OEHQPSOB.js.map} +0 -0
- /package/dist/{chunk-ENORQO5K.js.map → chunk-UMZDRAAK.js.map} +0 -0
- /package/dist/{http-IOT2GDD2.js.map → http-53RWIBXJ.js.map} +0 -0
- /package/dist/{http-MFXRJ6RS.js.map → http-Y6T6T2AP.js.map} +0 -0
- /package/dist/{replay-QZQYRTAO.js.map → replay-SIKHFPKA.js.map} +0 -0
package/cloudReplay.py
CHANGED
|
@@ -40,6 +40,7 @@ OUTPUT_BEGIN = "bitfab-replay-output-begin"
|
|
|
40
40
|
OUTPUT_END = "bitfab-replay-output-end"
|
|
41
41
|
LOG_TAIL_LINES = 40
|
|
42
42
|
OUTPUT_LIMIT = 16 * 1024 * 1024
|
|
43
|
+
DISK_LIMIT = 1024 * 1024 * 1024
|
|
43
44
|
NEW_FILES_LIMIT = 20 * 1024 * 1024
|
|
44
45
|
POLL_SECONDS = 5
|
|
45
46
|
ITEM_ERROR_FIELDS = (
|
|
@@ -52,6 +53,14 @@ ITEM_ERROR_FIELDS = (
|
|
|
52
53
|
ITEM_ID_FIELDS = ("originalTraceId", "original_trace_id", "traceId", "trace_id")
|
|
53
54
|
ITEM_ERROR_LINES = 20
|
|
54
55
|
ITEM_ERROR_LENGTH = 500
|
|
56
|
+
SUMMARY_ERROR_ITEMS = 100
|
|
57
|
+
EXPERIMENT_FIELDS = {
|
|
58
|
+
"experimentId": ("experimentId", "experiment_id"),
|
|
59
|
+
"experimentUrl": ("experimentUrl", "experiment_url"),
|
|
60
|
+
"testRunId": ("testRunId", "test_run_id"),
|
|
61
|
+
"testRunUrl": ("testRunUrl", "test_run_url"),
|
|
62
|
+
"attempts": ("attempts",),
|
|
63
|
+
}
|
|
55
64
|
UUID = re.compile(r"^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$")
|
|
56
65
|
SHA = re.compile(r"^[0-9a-f]{40}$")
|
|
57
66
|
GITHUB_REMOTE = re.compile(
|
|
@@ -59,6 +68,15 @@ GITHUB_REMOTE = re.compile(
|
|
|
59
68
|
)
|
|
60
69
|
LOG_TIMESTAMP = re.compile(r"^?\d{4}-\d\d-\d\dT[\d:.]+Z ?")
|
|
61
70
|
EXPERIMENT_LINE = re.compile(rb"^\[replay\] Experiment ([0-9a-f-]{36}):")
|
|
71
|
+
PROGRESS_PREFIX = b"@@bitfab:progress "
|
|
72
|
+
PROGRESS_PAYLOAD_FIELDS = (
|
|
73
|
+
"input",
|
|
74
|
+
"result",
|
|
75
|
+
"originalOutput",
|
|
76
|
+
"original_output",
|
|
77
|
+
"selectiveReplay",
|
|
78
|
+
"selective_replay",
|
|
79
|
+
)
|
|
62
80
|
SECRET_REFERENCE = re.compile(
|
|
63
81
|
r"[\"']?([A-Za-z_][A-Za-z0-9_]*)[\"']?[ \t]*:[ \t]*[\"']?\$\{\{\s*secrets(?:\.([A-Za-z_][A-Za-z0-9_]*)|\[\s*[\"']([A-Za-z_][A-Za-z0-9_]*)[\"']\s*\])\s*\}\}"
|
|
64
82
|
)
|
|
@@ -77,7 +95,7 @@ FOLLOW_FLAGS = (
|
|
|
77
95
|
"--cloud-cleanup",
|
|
78
96
|
)
|
|
79
97
|
CLOUD_VALUE_FLAGS = (*FOLLOW_FLAGS, "--cloud-request-id", "--cloud-timeout")
|
|
80
|
-
CLOUD_SWITCHES = ("--cloud", "--cloud-preview", "--cloud-detach")
|
|
98
|
+
CLOUD_SWITCHES = ("--cloud", "--cloud-preview", "--cloud-detach", "--cloud-full-output")
|
|
81
99
|
RENAMED_FLAGS = {
|
|
82
100
|
"--cloud-dry-run": "--cloud-preview",
|
|
83
101
|
"--cloud-check": "--dry-run",
|
|
@@ -113,9 +131,17 @@ does not ignore) from the same directory, and this command prints the replay's o
|
|
|
113
131
|
and exits with its exit code. Credential-like files are refused. Requires git and
|
|
114
132
|
Python 3.10+ on macOS or Linux, with a github.com origin.
|
|
115
133
|
|
|
134
|
+
The replay's stderr comes back as it printed it. Its stdout result comes back as a
|
|
135
|
+
short summary instead of every item: the experiment ID and URL, how many traces were
|
|
136
|
+
replayed, same, changed, and errored, each errored trace with its error, and the exit
|
|
137
|
+
code. Every item's input and output are already saved in the experiment in Bitfab.
|
|
138
|
+
|
|
116
139
|
Options added by --cloud:
|
|
117
140
|
--cloud-preview List what the snapshot would contain and push nothing.
|
|
118
141
|
--cloud-detach Return after dispatch instead of waiting.
|
|
142
|
+
--cloud-full-output Print the replay's whole stdout result, every item included,
|
|
143
|
+
instead of the summary. Large runs can exceed what the
|
|
144
|
+
GitHub job log holds.
|
|
119
145
|
--cloud-timeout MIN Stop the replay after MIN minutes (1..7200).
|
|
120
146
|
--cloud-request-id ID Recover a submission whose response was lost.
|
|
121
147
|
|
|
@@ -310,6 +336,16 @@ def parse_environment_file(text):
|
|
|
310
336
|
return values
|
|
311
337
|
|
|
312
338
|
|
|
339
|
+
def secret_value(value):
|
|
340
|
+
if "\n" not in value:
|
|
341
|
+
return value
|
|
342
|
+
try:
|
|
343
|
+
parsed = json.loads(value)
|
|
344
|
+
except ValueError:
|
|
345
|
+
return value
|
|
346
|
+
return json.dumps(parsed, separators=(",", ":"))
|
|
347
|
+
|
|
348
|
+
|
|
313
349
|
def ensure_environment(repo, environment):
|
|
314
350
|
try:
|
|
315
351
|
api(repo, f"environments/{environment}")
|
|
@@ -412,7 +448,7 @@ def configure_secrets(argv):
|
|
|
412
448
|
repo,
|
|
413
449
|
*(["--env", environment] if environment else []),
|
|
414
450
|
],
|
|
415
|
-
input=values.get(name, values.get(targets[name])),
|
|
451
|
+
input=secret_value(values.get(name, values.get(targets[name]))),
|
|
416
452
|
env={**os.environ, "GH_TOKEN": token},
|
|
417
453
|
)
|
|
418
454
|
result["set"] = [targets[name] for name in found]
|
|
@@ -592,10 +628,8 @@ def validate_arguments(args):
|
|
|
592
628
|
if not isinstance(args, list) or not all(isinstance(value, str) for value in args):
|
|
593
629
|
raise ValueError("Replay arguments must be a list of strings")
|
|
594
630
|
for value in args:
|
|
595
|
-
if "\x00" in value or "\n" in value
|
|
596
|
-
raise ValueError(
|
|
597
|
-
"Replay options must be single-line values of at most 1000 characters"
|
|
598
|
-
)
|
|
631
|
+
if "\x00" in value or "\n" in value:
|
|
632
|
+
raise ValueError("Replay options must be single-line values")
|
|
599
633
|
if value.startswith("--cloud"):
|
|
600
634
|
raise ValueError("The runner replays locally and never dispatches again")
|
|
601
635
|
if sum(len(value) for value in args) > 16000:
|
|
@@ -639,6 +673,8 @@ def replay_request(root, parsed):
|
|
|
639
673
|
}
|
|
640
674
|
if "--cloud-timeout" in parsed["cloud"]:
|
|
641
675
|
request["timeoutMinutes"] = int(parsed["cloud"]["--cloud-timeout"])
|
|
676
|
+
if "--cloud-full-output" in parsed["switches"]:
|
|
677
|
+
request["fullOutput"] = True
|
|
642
678
|
return request, files
|
|
643
679
|
|
|
644
680
|
|
|
@@ -862,9 +898,7 @@ def outcome(record):
|
|
|
862
898
|
raise ValueError("Replay result does not match this execution")
|
|
863
899
|
record["exitCode"] = result["exitCode"]
|
|
864
900
|
parsed = last_json(result.get("stdout", "")) or {}
|
|
865
|
-
test_run = result.get("testRunId") or parsed.get(
|
|
866
|
-
"testRunId", parsed.get("test_run_id")
|
|
867
|
-
)
|
|
901
|
+
test_run = result.get("testRunId") or experiment_fields(parsed).get("testRunId")
|
|
868
902
|
if isinstance(test_run, str) and UUID.fullmatch(test_run):
|
|
869
903
|
record["testRunId"] = test_run
|
|
870
904
|
if result.get("stoppedEarly"):
|
|
@@ -889,7 +923,22 @@ def print_outcome(found):
|
|
|
889
923
|
for line in replay_output(lines) or []:
|
|
890
924
|
print(line, file=sys.stderr)
|
|
891
925
|
sys.stderr.flush()
|
|
926
|
+
if result.get("stdoutOmittedBytes"):
|
|
927
|
+
progress(
|
|
928
|
+
f"The replay also printed {result['stdoutOmittedBytes'] // (1024 * 1024)} MiB to stdout before its result, such as a library's logging; only the end was kept"
|
|
929
|
+
)
|
|
892
930
|
print(result.get("stdout", ""), end="", flush=True)
|
|
931
|
+
summary = last_json(result.get("stdout", "")) or {}
|
|
932
|
+
if summary.get("cloudSummary") is True:
|
|
933
|
+
progress(
|
|
934
|
+
"Printed a summary of the result; every item's input and output are in the experiment"
|
|
935
|
+
+ (
|
|
936
|
+
f" at {summary['experimentUrl']}"
|
|
937
|
+
if summary.get("experimentUrl")
|
|
938
|
+
else ""
|
|
939
|
+
)
|
|
940
|
+
+ ". Add --cloud-full-output to print every item here instead"
|
|
941
|
+
)
|
|
893
942
|
if record.get("stoppedEarly"):
|
|
894
943
|
progress(
|
|
895
944
|
f"Replay stopped early: {record['stoppedEarly']}. Traces that finished are saved"
|
|
@@ -1049,7 +1098,10 @@ def submit(root, repo, workflow, parsed, path):
|
|
|
1049
1098
|
def report_steps(record, shown):
|
|
1050
1099
|
job = replay_job(record)
|
|
1051
1100
|
for step in (job or {}).get("steps", []):
|
|
1052
|
-
if
|
|
1101
|
+
if (
|
|
1102
|
+
step.get("status") in ("in_progress", "completed")
|
|
1103
|
+
and step.get("name") not in shown
|
|
1104
|
+
):
|
|
1053
1105
|
shown.add(step.get("name"))
|
|
1054
1106
|
progress(step.get("name"))
|
|
1055
1107
|
|
|
@@ -1186,8 +1238,12 @@ def execute():
|
|
|
1186
1238
|
previous = signal.signal(signal.SIGTERM, raise_interrupt)
|
|
1187
1239
|
print(OUTPUT_BEGIN, flush=True)
|
|
1188
1240
|
try:
|
|
1189
|
-
code, stdout, stopped = run_command(
|
|
1190
|
-
directory,
|
|
1241
|
+
code, stdout, omitted, stopped = run_command(
|
|
1242
|
+
directory,
|
|
1243
|
+
args,
|
|
1244
|
+
request.get("timeoutMinutes"),
|
|
1245
|
+
experiment,
|
|
1246
|
+
summarize=request.get("fullOutput") is not True,
|
|
1191
1247
|
)
|
|
1192
1248
|
finally:
|
|
1193
1249
|
print(OUTPUT_END, flush=True)
|
|
@@ -1198,6 +1254,8 @@ def execute():
|
|
|
1198
1254
|
"exitCode": code,
|
|
1199
1255
|
"stdout": stdout,
|
|
1200
1256
|
}
|
|
1257
|
+
if omitted:
|
|
1258
|
+
result["stdoutOmittedBytes"] = omitted
|
|
1201
1259
|
if stopped:
|
|
1202
1260
|
result["stoppedEarly"] = stopped
|
|
1203
1261
|
if UUID.fullmatch(experiment.get("id", "")):
|
|
@@ -1224,6 +1282,15 @@ def check_secrets(root):
|
|
|
1224
1282
|
+ ", ".join(f"{name} (secret {targets[name]})" for name in empty)
|
|
1225
1283
|
+ ". GitHub passes a secret that does not exist as an empty string. Create each one under Settings, Secrets and variables, Actions, in the repository or in the job's Environment"
|
|
1226
1284
|
)
|
|
1285
|
+
for name in targets:
|
|
1286
|
+
if any(
|
|
1287
|
+
0 < len(line.strip()) < 4
|
|
1288
|
+
for line in os.environ.get(name, "").splitlines()[1:]
|
|
1289
|
+
):
|
|
1290
|
+
print(
|
|
1291
|
+
f"::warning title=Bitfab replay::{name} has a line of three characters or fewer, such as a lone brace in multi-line JSON, and GitHub masks that text everywhere in this log. Store it on one line; bitfab-replay --cloud-secrets does this for JSON",
|
|
1292
|
+
flush=True,
|
|
1293
|
+
)
|
|
1227
1294
|
|
|
1228
1295
|
|
|
1229
1296
|
def run_check_command(directory):
|
|
@@ -1259,10 +1326,75 @@ def item_errors(items):
|
|
|
1259
1326
|
text = " ".join(str(error).split())
|
|
1260
1327
|
if len(text) > ITEM_ERROR_LENGTH:
|
|
1261
1328
|
text = text[:ITEM_ERROR_LENGTH] + "..."
|
|
1262
|
-
|
|
1329
|
+
errored = {"originalTraceId": trace, "error": text}
|
|
1330
|
+
if isinstance(item.get("attempt"), int):
|
|
1331
|
+
errored["attempt"] = item["attempt"]
|
|
1332
|
+
errors.append(errored)
|
|
1263
1333
|
return errors
|
|
1264
1334
|
|
|
1265
1335
|
|
|
1336
|
+
def item_field(item, camel, snake):
|
|
1337
|
+
return item[camel] if camel in item else item.get(snake)
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def canonical(value):
|
|
1341
|
+
return json.dumps(value, separators=(",", ":"))
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def experiment_fields(parsed):
|
|
1345
|
+
fields = {}
|
|
1346
|
+
for name, spellings in EXPERIMENT_FIELDS.items():
|
|
1347
|
+
value = next(
|
|
1348
|
+
(parsed[key] for key in spellings if parsed.get(key) is not None), None
|
|
1349
|
+
)
|
|
1350
|
+
if value is not None:
|
|
1351
|
+
fields[name] = value
|
|
1352
|
+
return fields
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def replay_summary(parsed):
|
|
1356
|
+
items = [item for item in parsed["items"] if isinstance(item, dict)]
|
|
1357
|
+
replayed = [item for item in items if not carried_over(item)]
|
|
1358
|
+
errors = item_errors(replayed)
|
|
1359
|
+
counts = {
|
|
1360
|
+
"replayed": len(replayed),
|
|
1361
|
+
"carriedOver": len(items) - len(replayed),
|
|
1362
|
+
"same": 0,
|
|
1363
|
+
"changed": 0,
|
|
1364
|
+
"matchedExpected": 0,
|
|
1365
|
+
"missedExpected": 0,
|
|
1366
|
+
"errored": len(errors),
|
|
1367
|
+
}
|
|
1368
|
+
for item in replayed:
|
|
1369
|
+
if item_errored(item):
|
|
1370
|
+
continue
|
|
1371
|
+
equal = canonical(item.get("result")) == canonical(
|
|
1372
|
+
item_field(item, "originalOutput", "original_output")
|
|
1373
|
+
)
|
|
1374
|
+
if item_field(item, "ingestionType", "ingestion_type") == "seeded":
|
|
1375
|
+
counts["matchedExpected" if equal else "missedExpected"] += 1
|
|
1376
|
+
else:
|
|
1377
|
+
counts["same" if equal else "changed"] += 1
|
|
1378
|
+
summary = {
|
|
1379
|
+
"cloudSummary": True,
|
|
1380
|
+
**experiment_fields(parsed),
|
|
1381
|
+
"counts": counts,
|
|
1382
|
+
"erroredItems": errors[:SUMMARY_ERROR_ITEMS],
|
|
1383
|
+
}
|
|
1384
|
+
if len(errors) > SUMMARY_ERROR_ITEMS:
|
|
1385
|
+
summary["erroredItemsOmitted"] = len(errors) - SUMMARY_ERROR_ITEMS
|
|
1386
|
+
return summary
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def result_summary(stdout):
|
|
1390
|
+
parsed = last_json(stdout) or {}
|
|
1391
|
+
if parsed.get("cloudSummary") is True:
|
|
1392
|
+
return parsed
|
|
1393
|
+
if isinstance(parsed.get("items"), list):
|
|
1394
|
+
return replay_summary(parsed)
|
|
1395
|
+
return None
|
|
1396
|
+
|
|
1397
|
+
|
|
1266
1398
|
def item_errored(item):
|
|
1267
1399
|
return isinstance(item, dict) and any(
|
|
1268
1400
|
item.get(field) is not None for field in ITEM_ERROR_FIELDS
|
|
@@ -1279,8 +1411,24 @@ def raise_interrupt(signum, frame):
|
|
|
1279
1411
|
raise KeyboardInterrupt
|
|
1280
1412
|
|
|
1281
1413
|
|
|
1282
|
-
def
|
|
1414
|
+
def without_item_payload(line):
|
|
1415
|
+
try:
|
|
1416
|
+
event = json.loads(line[len(PROGRESS_PREFIX) :])
|
|
1417
|
+
except ValueError:
|
|
1418
|
+
return line
|
|
1419
|
+
item = event.get("item") if isinstance(event, dict) else None
|
|
1420
|
+
if not isinstance(item, dict):
|
|
1421
|
+
return line
|
|
1422
|
+
event["item"] = {
|
|
1423
|
+
key: value for key, value in item.items() if key not in PROGRESS_PAYLOAD_FIELDS
|
|
1424
|
+
}
|
|
1425
|
+
return PROGRESS_PREFIX + json.dumps(event).encode() + b"\n"
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def forward_stderr(stream, experiment, summarize):
|
|
1283
1429
|
for line in iter(stream.readline, b""):
|
|
1430
|
+
if summarize and line.startswith(PROGRESS_PREFIX):
|
|
1431
|
+
line = without_item_payload(line)
|
|
1284
1432
|
sys.stdout.write(line.decode(errors="replace"))
|
|
1285
1433
|
sys.stdout.flush()
|
|
1286
1434
|
if "id" not in experiment:
|
|
@@ -1289,18 +1437,40 @@ def forward_stderr(stream, experiment):
|
|
|
1289
1437
|
experiment["id"] = match[1].decode()
|
|
1290
1438
|
|
|
1291
1439
|
|
|
1292
|
-
def
|
|
1440
|
+
def trailing_json(text):
|
|
1293
1441
|
decoder = json.JSONDecoder()
|
|
1294
|
-
result = None
|
|
1295
1442
|
for offset in [0, *[i + 1 for i, value in enumerate(text) if value == "\n"]]:
|
|
1296
1443
|
if text.startswith("{", offset):
|
|
1297
1444
|
try:
|
|
1298
1445
|
value, end = decoder.raw_decode(text, offset)
|
|
1299
|
-
if not text[end:].strip() and isinstance(value, dict):
|
|
1300
|
-
result = value
|
|
1301
1446
|
except json.JSONDecodeError:
|
|
1302
|
-
|
|
1303
|
-
|
|
1447
|
+
continue
|
|
1448
|
+
if not text[end:].strip() and isinstance(value, dict):
|
|
1449
|
+
return offset, value
|
|
1450
|
+
return None, None
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
def last_json(text):
|
|
1454
|
+
return trailing_json(text)[1]
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
def relayed_stdout(output, code, summarize):
|
|
1458
|
+
if summarize:
|
|
1459
|
+
output.seek(0)
|
|
1460
|
+
parsed = last_json(output.read().decode(errors="replace"))
|
|
1461
|
+
if isinstance(parsed, dict) and isinstance(parsed.get("items"), list):
|
|
1462
|
+
return json.dumps({**replay_summary(parsed), "exitCode": code}) + "\n", 0
|
|
1463
|
+
return carried_stdout(output)
|
|
1464
|
+
|
|
1465
|
+
|
|
1466
|
+
def carried_stdout(output):
|
|
1467
|
+
start = max(0, os.fstat(output.fileno()).st_size - OUTPUT_LIMIT)
|
|
1468
|
+
output.seek(start)
|
|
1469
|
+
text = output.read().decode(errors="replace")
|
|
1470
|
+
if not start:
|
|
1471
|
+
return text, 0
|
|
1472
|
+
offset = trailing_json(text)[0] or 0
|
|
1473
|
+
return text[offset:], start + len(text[:offset].encode())
|
|
1304
1474
|
|
|
1305
1475
|
|
|
1306
1476
|
def write_result(result):
|
|
@@ -1314,35 +1484,45 @@ def write_result(result):
|
|
|
1314
1484
|
file.write(summary_markdown(result))
|
|
1315
1485
|
|
|
1316
1486
|
|
|
1487
|
+
def annotate(result):
|
|
1488
|
+
if result.get("stoppedEarly"):
|
|
1489
|
+
message = f"Replay stopped early: {result['stoppedEarly']}"
|
|
1490
|
+
elif result["exitCode"]:
|
|
1491
|
+
message = f"Replay exited {result['exitCode']}; bitfab-replay --cloud reports it as its own exit code"
|
|
1492
|
+
else:
|
|
1493
|
+
return
|
|
1494
|
+
print(f"::warning title=Bitfab replay::{message}", flush=True)
|
|
1495
|
+
|
|
1496
|
+
|
|
1317
1497
|
def summary_markdown(result):
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
)
|
|
1322
|
-
items = parsed.get("items") if isinstance(parsed.get("items"), list) else []
|
|
1323
|
-
replayed = [item for item in items if not carried_over(item)]
|
|
1324
|
-
errors = item_errors(replayed)
|
|
1498
|
+
summary = result_summary(result["stdout"]) or {}
|
|
1499
|
+
counts = summary.get("counts", {})
|
|
1500
|
+
test_run = result.get("testRunId") or summary.get("testRunId")
|
|
1501
|
+
errors = summary.get("erroredItems", [])
|
|
1325
1502
|
lines = [f"Commit: `{result['commitSha']}`", f"Exit code: {result['exitCode']}"]
|
|
1326
1503
|
if test_run:
|
|
1327
1504
|
lines.insert(0, f"Test run: `{test_run}`")
|
|
1328
1505
|
if result.get("stoppedEarly"):
|
|
1329
1506
|
lines.append(f"Stopped early: {result['stoppedEarly']}")
|
|
1330
|
-
elif
|
|
1331
|
-
lines.append(
|
|
1507
|
+
elif counts.get("replayed") or counts.get("carriedOver"):
|
|
1508
|
+
lines.append(
|
|
1509
|
+
f"Replayed: {counts['replayed']}, same: {counts['same']}, changed: {counts['changed']}, errored: {counts['errored']}"
|
|
1510
|
+
)
|
|
1332
1511
|
text = "### Bitfab replay\n\n" + "\n\n".join(lines) + "\n"
|
|
1333
1512
|
if errors:
|
|
1334
1513
|
shown = [
|
|
1335
|
-
f"trace {
|
|
1514
|
+
f"trace {error['originalTraceId']}: {error['error']}"
|
|
1515
|
+
for error in errors[:ITEM_ERROR_LINES]
|
|
1336
1516
|
]
|
|
1337
|
-
if
|
|
1338
|
-
shown.append(f"and {
|
|
1517
|
+
if counts.get("errored", 0) > len(shown):
|
|
1518
|
+
shown.append(f"and {counts['errored'] - len(shown)} more errored items")
|
|
1339
1519
|
text += "\n#### Errored items\n\n" + "".join(
|
|
1340
1520
|
f"- `{line.replace('`', chr(39))}`\n" for line in shown
|
|
1341
1521
|
)
|
|
1342
1522
|
return text
|
|
1343
1523
|
|
|
1344
1524
|
|
|
1345
|
-
def run_command(directory, args, timeout, experiment):
|
|
1525
|
+
def run_command(directory, args, timeout, experiment, *, summarize=True):
|
|
1346
1526
|
stopped = None
|
|
1347
1527
|
with tempfile.TemporaryFile() as output:
|
|
1348
1528
|
with subprocess.Popen(
|
|
@@ -1354,18 +1534,26 @@ def run_command(directory, args, timeout, experiment):
|
|
|
1354
1534
|
start_new_session=True,
|
|
1355
1535
|
) as child:
|
|
1356
1536
|
reader = threading.Thread(
|
|
1357
|
-
target=forward_stderr,
|
|
1537
|
+
target=forward_stderr,
|
|
1538
|
+
args=(child.stderr, experiment, summarize),
|
|
1539
|
+
daemon=True,
|
|
1358
1540
|
)
|
|
1359
1541
|
reader.start()
|
|
1360
1542
|
try:
|
|
1361
1543
|
deadline = None if timeout is None else time.monotonic() + timeout * 60
|
|
1544
|
+
parent = os.getppid()
|
|
1545
|
+
checked = time.monotonic()
|
|
1362
1546
|
while child.poll() is None:
|
|
1363
|
-
if os.fstat(output.fileno()).st_size >
|
|
1364
|
-
raise ValueError("Replay
|
|
1547
|
+
if os.fstat(output.fileno()).st_size > DISK_LIMIT:
|
|
1548
|
+
raise ValueError("Replay printed more than 1 GiB to stdout")
|
|
1365
1549
|
if deadline is not None and time.monotonic() >= deadline:
|
|
1366
1550
|
stopped = f"it reached the {timeout}-minute --cloud-timeout"
|
|
1367
1551
|
stop(child)
|
|
1368
1552
|
break
|
|
1553
|
+
if time.monotonic() - checked >= 1:
|
|
1554
|
+
checked = time.monotonic()
|
|
1555
|
+
if os.getppid() != parent:
|
|
1556
|
+
raise KeyboardInterrupt
|
|
1369
1557
|
time.sleep(0.1)
|
|
1370
1558
|
except KeyboardInterrupt:
|
|
1371
1559
|
stopped = "the GitHub run was cancelled"
|
|
@@ -1375,12 +1563,9 @@ def run_command(directory, args, timeout, experiment):
|
|
|
1375
1563
|
raise
|
|
1376
1564
|
finally:
|
|
1377
1565
|
reader.join(timeout=5)
|
|
1378
|
-
code = child.returncode
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
output.seek(0)
|
|
1382
|
-
text = output.read().decode(errors="replace")
|
|
1383
|
-
return (code if code >= 0 else 1), text, stopped
|
|
1566
|
+
code = child.returncode if child.returncode >= 0 else 1
|
|
1567
|
+
text, omitted = relayed_stdout(output, code, summarize)
|
|
1568
|
+
return code, text, omitted, stopped
|
|
1384
1569
|
|
|
1385
1570
|
|
|
1386
1571
|
def stop(child):
|
|
@@ -1772,8 +1957,8 @@ def main():
|
|
|
1772
1957
|
print_json(configure_secrets(argv[1:]))
|
|
1773
1958
|
return 0
|
|
1774
1959
|
if argv == ["--cloud-execute"]:
|
|
1775
|
-
|
|
1776
|
-
return
|
|
1960
|
+
annotate(execute())
|
|
1961
|
+
return 0
|
|
1777
1962
|
if "-h" in argv or "--help" in argv:
|
|
1778
1963
|
print(HELP, end="")
|
|
1779
1964
|
return 0
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
replayContextReady,
|
|
7
7
|
runWithReplayContext,
|
|
8
8
|
unrefTimer
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-ZPVUSLIU.js";
|
|
10
10
|
import {
|
|
11
11
|
runWithCaptureSuppressed,
|
|
12
12
|
warnOnce,
|
|
@@ -1884,4 +1884,4 @@ export {
|
|
|
1884
1884
|
warnJudgeAssertionsDeprecated,
|
|
1885
1885
|
replay
|
|
1886
1886
|
};
|
|
1887
|
-
//# sourceMappingURL=chunk-
|
|
1887
|
+
//# sourceMappingURL=chunk-I6B5R2FP.js.map
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
BitfabError,
|
|
3
3
|
warnOnce,
|
|
4
4
|
writeOnce
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-XASGXGQN.js";
|
|
6
6
|
|
|
7
7
|
// src/codeChange.ts
|
|
8
8
|
var NUL = String.fromCharCode(0);
|
|
@@ -316,7 +316,7 @@ async function seedFromRegistry(registry, pipeline, cases, options = {}) {
|
|
|
316
316
|
sessionId: seedCase.sessionId
|
|
317
317
|
})
|
|
318
318
|
);
|
|
319
|
-
const { flushTraces: flushTraces2 } = await import("./http-
|
|
319
|
+
const { flushTraces: flushTraces2 } = await import("./http-Y6T6T2AP.js");
|
|
320
320
|
await flushTraces2(3e4);
|
|
321
321
|
return { pipeline, traceFunctionKey, traceIds };
|
|
322
322
|
}
|
|
@@ -1007,4 +1007,4 @@ export {
|
|
|
1007
1007
|
runRegistryMain,
|
|
1008
1008
|
runIfEntrypoint
|
|
1009
1009
|
};
|
|
1010
|
-
//# sourceMappingURL=chunk-
|
|
1010
|
+
//# sourceMappingURL=chunk-OEHQPSOB.js.map
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
serializeValue,
|
|
18
18
|
toJsonSafe,
|
|
19
19
|
toJsonSafeReport
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-I6B5R2FP.js";
|
|
21
21
|
import {
|
|
22
22
|
BitfabError,
|
|
23
23
|
CONTENT_OFF_KEY,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
readEnv,
|
|
37
37
|
recordCallerTraceMetadata,
|
|
38
38
|
retireTraceMetadata
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-ZPVUSLIU.js";
|
|
40
40
|
import {
|
|
41
41
|
__privateAdd,
|
|
42
42
|
__privateGet,
|
|
@@ -6253,7 +6253,7 @@ var Bitfab = class {
|
|
|
6253
6253
|
await runWithSeedContext({ traceId }, async () => target(...args));
|
|
6254
6254
|
} finally {
|
|
6255
6255
|
try {
|
|
6256
|
-
const { flushTraces: flushTraces2 } = await import("./http-
|
|
6256
|
+
const { flushTraces: flushTraces2 } = await import("./http-53RWIBXJ.js");
|
|
6257
6257
|
await flushTraces2(3e4);
|
|
6258
6258
|
} finally {
|
|
6259
6259
|
unrecorded = activeTraceStates.delete(traceId);
|
|
@@ -6312,7 +6312,7 @@ var Bitfab = class {
|
|
|
6312
6312
|
`Function is wrapped with trace function key '${wrappedKey}' but replay was called with '${traceFunctionKey}'. Pass matching keys, or pass the unwrapped function to replay it under the explicit key.`
|
|
6313
6313
|
);
|
|
6314
6314
|
}
|
|
6315
|
-
const { replay: doReplay } = await import("./replay-
|
|
6315
|
+
const { replay: doReplay } = await import("./replay-SIKHFPKA.js");
|
|
6316
6316
|
return doReplay(
|
|
6317
6317
|
this.httpClient,
|
|
6318
6318
|
this.serviceUrl,
|
|
@@ -6658,7 +6658,7 @@ async function seedFromRegistry(registry, pipeline, cases, options = {}) {
|
|
|
6658
6658
|
sessionId: seedCase.sessionId
|
|
6659
6659
|
})
|
|
6660
6660
|
);
|
|
6661
|
-
const { flushTraces: flushTraces2 } = await import("./http-
|
|
6661
|
+
const { flushTraces: flushTraces2 } = await import("./http-53RWIBXJ.js");
|
|
6662
6662
|
await flushTraces2(3e4);
|
|
6663
6663
|
return { pipeline, traceFunctionKey, traceIds };
|
|
6664
6664
|
}
|
|
@@ -6703,4 +6703,4 @@ export {
|
|
|
6703
6703
|
reseedFromRegistry,
|
|
6704
6704
|
seedFromRegistry
|
|
6705
6705
|
};
|
|
6706
|
-
//# sourceMappingURL=chunk-
|
|
6706
|
+
//# sourceMappingURL=chunk-UMZDRAAK.js.map
|
|
@@ -181,7 +181,7 @@ function encodeRequestBody(body) {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
// src/version.generated.ts
|
|
184
|
-
var __version__ = "0.64.
|
|
184
|
+
var __version__ = "0.64.4";
|
|
185
185
|
var __packageName__ = "bitfab";
|
|
186
186
|
|
|
187
187
|
// src/errors.ts
|
|
@@ -2500,4 +2500,4 @@ export {
|
|
|
2500
2500
|
HttpClient,
|
|
2501
2501
|
experimentIdWireFields
|
|
2502
2502
|
};
|
|
2503
|
-
//# sourceMappingURL=chunk-
|
|
2503
|
+
//# sourceMappingURL=chunk-XASGXGQN.js.map
|