bitfab 0.65.1 → 0.65.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 +207 -30
- package/dist/{chunk-HW4ZKFYV.js → chunk-3266UYG5.js} +33 -30
- package/dist/chunk-3266UYG5.js.map +1 -0
- package/dist/{chunk-VG54N52W.js → chunk-32BQRD37.js} +2 -2
- package/dist/{chunk-VG54N52W.js.map → chunk-32BQRD37.js.map} +1 -1
- package/dist/{chunk-GS7FUQ6U.js → chunk-6WMY7SWO.js} +2 -2
- package/dist/{chunk-GS7FUQ6U.js.map → chunk-6WMY7SWO.js.map} +1 -1
- package/dist/{chunk-U2GQPMOK.js → chunk-7MT7ODVG.js} +2 -2
- package/dist/{chunk-4SBOBQGH.js → chunk-OJMUXZBC.js} +4 -4
- package/dist/{chunk-3FTI76L6.js → chunk-OPOYSRYU.js} +2 -2
- package/dist/{chunk-VS36LW4L.js → chunk-PTCLRG4E.js} +3 -3
- package/dist/{chunk-P7JUVRYZ.js → chunk-YVJAOGHE.js} +2 -2
- package/dist/{chunk-LXBV37XY.js → chunk-ZG6SHKEF.js} +11 -3
- package/dist/chunk-ZG6SHKEF.js.map +1 -0
- package/dist/{http-FN74JEHE.js → http-2PRP3HWS.js} +2 -2
- package/dist/{http-G32WE67W.js → http-IJB2G6EO.js} +2 -2
- package/dist/index.cjs +28 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.js +3 -3
- package/dist/node.cjs +28 -25
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +3 -3
- package/dist/{replay-FHHOJPX7.js → replay-J5SGUAGH.js} +3 -3
- package/dist/{replay-SVHAY4S6.js → replay-YMNBRGZ7.js} +3 -3
- package/dist/replayCli.js +3 -3
- package/dist/seedCli.js +3 -3
- package/dist/transform/babel.cjs +10 -2
- package/dist/transform/babel.cjs.map +1 -1
- package/dist/transform/babel.js +1 -1
- package/dist/transform/bun.cjs +10 -2
- package/dist/transform/bun.cjs.map +1 -1
- package/dist/transform/bun.js +3 -3
- package/dist/transform/esbuild.cjs +10 -2
- package/dist/transform/esbuild.cjs.map +1 -1
- package/dist/transform/esbuild.js +3 -3
- package/dist/transform/loaderRuntime.cjs +10 -2
- package/dist/transform/loaderRuntime.cjs.map +1 -1
- package/dist/transform/loaderRuntime.js +2 -2
- package/dist/transform/nest.cjs +10 -2
- package/dist/transform/nest.cjs.map +1 -1
- package/dist/transform/nest.js +3 -3
- package/dist/transform/next.cjs +10 -2
- package/dist/transform/next.cjs.map +1 -1
- package/dist/transform/next.js +3 -3
- package/dist/transform/nodeLoader.js +10 -2
- package/dist/transform/nodeLoader.js.map +1 -1
- package/dist/transform/rolldown.cjs +10 -2
- package/dist/transform/rolldown.cjs.map +1 -1
- package/dist/transform/rolldown.js +3 -3
- package/dist/transform/rollup.cjs +10 -2
- package/dist/transform/rollup.cjs.map +1 -1
- package/dist/transform/rollup.js +3 -3
- package/dist/transform/rsbuild.cjs +10 -2
- package/dist/transform/rsbuild.cjs.map +1 -1
- package/dist/transform/rsbuild.js +3 -3
- package/dist/transform/rspack.cjs +10 -2
- package/dist/transform/rspack.cjs.map +1 -1
- package/dist/transform/rspack.js +3 -3
- package/dist/transform/swc.cjs +10 -2
- package/dist/transform/swc.cjs.map +1 -1
- package/dist/transform/swc.js +2 -2
- package/dist/transform/unplugin.cjs +10 -2
- package/dist/transform/unplugin.cjs.map +1 -1
- package/dist/transform/unplugin.js +3 -3
- package/dist/transform/vite.cjs +10 -2
- package/dist/transform/vite.cjs.map +1 -1
- package/dist/transform/vite.js +3 -3
- package/dist/transform/webpack.cjs +10 -2
- package/dist/transform/webpack.cjs.map +1 -1
- package/dist/transform/webpack.js +3 -3
- package/package.json +11 -11
- package/dist/chunk-HW4ZKFYV.js.map +0 -1
- package/dist/chunk-LXBV37XY.js.map +0 -1
- /package/dist/{chunk-U2GQPMOK.js.map → chunk-7MT7ODVG.js.map} +0 -0
- /package/dist/{chunk-4SBOBQGH.js.map → chunk-OJMUXZBC.js.map} +0 -0
- /package/dist/{chunk-3FTI76L6.js.map → chunk-OPOYSRYU.js.map} +0 -0
- /package/dist/{chunk-VS36LW4L.js.map → chunk-PTCLRG4E.js.map} +0 -0
- /package/dist/{chunk-P7JUVRYZ.js.map → chunk-YVJAOGHE.js.map} +0 -0
- /package/dist/{http-FN74JEHE.js.map → http-2PRP3HWS.js.map} +0 -0
- /package/dist/{http-G32WE67W.js.map → http-IJB2G6EO.js.map} +0 -0
- /package/dist/{replay-FHHOJPX7.js.map → replay-J5SGUAGH.js.map} +0 -0
- /package/dist/{replay-SVHAY4S6.js.map → replay-YMNBRGZ7.js.map} +0 -0
package/cloudReplay.py
CHANGED
|
@@ -116,6 +116,8 @@ PUSH_TRIGGER = re.compile(
|
|
|
116
116
|
r"^[ \t]*\"?(?:on\"?[ \t]*:.*\bpush\b|push\"?[ \t]*:)", re.MULTILINE
|
|
117
117
|
)
|
|
118
118
|
DISPATCH_INPUTS = ("execution_id", "request", KEY_INPUT)
|
|
119
|
+
PUSH_SUBJECT = "Bitfab replay started on push"
|
|
120
|
+
PUSH_CONDITION = f"github.event_name != 'push' || !startsWith(github.ref_name, '{PREFIX}') || startsWith(github.event.head_commit.message, '{PUSH_SUBJECT}')"
|
|
119
121
|
BRANCH_NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/-]*")
|
|
120
122
|
ENV_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
121
123
|
RESERVED_ENV_PREFIXES = ("GITHUB_", "RUNNER_", "ACTIONS_", "BITFAB_REPLAY_")
|
|
@@ -254,7 +256,8 @@ Set up once:
|
|
|
254
256
|
[--runs-on LABEL] [--secret-prefix PREFIX] [--check COMMAND]
|
|
255
257
|
Writes .github/workflows/bitfab-replay.yml, the only file cloud replay keeps
|
|
256
258
|
in the repository, and lists what is left to do. On an existing workflow it
|
|
257
|
-
adds only the lines a newer SDK needs
|
|
259
|
+
adds only the lines a newer SDK needs, such as the push trigger that lets a
|
|
260
|
+
replay run without GitHub access, and lists what it changed.
|
|
258
261
|
--cloud-update [same options] [--remove-old-key-secrets]
|
|
259
262
|
The same command as --cloud-init, for bringing an existing setup up to date.
|
|
260
263
|
It also lists BITFAB_REPLAY_KEY_* secrets an older SDK left in the
|
|
@@ -266,6 +269,16 @@ Set up once:
|
|
|
266
269
|
GitHub access comes from the GitHub CLI when it is logged in, otherwise from GH_TOKEN
|
|
267
270
|
or GITHUB_TOKEN, otherwise from the github.com credential git already stores. Without
|
|
268
271
|
the GitHub CLI, --cloud-secrets lists the secrets to create by hand and where.
|
|
272
|
+
|
|
273
|
+
Without GitHub access, or with access that cannot read or push to the repository, a
|
|
274
|
+
replay still runs when git can push the snapshot branch and the workflow also starts
|
|
275
|
+
on a push to a bitfab-replay/ branch, as one written or updated by --cloud-init does.
|
|
276
|
+
This command then warns, prints where to watch the run, and returns once the branch
|
|
277
|
+
is pushed, with no updates and exit code 0. The run uses the workflow's BITFAB_API_KEY
|
|
278
|
+
secret, and --cloud-full-output and --cloud-allow-file are refused, since only GitHub
|
|
279
|
+
access can confirm the repository is private. Delete the snapshot branch once the run
|
|
280
|
+
finishes. With access, a replay starts through the GitHub API as before, and its
|
|
281
|
+
snapshot commit asks GitHub not to start any workflow on the push.
|
|
269
282
|
"""
|
|
270
283
|
|
|
271
284
|
|
|
@@ -281,6 +294,10 @@ class DispatchRejected(ValueError):
|
|
|
281
294
|
self.record = record
|
|
282
295
|
|
|
283
296
|
|
|
297
|
+
class GithubUnreachable(ValueError):
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
|
|
284
301
|
class KeepRedirect(urllib.request.HTTPRedirectHandler):
|
|
285
302
|
def redirect_request(self, *args, **kwargs):
|
|
286
303
|
return None
|
|
@@ -939,7 +956,15 @@ def sensitive(path):
|
|
|
939
956
|
)
|
|
940
957
|
|
|
941
958
|
|
|
942
|
-
def
|
|
959
|
+
def snapshot_subject(execution_id, on_push):
|
|
960
|
+
if on_push:
|
|
961
|
+
return f"{PUSH_SUBJECT} {execution_id}"
|
|
962
|
+
return f"Bitfab replay {execution_id} [skip actions]"
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def snapshot(
|
|
966
|
+
root, workflow, files, request, *, preview=False, allowed=(), on_push=False
|
|
967
|
+
):
|
|
943
968
|
if git(root, "ls-files", "-u"):
|
|
944
969
|
raise ValueError("Resolve merge conflicts before snapshotting")
|
|
945
970
|
head = git(root, "rev-parse", "HEAD")
|
|
@@ -1014,7 +1039,7 @@ def snapshot(root, workflow, files, request, *, preview=False, allowed=()):
|
|
|
1014
1039
|
tree,
|
|
1015
1040
|
"-p",
|
|
1016
1041
|
head,
|
|
1017
|
-
input=f"
|
|
1042
|
+
input=f"{snapshot_subject(request['id'], on_push)}\n\n{json.dumps(request)}\n",
|
|
1018
1043
|
)
|
|
1019
1044
|
return {**summary, "sha": sha}
|
|
1020
1045
|
|
|
@@ -1050,9 +1075,8 @@ def execution_lock(directory, execution_id):
|
|
|
1050
1075
|
def find_run(record):
|
|
1051
1076
|
if record.get("snapshot") is False:
|
|
1052
1077
|
return find_branch_run(record)
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
)
|
|
1078
|
+
event = "push" if record.get("startedOnPush") else "workflow_dispatch"
|
|
1079
|
+
query = urlencode({"event": event, "branch": record["branch"], "per_page": 100})
|
|
1056
1080
|
runs = api(
|
|
1057
1081
|
record["repository"], f"actions/workflows/{record['workflow']}/runs?{query}"
|
|
1058
1082
|
)
|
|
@@ -1274,15 +1298,19 @@ def http_status(error):
|
|
|
1274
1298
|
|
|
1275
1299
|
|
|
1276
1300
|
def preflight(repo, workflow, *, pushes=True, full_output=False, allows_files=False):
|
|
1277
|
-
|
|
1301
|
+
try:
|
|
1302
|
+
access = github_access()
|
|
1303
|
+
except ValueError as error:
|
|
1304
|
+
raise GithubUnreachable(str(error)) from error
|
|
1278
1305
|
try:
|
|
1279
1306
|
details = api(repo, "")
|
|
1280
1307
|
except RuntimeError as error:
|
|
1281
|
-
|
|
1308
|
+
refused = getattr(error, "http_status", None) in (401, 403, 404)
|
|
1309
|
+
raise (GithubUnreachable if refused else ValueError)(
|
|
1282
1310
|
f"GitHub access from {access['source']} cannot read {repo}{http_status(error)}; use an account or token with write access to it"
|
|
1283
1311
|
) from error
|
|
1284
1312
|
if pushes and (details or {}).get("permissions", {}).get("push") is False:
|
|
1285
|
-
raise
|
|
1313
|
+
raise GithubUnreachable(
|
|
1286
1314
|
f"GitHub access from {access['source']} cannot push to {repo}; use an account or token with write access to it"
|
|
1287
1315
|
)
|
|
1288
1316
|
if full_output and (details or {}).get("private") is False:
|
|
@@ -1344,6 +1372,12 @@ def submit(root, repo, workflow, parsed, path):
|
|
|
1344
1372
|
record = json.loads(path.read_text())
|
|
1345
1373
|
if record["request"] != request or record["repository"].lower() != repo.lower():
|
|
1346
1374
|
raise ValueError("Execution ID already belongs to a different request")
|
|
1375
|
+
if record.get("startedOnPush") and record["state"] != "prepared":
|
|
1376
|
+
try:
|
|
1377
|
+
github_access()
|
|
1378
|
+
except ValueError as error:
|
|
1379
|
+
record["unfollowed"] = str(error)
|
|
1380
|
+
return record
|
|
1347
1381
|
status(record, fetch_result=False)
|
|
1348
1382
|
if record["state"] == "prepared":
|
|
1349
1383
|
raise ValueError(
|
|
@@ -1351,17 +1385,24 @@ def submit(root, repo, workflow, parsed, path):
|
|
|
1351
1385
|
)
|
|
1352
1386
|
refuse_rejected(record)
|
|
1353
1387
|
return record
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1388
|
+
unfollowed = None
|
|
1389
|
+
try:
|
|
1390
|
+
preflight(
|
|
1391
|
+
repo,
|
|
1392
|
+
workflow,
|
|
1393
|
+
full_output=request.get("fullOutput") is True,
|
|
1394
|
+
allows_files=bool(allowed),
|
|
1395
|
+
)
|
|
1396
|
+
except GithubUnreachable as error:
|
|
1397
|
+
unfollowed = str(error)
|
|
1398
|
+
refuse_unfollowed(root, repo, workflow, request, allowed, unfollowed)
|
|
1360
1399
|
if dry_run(request["args"]):
|
|
1361
1400
|
progress(
|
|
1362
1401
|
"--dry-run dispatches a GitHub run that checks secrets and resolves the traces without replaying; it receives the workflow's secrets"
|
|
1363
1402
|
)
|
|
1364
|
-
source = snapshot(
|
|
1403
|
+
source = snapshot(
|
|
1404
|
+
root, workflow, files, request, allowed=allowed, on_push=bool(unfollowed)
|
|
1405
|
+
)
|
|
1365
1406
|
describe_snapshot(source)
|
|
1366
1407
|
record = {
|
|
1367
1408
|
"id": execution_id,
|
|
@@ -1370,6 +1411,7 @@ def submit(root, repo, workflow, parsed, path):
|
|
|
1370
1411
|
"branch": PREFIX + execution_id,
|
|
1371
1412
|
"request": request,
|
|
1372
1413
|
"state": "prepared",
|
|
1414
|
+
**({"startedOnPush": True} if unfollowed else {}),
|
|
1373
1415
|
**source,
|
|
1374
1416
|
}
|
|
1375
1417
|
save(path, record)
|
|
@@ -1389,6 +1431,9 @@ def submit(root, repo, workflow, parsed, path):
|
|
|
1389
1431
|
) from error
|
|
1390
1432
|
record["state"] = "dispatch_unknown"
|
|
1391
1433
|
save(path, record)
|
|
1434
|
+
if unfollowed:
|
|
1435
|
+
record["unfollowed"] = unfollowed
|
|
1436
|
+
return record
|
|
1392
1437
|
text = within(root, f"{WORKFLOW_DIRECTORY}/{workflow}").read_text()
|
|
1393
1438
|
declared = declared_inputs(text)
|
|
1394
1439
|
key_inputs = issue_key_token(root, record, text)
|
|
@@ -1400,6 +1445,62 @@ def submit(root, repo, workflow, parsed, path):
|
|
|
1400
1445
|
)
|
|
1401
1446
|
|
|
1402
1447
|
|
|
1448
|
+
def push_trigger(lines):
|
|
1449
|
+
for index, line in enumerate(lines):
|
|
1450
|
+
if re.fullmatch(r'"?on"?[ \t]*:[ \t]*(#.*)?', line):
|
|
1451
|
+
end = block_end(lines, index, 0)
|
|
1452
|
+
step = child_indent(lines, index, end, " ")
|
|
1453
|
+
row = next(
|
|
1454
|
+
(
|
|
1455
|
+
row
|
|
1456
|
+
for row in range(index + 1, end)
|
|
1457
|
+
if re.fullmatch(rf'{step}"?push"?[ \t]*:.*', lines[row])
|
|
1458
|
+
),
|
|
1459
|
+
None,
|
|
1460
|
+
)
|
|
1461
|
+
if row is None:
|
|
1462
|
+
return None
|
|
1463
|
+
return lines[row : block_end(lines, row, len(step))]
|
|
1464
|
+
if re.match(r'"?on"?[ \t]*:', line):
|
|
1465
|
+
return [line] if re.search(r"\bpush\b", line) else None
|
|
1466
|
+
return None
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def starts_on_push(text):
|
|
1470
|
+
trigger = push_trigger(text.splitlines()) or []
|
|
1471
|
+
return any(PREFIX in line for line in trigger) and not any(
|
|
1472
|
+
"branches-ignore" in line for line in trigger
|
|
1473
|
+
)
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
def refuse_unfollowed(root, repo, workflow, request, allowed, reason):
|
|
1477
|
+
path = f"{WORKFLOW_DIRECTORY}/{workflow}"
|
|
1478
|
+
if not starts_on_push(within(root, path).read_text()):
|
|
1479
|
+
raise ValueError(
|
|
1480
|
+
f"{reason}. Without it a replay can still run, with no updates here, once {path} also starts on a push to a {PREFIX} branch: run bitfab-replay --cloud-update and commit the workflow it updates"
|
|
1481
|
+
)
|
|
1482
|
+
if request.get("fullOutput") is True:
|
|
1483
|
+
raise ValueError(
|
|
1484
|
+
f"--cloud-full-output writes every trace's input and output into the Actions job log, so it needs GitHub access that confirms {repo} is private: {reason}"
|
|
1485
|
+
)
|
|
1486
|
+
if allowed:
|
|
1487
|
+
raise ValueError(
|
|
1488
|
+
f"{ALLOW_FLAG} uploads a credential-like file to a snapshot branch, so it needs GitHub access that confirms {repo} is private: {reason}"
|
|
1489
|
+
)
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
def report_unfollowed(record, reason):
|
|
1493
|
+
progress(
|
|
1494
|
+
f"Warning: the replay started on GitHub, but this command gets no updates on it: {reason}"
|
|
1495
|
+
)
|
|
1496
|
+
progress(
|
|
1497
|
+
f"It runs with the workflow's BITFAB_API_KEY secret, since replaying as you needs GitHub access. Watch it at https://github.com/{record['repository']}/actions/workflows/{record['workflow']}; its experiment appears in Bitfab when it finishes"
|
|
1498
|
+
)
|
|
1499
|
+
progress(
|
|
1500
|
+
f"Once it finishes, delete its snapshot branch with git push origin --delete {record['branch']}, or with --cloud-cleanup {record['id']} from a machine with GitHub access"
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
|
|
1403
1504
|
def local_client(root, request):
|
|
1404
1505
|
try:
|
|
1405
1506
|
output = command(
|
|
@@ -1736,6 +1837,11 @@ def run_cli(argv):
|
|
|
1736
1837
|
raise
|
|
1737
1838
|
if record.get("preview"):
|
|
1738
1839
|
return record, None
|
|
1840
|
+
reason = record.pop("unfollowed", None)
|
|
1841
|
+
if reason:
|
|
1842
|
+
report_unfollowed(record, reason)
|
|
1843
|
+
save(path, record)
|
|
1844
|
+
return record, None
|
|
1739
1845
|
else:
|
|
1740
1846
|
if not path.exists():
|
|
1741
1847
|
raise ValueError(
|
|
@@ -2791,7 +2897,7 @@ def push_triggers(root, workflow):
|
|
|
2791
2897
|
|
|
2792
2898
|
|
|
2793
2899
|
def workflow_document(steps, run, directory, env, *, runs_on, environment):
|
|
2794
|
-
job = {"runs-on": runs_on}
|
|
2900
|
+
job = {"if": PUSH_CONDITION, "runs-on": runs_on}
|
|
2795
2901
|
if environment:
|
|
2796
2902
|
job["environment"] = environment
|
|
2797
2903
|
job["steps"] = [
|
|
@@ -2816,7 +2922,8 @@ def workflow_document(steps, run, directory, env, *, runs_on, environment):
|
|
|
2816
2922
|
"request": {"type": "string", "required": False},
|
|
2817
2923
|
KEY_INPUT: {"type": "string", "required": False},
|
|
2818
2924
|
}
|
|
2819
|
-
}
|
|
2925
|
+
},
|
|
2926
|
+
"push": {"branches": [PREFIX + "**"]},
|
|
2820
2927
|
},
|
|
2821
2928
|
"permissions": {"contents": "read", "id-token": "write"},
|
|
2822
2929
|
"jobs": {"replay": job},
|
|
@@ -2830,10 +2937,18 @@ def indentation(line):
|
|
|
2830
2937
|
def block_end(lines, start, indent):
|
|
2831
2938
|
end = start + 1
|
|
2832
2939
|
while end < len(lines) and (
|
|
2833
|
-
not lines[end].strip()
|
|
2940
|
+
not lines[end].strip()
|
|
2941
|
+
or lines[end].lstrip().startswith("#")
|
|
2942
|
+
or indentation(lines[end]) > indent
|
|
2834
2943
|
):
|
|
2835
2944
|
end += 1
|
|
2836
|
-
while end > start + 1 and
|
|
2945
|
+
while end > start + 1 and (
|
|
2946
|
+
not lines[end - 1].strip()
|
|
2947
|
+
or (
|
|
2948
|
+
lines[end - 1].lstrip().startswith("#")
|
|
2949
|
+
and indentation(lines[end - 1]) <= indent
|
|
2950
|
+
)
|
|
2951
|
+
):
|
|
2837
2952
|
end -= 1
|
|
2838
2953
|
return end
|
|
2839
2954
|
|
|
@@ -2938,19 +3053,67 @@ def add_id_token_permission(lines):
|
|
|
2938
3053
|
if not re.fullmatch(r'"?permissions"?[ \t]*:[ \t]*(#.*)?', line):
|
|
2939
3054
|
continue
|
|
2940
3055
|
end = block_end(lines, index, 0)
|
|
2941
|
-
|
|
2942
|
-
(
|
|
2943
|
-
lines[row][: indentation(lines[row])]
|
|
2944
|
-
for row in range(index + 1, end)
|
|
2945
|
-
if lines[row].strip()
|
|
2946
|
-
),
|
|
2947
|
-
" ",
|
|
2948
|
-
)
|
|
2949
|
-
lines.insert(end, f"{child}id-token: write")
|
|
3056
|
+
lines.insert(end, f"{child_indent(lines, index, end, ' ')}id-token: write")
|
|
2950
3057
|
return True
|
|
2951
3058
|
return False
|
|
2952
3059
|
|
|
2953
3060
|
|
|
3061
|
+
def holds_yaml(line):
|
|
3062
|
+
return bool(line.strip()) and not line.lstrip().startswith("#")
|
|
3063
|
+
|
|
3064
|
+
|
|
3065
|
+
def child_indent(lines, index, end, fallback):
|
|
3066
|
+
return next(
|
|
3067
|
+
(
|
|
3068
|
+
lines[row][: indentation(lines[row])]
|
|
3069
|
+
for row in range(index + 1, end)
|
|
3070
|
+
if holds_yaml(lines[row])
|
|
3071
|
+
),
|
|
3072
|
+
fallback,
|
|
3073
|
+
)
|
|
3074
|
+
|
|
3075
|
+
|
|
3076
|
+
def add_push_trigger(lines):
|
|
3077
|
+
for index, line in enumerate(lines):
|
|
3078
|
+
if not re.fullmatch(r'"?on"?[ \t]*:[ \t]*(#.*)?', line):
|
|
3079
|
+
continue
|
|
3080
|
+
end = block_end(lines, index, 0)
|
|
3081
|
+
step = child_indent(lines, index, end, " ")
|
|
3082
|
+
lines[end:end] = [
|
|
3083
|
+
f"{step}push:",
|
|
3084
|
+
f"{step * 2}branches:",
|
|
3085
|
+
f"{step * 3}- {PREFIX}**",
|
|
3086
|
+
]
|
|
3087
|
+
return True
|
|
3088
|
+
return False
|
|
3089
|
+
|
|
3090
|
+
|
|
3091
|
+
def add_push_condition(lines):
|
|
3092
|
+
jobs = next(
|
|
3093
|
+
(
|
|
3094
|
+
index
|
|
3095
|
+
for index, line in enumerate(lines)
|
|
3096
|
+
if re.fullmatch(r'"?jobs"?[ \t]*:[ \t]*(#.*)?', line)
|
|
3097
|
+
),
|
|
3098
|
+
None,
|
|
3099
|
+
)
|
|
3100
|
+
if jobs is None:
|
|
3101
|
+
return False
|
|
3102
|
+
end = block_end(lines, jobs, 0)
|
|
3103
|
+
job = next((row for row in range(jobs + 1, end) if holds_yaml(lines[row])), None)
|
|
3104
|
+
if job is None or not lines[job].rstrip().endswith(":"):
|
|
3105
|
+
return False
|
|
3106
|
+
job_end = block_end(lines, job, indentation(lines[job]))
|
|
3107
|
+
step = child_indent(lines, job, job_end, " " * (indentation(lines[job]) + 2))
|
|
3108
|
+
if any(
|
|
3109
|
+
re.fullmatch(rf'{step}"?if"?[ \t]*:.*', lines[row])
|
|
3110
|
+
for row in range(job + 1, job_end)
|
|
3111
|
+
):
|
|
3112
|
+
return False
|
|
3113
|
+
lines.insert(job + 1, f"{step}if: {json.dumps(PUSH_CONDITION)}")
|
|
3114
|
+
return True
|
|
3115
|
+
|
|
3116
|
+
|
|
2954
3117
|
def secret_scope(environment):
|
|
2955
3118
|
return f"environments/{environment}/" if environment else "actions/"
|
|
2956
3119
|
|
|
@@ -3041,6 +3204,20 @@ def update_workflow(root, path, *, remove_old_key_secrets=False):
|
|
|
3041
3204
|
by_hand.append(
|
|
3042
3205
|
f"{name} (type: string, required: false) under on.workflow_dispatch.inputs"
|
|
3043
3206
|
)
|
|
3207
|
+
if not starts_on_push(text):
|
|
3208
|
+
if push_trigger(lines) is None and add_push_trigger(lines):
|
|
3209
|
+
updated.append(
|
|
3210
|
+
f"Started the workflow on a push to a {PREFIX} branch too, so a replay runs without GitHub access, with no updates on it"
|
|
3211
|
+
)
|
|
3212
|
+
else:
|
|
3213
|
+
by_hand.append(f"{PREFIX}** in on.push.branches")
|
|
3214
|
+
if push_trigger(lines) is not None and PUSH_SUBJECT not in text:
|
|
3215
|
+
if add_push_condition(lines):
|
|
3216
|
+
updated.append(
|
|
3217
|
+
"Skipped the push run of a replay started through the GitHub API, so each replay runs once"
|
|
3218
|
+
)
|
|
3219
|
+
else:
|
|
3220
|
+
by_hand.append(f"if: {json.dumps(PUSH_CONDITION)} on the replay job")
|
|
3044
3221
|
if not re.search(r'^"?run-name"?[ \t]*:', text, re.MULTILINE):
|
|
3045
3222
|
add_run_name(lines)
|
|
3046
3223
|
updated.append("Named each run after its execution")
|
|
@@ -3095,7 +3272,7 @@ def update_workflow(root, path, *, remove_old_key_secrets=False):
|
|
|
3095
3272
|
def initialize(argv, name="--cloud-init"):
|
|
3096
3273
|
parser = argparse.ArgumentParser(
|
|
3097
3274
|
prog=f"bitfab-replay {name}",
|
|
3098
|
-
description="Write .github/workflows/bitfab-replay.yml, the only file cloud replay keeps in the repository, and list what is left to do. It detects the runtime, package manager, install command, and replay command from the directory you run it in. On an existing workflow it only adds what a newer SDK needs (the execution_id and key_id inputs, the run name, and the id-token: write permission), removes the BITFAB_REPLAY_BITFAB_API_KEY line older SDKs wrote, lists what it changed, and lists the BITFAB_REPLAY_KEY_* secrets older SDKs left in the repository; edit it directly for anything else.",
|
|
3275
|
+
description="Write .github/workflows/bitfab-replay.yml, the only file cloud replay keeps in the repository, and list what is left to do. It detects the runtime, package manager, install command, and replay command from the directory you run it in. On an existing workflow it only adds what a newer SDK needs (the execution_id and key_id inputs, the push trigger and the condition that skips a duplicate push run, the run name, and the id-token: write permission), removes the BITFAB_REPLAY_BITFAB_API_KEY line older SDKs wrote, lists what it changed, and lists the BITFAB_REPLAY_KEY_* secrets older SDKs left in the repository; edit it directly for anything else.",
|
|
3099
3276
|
)
|
|
3100
3277
|
parser.add_argument(
|
|
3101
3278
|
"--secret",
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
serializeValue,
|
|
19
19
|
toJsonSafe,
|
|
20
20
|
toJsonSafeReport
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-OPOYSRYU.js";
|
|
22
22
|
import {
|
|
23
23
|
BitfabError,
|
|
24
24
|
CONTENT_OFF_KEY,
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
readEnv,
|
|
39
39
|
recordCallerTraceMetadata,
|
|
40
40
|
retireTraceMetadata
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-6WMY7SWO.js";
|
|
42
42
|
import {
|
|
43
43
|
__privateAdd,
|
|
44
44
|
__privateGet,
|
|
@@ -1312,6 +1312,12 @@ function finishedRerun(datasetId, run, joinedExisting) {
|
|
|
1312
1312
|
}
|
|
1313
1313
|
return { run, joinedExisting };
|
|
1314
1314
|
}
|
|
1315
|
+
function normalizeDataset(dataset) {
|
|
1316
|
+
return { ...dataset, golden: dataset.golden ?? false };
|
|
1317
|
+
}
|
|
1318
|
+
function normalizeDatasetResult(response) {
|
|
1319
|
+
return { ...response, dataset: normalizeDataset(response.dataset) };
|
|
1320
|
+
}
|
|
1315
1321
|
function sleep(ms) {
|
|
1316
1322
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1317
1323
|
}
|
|
@@ -1328,11 +1334,13 @@ var DatasetsClient = class {
|
|
|
1328
1334
|
* leaves an existing one untouched.
|
|
1329
1335
|
*/
|
|
1330
1336
|
async save(params) {
|
|
1331
|
-
|
|
1337
|
+
const response = await this.httpClient.request("/api/sdk/datasets", {
|
|
1332
1338
|
traceFunctionKey: params.traceFunctionKey,
|
|
1333
1339
|
name: params.name,
|
|
1334
|
-
...params.description === void 0 ? {} : { description: params.description }
|
|
1340
|
+
...params.description === void 0 ? {} : { description: params.description },
|
|
1341
|
+
...typeof params.golden === "boolean" ? { golden: params.golden } : {}
|
|
1335
1342
|
});
|
|
1343
|
+
return normalizeDatasetResult(response);
|
|
1336
1344
|
}
|
|
1337
1345
|
/**
|
|
1338
1346
|
* Rename a dataset or change its description in place. The dataset keeps its
|
|
@@ -1342,18 +1350,21 @@ var DatasetsClient = class {
|
|
|
1342
1350
|
* organization.
|
|
1343
1351
|
*/
|
|
1344
1352
|
async update(datasetId, params) {
|
|
1345
|
-
if (params.name === void 0 && params.description === void 0) {
|
|
1346
|
-
throw new BitfabError(
|
|
1353
|
+
if (params.name === void 0 && params.description === void 0 && params.golden === void 0) {
|
|
1354
|
+
throw new BitfabError(
|
|
1355
|
+
"Provide name, description, and/or golden to update."
|
|
1356
|
+
);
|
|
1347
1357
|
}
|
|
1348
1358
|
const response = await this.httpClient.request(
|
|
1349
1359
|
datasetPath(datasetId),
|
|
1350
1360
|
{
|
|
1351
1361
|
...params.name === void 0 ? {} : { name: params.name },
|
|
1352
|
-
...params.description === void 0 ? {} : { description: params.description }
|
|
1362
|
+
...params.description === void 0 ? {} : { description: params.description },
|
|
1363
|
+
...typeof params.golden === "boolean" ? { golden: params.golden } : {}
|
|
1353
1364
|
},
|
|
1354
1365
|
{ method: "PATCH" }
|
|
1355
1366
|
);
|
|
1356
|
-
return response.dataset;
|
|
1367
|
+
return normalizeDataset(response.dataset);
|
|
1357
1368
|
}
|
|
1358
1369
|
/**
|
|
1359
1370
|
* List datasets, scoped to one trace function when `traceFunctionKey` is
|
|
@@ -1371,7 +1382,7 @@ var DatasetsClient = class {
|
|
|
1371
1382
|
query.set("cursor", cursor);
|
|
1372
1383
|
}
|
|
1373
1384
|
const response = await this.httpClient.get(`/api/sdk/datasets?${query}`);
|
|
1374
|
-
datasets.push(...response.datasets);
|
|
1385
|
+
datasets.push(...response.datasets.map(normalizeDataset));
|
|
1375
1386
|
cursor = response.nextCursor ?? void 0;
|
|
1376
1387
|
} while (cursor !== void 0);
|
|
1377
1388
|
return datasets;
|
|
@@ -1381,7 +1392,7 @@ var DatasetsClient = class {
|
|
|
1381
1392
|
const response = await this.httpClient.get(
|
|
1382
1393
|
datasetPath(datasetId)
|
|
1383
1394
|
);
|
|
1384
|
-
return response.dataset;
|
|
1395
|
+
return normalizeDataset(response.dataset);
|
|
1385
1396
|
}
|
|
1386
1397
|
/** Fetches all trace-ID pages, the same membership a replay with `datasetId` selects. */
|
|
1387
1398
|
async listTraces(datasetId) {
|
|
@@ -1402,17 +1413,13 @@ var DatasetsClient = class {
|
|
|
1402
1413
|
* `skippedTraceIds` rather than failing the call.
|
|
1403
1414
|
*/
|
|
1404
1415
|
async addTraces(datasetId, traceIds) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
{ traceIds }
|
|
1408
|
-
);
|
|
1416
|
+
const response = await this.httpClient.request(datasetPath(datasetId, "/traces"), { traceIds });
|
|
1417
|
+
return normalizeDatasetResult(response);
|
|
1409
1418
|
}
|
|
1410
1419
|
/** Remove traces from the dataset. The traces themselves are never deleted. */
|
|
1411
1420
|
async removeTraces(datasetId, traceIds) {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
{ traceIds }
|
|
1415
|
-
);
|
|
1421
|
+
const response = await this.httpClient.request(datasetPath(datasetId, "/removeTraces"), { traceIds });
|
|
1422
|
+
return normalizeDatasetResult(response);
|
|
1416
1423
|
}
|
|
1417
1424
|
/**
|
|
1418
1425
|
* Assign graders to the dataset (1 to 100 ids per call). Graders outside the
|
|
@@ -1420,17 +1427,13 @@ var DatasetsClient = class {
|
|
|
1420
1427
|
* `skippedGraderIds` rather than failing the call.
|
|
1421
1428
|
*/
|
|
1422
1429
|
async addGraders(datasetId, graderIds) {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
{ graderIds }
|
|
1426
|
-
);
|
|
1430
|
+
const response = await this.httpClient.request(datasetPath(datasetId, "/graders"), { graderIds });
|
|
1431
|
+
return normalizeDatasetResult(response);
|
|
1427
1432
|
}
|
|
1428
1433
|
/** Unassign graders from the dataset. */
|
|
1429
1434
|
async removeGraders(datasetId, graderIds) {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
{ graderIds }
|
|
1433
|
-
);
|
|
1435
|
+
const response = await this.httpClient.request(datasetPath(datasetId, "/removeGraders"), { graderIds });
|
|
1436
|
+
return normalizeDatasetResult(response);
|
|
1434
1437
|
}
|
|
1435
1438
|
async rerunGraders(datasetId, options = {}) {
|
|
1436
1439
|
const started = await this.httpClient.request(
|
|
@@ -6419,7 +6422,7 @@ var Bitfab = class {
|
|
|
6419
6422
|
await runWithSeedContext({ traceId }, async () => target(...args));
|
|
6420
6423
|
} finally {
|
|
6421
6424
|
try {
|
|
6422
|
-
const { flushTraces: flushTraces2 } = await import("./http-
|
|
6425
|
+
const { flushTraces: flushTraces2 } = await import("./http-2PRP3HWS.js");
|
|
6423
6426
|
await flushTraces2(3e4);
|
|
6424
6427
|
} finally {
|
|
6425
6428
|
unrecorded = activeTraceStates.delete(traceId);
|
|
@@ -6478,7 +6481,7 @@ var Bitfab = class {
|
|
|
6478
6481
|
`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.`
|
|
6479
6482
|
);
|
|
6480
6483
|
}
|
|
6481
|
-
const { replay: doReplay } = await import("./replay-
|
|
6484
|
+
const { replay: doReplay } = await import("./replay-YMNBRGZ7.js");
|
|
6482
6485
|
return doReplay(
|
|
6483
6486
|
this.httpClient,
|
|
6484
6487
|
this.serviceUrl,
|
|
@@ -6824,7 +6827,7 @@ async function seedFromRegistry(registry, pipeline, cases, options = {}) {
|
|
|
6824
6827
|
sessionId: seedCase.sessionId
|
|
6825
6828
|
})
|
|
6826
6829
|
);
|
|
6827
|
-
const { flushTraces: flushTraces2 } = await import("./http-
|
|
6830
|
+
const { flushTraces: flushTraces2 } = await import("./http-2PRP3HWS.js");
|
|
6828
6831
|
await flushTraces2(3e4);
|
|
6829
6832
|
return { pipeline, traceFunctionKey, traceIds };
|
|
6830
6833
|
}
|
|
@@ -6871,4 +6874,4 @@ export {
|
|
|
6871
6874
|
reseedFromRegistry,
|
|
6872
6875
|
seedFromRegistry
|
|
6873
6876
|
};
|
|
6874
|
-
//# sourceMappingURL=chunk-
|
|
6877
|
+
//# sourceMappingURL=chunk-3266UYG5.js.map
|