bitfab 0.64.2 → 0.64.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/cloudReplay.py +75 -22
- package/dist/{chunk-GLWAET3G.js → chunk-2YDH4SPS.js} +2 -2
- package/dist/{chunk-XHUW3NY6.js → chunk-JIPKI6RB.js} +2 -2
- package/dist/{chunk-XHUW3NY6.js.map → chunk-JIPKI6RB.js.map} +1 -1
- package/dist/{chunk-O632VPWV.js → chunk-RC5FXV76.js} +2 -2
- package/dist/{chunk-O632VPWV.js.map → chunk-RC5FXV76.js.map} +1 -1
- package/dist/{chunk-ENORQO5K.js → chunk-UZU2PNO7.js} +6 -6
- package/dist/{chunk-FX4X7LOW.js → chunk-XNJ5AGAF.js} +3 -3
- package/dist/{http-IOT2GDD2.js → http-D5YRJ57X.js} +2 -2
- package/dist/{http-MFXRJ6RS.js → http-KBTWSYL7.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-PFHS24KV.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-2YDH4SPS.js.map} +0 -0
- /package/dist/{chunk-ENORQO5K.js.map → chunk-UZU2PNO7.js.map} +0 -0
- /package/dist/{chunk-FX4X7LOW.js.map → chunk-XNJ5AGAF.js.map} +0 -0
- /package/dist/{http-IOT2GDD2.js.map → http-D5YRJ57X.js.map} +0 -0
- /package/dist/{http-MFXRJ6RS.js.map → http-KBTWSYL7.js.map} +0 -0
- /package/dist/{replay-QZQYRTAO.js.map → replay-PFHS24KV.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 = (
|
|
@@ -310,6 +311,16 @@ def parse_environment_file(text):
|
|
|
310
311
|
return values
|
|
311
312
|
|
|
312
313
|
|
|
314
|
+
def secret_value(value):
|
|
315
|
+
if "\n" not in value:
|
|
316
|
+
return value
|
|
317
|
+
try:
|
|
318
|
+
parsed = json.loads(value)
|
|
319
|
+
except ValueError:
|
|
320
|
+
return value
|
|
321
|
+
return json.dumps(parsed, separators=(",", ":"))
|
|
322
|
+
|
|
323
|
+
|
|
313
324
|
def ensure_environment(repo, environment):
|
|
314
325
|
try:
|
|
315
326
|
api(repo, f"environments/{environment}")
|
|
@@ -412,7 +423,7 @@ def configure_secrets(argv):
|
|
|
412
423
|
repo,
|
|
413
424
|
*(["--env", environment] if environment else []),
|
|
414
425
|
],
|
|
415
|
-
input=values.get(name, values.get(targets[name])),
|
|
426
|
+
input=secret_value(values.get(name, values.get(targets[name]))),
|
|
416
427
|
env={**os.environ, "GH_TOKEN": token},
|
|
417
428
|
)
|
|
418
429
|
result["set"] = [targets[name] for name in found]
|
|
@@ -592,10 +603,8 @@ def validate_arguments(args):
|
|
|
592
603
|
if not isinstance(args, list) or not all(isinstance(value, str) for value in args):
|
|
593
604
|
raise ValueError("Replay arguments must be a list of strings")
|
|
594
605
|
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
|
-
)
|
|
606
|
+
if "\x00" in value or "\n" in value:
|
|
607
|
+
raise ValueError("Replay options must be single-line values")
|
|
599
608
|
if value.startswith("--cloud"):
|
|
600
609
|
raise ValueError("The runner replays locally and never dispatches again")
|
|
601
610
|
if sum(len(value) for value in args) > 16000:
|
|
@@ -889,6 +898,10 @@ def print_outcome(found):
|
|
|
889
898
|
for line in replay_output(lines) or []:
|
|
890
899
|
print(line, file=sys.stderr)
|
|
891
900
|
sys.stderr.flush()
|
|
901
|
+
if result.get("stdoutOmittedBytes"):
|
|
902
|
+
progress(
|
|
903
|
+
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"
|
|
904
|
+
)
|
|
892
905
|
print(result.get("stdout", ""), end="", flush=True)
|
|
893
906
|
if record.get("stoppedEarly"):
|
|
894
907
|
progress(
|
|
@@ -1049,7 +1062,10 @@ def submit(root, repo, workflow, parsed, path):
|
|
|
1049
1062
|
def report_steps(record, shown):
|
|
1050
1063
|
job = replay_job(record)
|
|
1051
1064
|
for step in (job or {}).get("steps", []):
|
|
1052
|
-
if
|
|
1065
|
+
if (
|
|
1066
|
+
step.get("status") in ("in_progress", "completed")
|
|
1067
|
+
and step.get("name") not in shown
|
|
1068
|
+
):
|
|
1053
1069
|
shown.add(step.get("name"))
|
|
1054
1070
|
progress(step.get("name"))
|
|
1055
1071
|
|
|
@@ -1186,7 +1202,7 @@ def execute():
|
|
|
1186
1202
|
previous = signal.signal(signal.SIGTERM, raise_interrupt)
|
|
1187
1203
|
print(OUTPUT_BEGIN, flush=True)
|
|
1188
1204
|
try:
|
|
1189
|
-
code, stdout, stopped = run_command(
|
|
1205
|
+
code, stdout, omitted, stopped = run_command(
|
|
1190
1206
|
directory, args, request.get("timeoutMinutes"), experiment
|
|
1191
1207
|
)
|
|
1192
1208
|
finally:
|
|
@@ -1198,6 +1214,8 @@ def execute():
|
|
|
1198
1214
|
"exitCode": code,
|
|
1199
1215
|
"stdout": stdout,
|
|
1200
1216
|
}
|
|
1217
|
+
if omitted:
|
|
1218
|
+
result["stdoutOmittedBytes"] = omitted
|
|
1201
1219
|
if stopped:
|
|
1202
1220
|
result["stoppedEarly"] = stopped
|
|
1203
1221
|
if UUID.fullmatch(experiment.get("id", "")):
|
|
@@ -1224,6 +1242,15 @@ def check_secrets(root):
|
|
|
1224
1242
|
+ ", ".join(f"{name} (secret {targets[name]})" for name in empty)
|
|
1225
1243
|
+ ". 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
1244
|
)
|
|
1245
|
+
for name in targets:
|
|
1246
|
+
if any(
|
|
1247
|
+
0 < len(line.strip()) < 4
|
|
1248
|
+
for line in os.environ.get(name, "").splitlines()[1:]
|
|
1249
|
+
):
|
|
1250
|
+
print(
|
|
1251
|
+
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",
|
|
1252
|
+
flush=True,
|
|
1253
|
+
)
|
|
1227
1254
|
|
|
1228
1255
|
|
|
1229
1256
|
def run_check_command(directory):
|
|
@@ -1289,18 +1316,31 @@ def forward_stderr(stream, experiment):
|
|
|
1289
1316
|
experiment["id"] = match[1].decode()
|
|
1290
1317
|
|
|
1291
1318
|
|
|
1292
|
-
def
|
|
1319
|
+
def trailing_json(text):
|
|
1293
1320
|
decoder = json.JSONDecoder()
|
|
1294
|
-
result = None
|
|
1295
1321
|
for offset in [0, *[i + 1 for i, value in enumerate(text) if value == "\n"]]:
|
|
1296
1322
|
if text.startswith("{", offset):
|
|
1297
1323
|
try:
|
|
1298
1324
|
value, end = decoder.raw_decode(text, offset)
|
|
1299
|
-
if not text[end:].strip() and isinstance(value, dict):
|
|
1300
|
-
result = value
|
|
1301
1325
|
except json.JSONDecodeError:
|
|
1302
|
-
|
|
1303
|
-
|
|
1326
|
+
continue
|
|
1327
|
+
if not text[end:].strip() and isinstance(value, dict):
|
|
1328
|
+
return offset, value
|
|
1329
|
+
return None, None
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def last_json(text):
|
|
1333
|
+
return trailing_json(text)[1]
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
def carried_stdout(output):
|
|
1337
|
+
start = max(0, os.fstat(output.fileno()).st_size - OUTPUT_LIMIT)
|
|
1338
|
+
output.seek(start)
|
|
1339
|
+
text = output.read().decode(errors="replace")
|
|
1340
|
+
if not start:
|
|
1341
|
+
return text, 0
|
|
1342
|
+
offset = trailing_json(text)[0] or 0
|
|
1343
|
+
return text[offset:], start + len(text[:offset].encode())
|
|
1304
1344
|
|
|
1305
1345
|
|
|
1306
1346
|
def write_result(result):
|
|
@@ -1314,6 +1354,16 @@ def write_result(result):
|
|
|
1314
1354
|
file.write(summary_markdown(result))
|
|
1315
1355
|
|
|
1316
1356
|
|
|
1357
|
+
def annotate(result):
|
|
1358
|
+
if result.get("stoppedEarly"):
|
|
1359
|
+
message = f"Replay stopped early: {result['stoppedEarly']}"
|
|
1360
|
+
elif result["exitCode"]:
|
|
1361
|
+
message = f"Replay exited {result['exitCode']}; bitfab-replay --cloud reports it as its own exit code"
|
|
1362
|
+
else:
|
|
1363
|
+
return
|
|
1364
|
+
print(f"::warning title=Bitfab replay::{message}", flush=True)
|
|
1365
|
+
|
|
1366
|
+
|
|
1317
1367
|
def summary_markdown(result):
|
|
1318
1368
|
parsed = last_json(result["stdout"]) or {}
|
|
1319
1369
|
test_run = result.get("testRunId") or parsed.get(
|
|
@@ -1359,13 +1409,19 @@ def run_command(directory, args, timeout, experiment):
|
|
|
1359
1409
|
reader.start()
|
|
1360
1410
|
try:
|
|
1361
1411
|
deadline = None if timeout is None else time.monotonic() + timeout * 60
|
|
1412
|
+
parent = os.getppid()
|
|
1413
|
+
checked = time.monotonic()
|
|
1362
1414
|
while child.poll() is None:
|
|
1363
|
-
if os.fstat(output.fileno()).st_size >
|
|
1364
|
-
raise ValueError("Replay
|
|
1415
|
+
if os.fstat(output.fileno()).st_size > DISK_LIMIT:
|
|
1416
|
+
raise ValueError("Replay printed more than 1 GiB to stdout")
|
|
1365
1417
|
if deadline is not None and time.monotonic() >= deadline:
|
|
1366
1418
|
stopped = f"it reached the {timeout}-minute --cloud-timeout"
|
|
1367
1419
|
stop(child)
|
|
1368
1420
|
break
|
|
1421
|
+
if time.monotonic() - checked >= 1:
|
|
1422
|
+
checked = time.monotonic()
|
|
1423
|
+
if os.getppid() != parent:
|
|
1424
|
+
raise KeyboardInterrupt
|
|
1369
1425
|
time.sleep(0.1)
|
|
1370
1426
|
except KeyboardInterrupt:
|
|
1371
1427
|
stopped = "the GitHub run was cancelled"
|
|
@@ -1376,11 +1432,8 @@ def run_command(directory, args, timeout, experiment):
|
|
|
1376
1432
|
finally:
|
|
1377
1433
|
reader.join(timeout=5)
|
|
1378
1434
|
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
|
|
1435
|
+
text, omitted = carried_stdout(output)
|
|
1436
|
+
return (code if code >= 0 else 1), text, omitted, stopped
|
|
1384
1437
|
|
|
1385
1438
|
|
|
1386
1439
|
def stop(child):
|
|
@@ -1772,8 +1825,8 @@ def main():
|
|
|
1772
1825
|
print_json(configure_secrets(argv[1:]))
|
|
1773
1826
|
return 0
|
|
1774
1827
|
if argv == ["--cloud-execute"]:
|
|
1775
|
-
|
|
1776
|
-
return
|
|
1828
|
+
annotate(execute())
|
|
1829
|
+
return 0
|
|
1777
1830
|
if "-h" in argv or "--help" in argv:
|
|
1778
1831
|
print(HELP, end="")
|
|
1779
1832
|
return 0
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
replayContextReady,
|
|
7
7
|
runWithReplayContext,
|
|
8
8
|
unrefTimer
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-JIPKI6RB.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-2YDH4SPS.js.map
|
|
@@ -189,7 +189,7 @@ function encodeRequestBody(body) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
// src/version.generated.ts
|
|
192
|
-
var __version__ = "0.64.
|
|
192
|
+
var __version__ = "0.64.3";
|
|
193
193
|
var __packageName__ = "bitfab";
|
|
194
194
|
|
|
195
195
|
// src/constants.ts
|
|
@@ -2946,4 +2946,4 @@ export {
|
|
|
2946
2946
|
HttpClient,
|
|
2947
2947
|
experimentIdWireFields
|
|
2948
2948
|
};
|
|
2949
|
-
//# sourceMappingURL=chunk-
|
|
2949
|
+
//# sourceMappingURL=chunk-JIPKI6RB.js.map
|