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 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 or len(value) > 1000:
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 step.get("status") != "queued" and step.get("name") not in shown:
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 last_json(text):
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
- pass
1303
- return result
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 > OUTPUT_LIMIT:
1364
- raise ValueError("Replay output exceeded 16 MiB")
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
- if output.tell() > OUTPUT_LIMIT:
1380
- raise ValueError("Replay output exceeded 16 MiB")
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
- result = execute()
1776
- return result["exitCode"] or (1 if result.get("stoppedEarly") else 0)
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-XHUW3NY6.js";
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-GLWAET3G.js.map
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.2";
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-XHUW3NY6.js.map
2949
+ //# sourceMappingURL=chunk-JIPKI6RB.js.map