@bitfab/sdk 0.64.15 → 0.64.17
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 +282 -24
- package/dist/{chunk-NYLVYTBO.js → chunk-G5BYSZLD.js} +30 -6
- package/dist/chunk-G5BYSZLD.js.map +1 -0
- package/dist/{chunk-Z4DIFZTO.js → chunk-I3LSDFMN.js} +2 -2
- package/dist/{chunk-Z4DIFZTO.js.map → chunk-I3LSDFMN.js.map} +1 -1
- package/dist/chunk-PQ6ABBRQ.js +1905 -0
- package/dist/chunk-PQ6ABBRQ.js.map +1 -0
- package/dist/{chunk-7ZJ7KXEV.js → chunk-QZFVHNQE.js} +2 -2
- package/dist/{chunk-2MHRNZH5.js → chunk-WLGH45CM.js} +204 -89
- package/dist/chunk-WLGH45CM.js.map +1 -0
- package/dist/{chunk-KVGRTCIE.js → chunk-YHKFC7XZ.js} +16 -2
- package/dist/{chunk-KVGRTCIE.js.map → chunk-YHKFC7XZ.js.map} +1 -1
- package/dist/{http-36VJ3FFM.js → http-AYZZNM2L.js} +2 -2
- package/dist/{http-IQ2AC36B.js → http-DAQLSTUN.js} +2 -2
- package/dist/index.cjs +25 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +99 -2
- package/dist/index.d.ts +99 -2
- package/dist/index.js +3 -3
- package/dist/node.cjs +25 -1
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +3 -3
- package/dist/replay-ARKSPOVK.js +28 -0
- package/dist/{replay-2QFPGFS7.js → replay-FFNEMPGO.js} +3 -3
- package/dist/replay-FFNEMPGO.js.map +1 -0
- package/dist/replayCli.js +3 -2
- package/dist/replayCli.js.map +1 -1
- package/dist/seedCli.js +3 -2
- package/dist/seedCli.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-2MHRNZH5.js.map +0 -1
- package/dist/chunk-NYLVYTBO.js.map +0 -1
- /package/dist/{chunk-7ZJ7KXEV.js.map → chunk-QZFVHNQE.js.map} +0 -0
- /package/dist/{http-36VJ3FFM.js.map → http-AYZZNM2L.js.map} +0 -0
- /package/dist/{http-IQ2AC36B.js.map → http-DAQLSTUN.js.map} +0 -0
- /package/dist/{replay-2QFPGFS7.js.map → replay-ARKSPOVK.js.map} +0 -0
package/cloudReplay.py
CHANGED
|
@@ -36,6 +36,16 @@ SDK_LANGUAGE_ENV = "BITFAB_SDK_LANGUAGE"
|
|
|
36
36
|
CHECK_COMMAND_ENV = "BITFAB_REPLAY_CHECK"
|
|
37
37
|
EXECUTION_TARGET_ENV = "BITFAB_REPLAY_EXECUTION_TARGET"
|
|
38
38
|
HOSTED_EXECUTION_TARGET = "hosted"
|
|
39
|
+
API_KEY_ENV = "BITFAB_REPLAY_BITFAB_API_KEY"
|
|
40
|
+
PRINT_API_KEY_FLAG = "--print-api-key"
|
|
41
|
+
API_KEY_LINE = "@@bitfab:api-key "
|
|
42
|
+
KEY_INPUT = "key_id"
|
|
43
|
+
KEY_SECRET_PREFIX = "BITFAB_REPLAY_KEY_"
|
|
44
|
+
KEY_SECRET_REFERENCE = (
|
|
45
|
+
"${{ secrets[format('" + KEY_SECRET_PREFIX + "{0}', inputs." + KEY_INPUT + ")] }}"
|
|
46
|
+
)
|
|
47
|
+
API_KEY_PROBE_SECONDS = 120
|
|
48
|
+
RUN_NAME = "Bitfab replay ${{ inputs.execution_id || github.ref_name }}"
|
|
39
49
|
REQUEST_VERSION = 4
|
|
40
50
|
PLAIN_REQUEST_VERSION = 3
|
|
41
51
|
OLDEST_DISPATCHED_VERSION = 2
|
|
@@ -95,7 +105,7 @@ ENVIRONMENT_SETTING = re.compile(
|
|
|
95
105
|
PUSH_TRIGGER = re.compile(
|
|
96
106
|
r"^[ \t]*\"?(?:on\"?[ \t]*:.*\bpush\b|push\"?[ \t]*:)", re.MULTILINE
|
|
97
107
|
)
|
|
98
|
-
DISPATCH_INPUTS = ("execution_id", "request")
|
|
108
|
+
DISPATCH_INPUTS = ("execution_id", "request", KEY_INPUT)
|
|
99
109
|
BRANCH_NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/-]*")
|
|
100
110
|
ENV_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
101
111
|
RESERVED_ENV_PREFIXES = ("GITHUB_", "RUNNER_", "ACTIONS_", "BITFAB_REPLAY_")
|
|
@@ -204,6 +214,14 @@ values --cloud-env sets. version and id are optional: version names the request
|
|
|
204
214
|
format (4 when env is set, otherwise 3; 2 is still accepted), and id is the
|
|
205
215
|
execution UUID, which must match an execution_id input when the workflow has one.
|
|
206
216
|
|
|
217
|
+
The replay runs with the Bitfab API key it would use on your machine, so the
|
|
218
|
+
experiment is recorded as yours. Before dispatch, the replay's registry is loaded
|
|
219
|
+
locally to read its client's key, which the GitHub CLI stores as a GitHub secret
|
|
220
|
+
named for this execution and deletes once the replay starts. Without the GitHub
|
|
221
|
+
CLI, without permission to manage the repository's Actions secrets, with no key
|
|
222
|
+
resolved locally, or with a workflow that declares no key_id input, the run uses
|
|
223
|
+
the workflow's BITFAB_API_KEY secret instead.
|
|
224
|
+
|
|
207
225
|
The runner's job exits with the replay's exit code, so the Actions run fails when
|
|
208
226
|
the replay does. Traces that errored fail it only under --fail-on-error.
|
|
209
227
|
|
|
@@ -216,7 +234,10 @@ Set up once:
|
|
|
216
234
|
--cloud-init [--secret NAME ...] [--environment NAME] [--run COMMAND]
|
|
217
235
|
[--runs-on LABEL] [--secret-prefix PREFIX] [--check COMMAND]
|
|
218
236
|
Writes .github/workflows/bitfab-replay.yml, the only file cloud replay keeps
|
|
219
|
-
in the repository, and lists what is left to do.
|
|
237
|
+
in the repository, and lists what is left to do. On an existing workflow it
|
|
238
|
+
adds only the lines a newer SDK needs and lists what it changed.
|
|
239
|
+
--cloud-update [same options]
|
|
240
|
+
The same command as --cloud-init, for bringing an existing setup up to date.
|
|
220
241
|
--cloud-secrets --env-file FILE [NAME ...] [--environment NAME] [--dry-run]
|
|
221
242
|
Copies local values into the GitHub secrets the workflow reads.
|
|
222
243
|
|
|
@@ -1189,6 +1210,7 @@ def print_outcome(found):
|
|
|
1189
1210
|
|
|
1190
1211
|
|
|
1191
1212
|
def cleanup(root, record):
|
|
1213
|
+
remove_key(record)
|
|
1192
1214
|
if record.get("cleaned"):
|
|
1193
1215
|
return
|
|
1194
1216
|
if record.get("snapshot") is False:
|
|
@@ -1335,10 +1357,110 @@ def submit(root, repo, workflow, parsed, path):
|
|
|
1335
1357
|
) from error
|
|
1336
1358
|
record["state"] = "dispatch_unknown"
|
|
1337
1359
|
save(path, record)
|
|
1338
|
-
|
|
1339
|
-
|
|
1360
|
+
text = within(root, f"{WORKFLOW_DIRECTORY}/{workflow}").read_text()
|
|
1361
|
+
declared = declared_inputs(text)
|
|
1362
|
+
key_inputs = store_key(root, record, declared, text)
|
|
1363
|
+
save(path, record)
|
|
1364
|
+
return dispatch(
|
|
1365
|
+
record,
|
|
1366
|
+
{"execution_id": execution_id, "request": "-", **key_inputs},
|
|
1367
|
+
declared,
|
|
1340
1368
|
)
|
|
1341
|
-
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
def local_api_key(root, request):
|
|
1372
|
+
try:
|
|
1373
|
+
output = command(
|
|
1374
|
+
[*replay_command(), *request["args"], PRINT_API_KEY_FLAG],
|
|
1375
|
+
cwd=within(root, request["cwd"]),
|
|
1376
|
+
timeout=API_KEY_PROBE_SECONDS,
|
|
1377
|
+
input="",
|
|
1378
|
+
)
|
|
1379
|
+
except (ValueError, RuntimeError, OSError, subprocess.SubprocessError):
|
|
1380
|
+
return None
|
|
1381
|
+
for line in reversed(output.splitlines()):
|
|
1382
|
+
if not line.startswith(API_KEY_LINE):
|
|
1383
|
+
continue
|
|
1384
|
+
try:
|
|
1385
|
+
key = json.loads(line.removeprefix(API_KEY_LINE)).get("apiKey")
|
|
1386
|
+
except (ValueError, AttributeError):
|
|
1387
|
+
return None
|
|
1388
|
+
return key if isinstance(key, str) and key.strip() else None
|
|
1389
|
+
return None
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
def store_key(root, record, declared, workflow_text):
|
|
1393
|
+
shared = "so the run uses the workflow's BITFAB_API_KEY secret"
|
|
1394
|
+
if KEY_INPUT not in declared:
|
|
1395
|
+
progress(
|
|
1396
|
+
f"The workflow takes no {KEY_INPUT} input, {shared}. To replay with your own Bitfab API key, run bitfab-replay --cloud-init, which adds the {KEY_INPUT} input and {API_KEY_ENV} to the workflow"
|
|
1397
|
+
)
|
|
1398
|
+
return {}
|
|
1399
|
+
if shutil.which("gh") is None:
|
|
1400
|
+
progress(
|
|
1401
|
+
f"Replaying with your own Bitfab API key needs the GitHub CLI (https://cli.github.com) to store it as a GitHub secret, {shared}"
|
|
1402
|
+
)
|
|
1403
|
+
return {}
|
|
1404
|
+
key = local_api_key(root, record["request"])
|
|
1405
|
+
if key is None:
|
|
1406
|
+
progress(
|
|
1407
|
+
f"The replay's Bitfab client resolved no API key on this machine, {shared}"
|
|
1408
|
+
)
|
|
1409
|
+
return {}
|
|
1410
|
+
key_id = record["id"].replace("-", "").upper()
|
|
1411
|
+
name = KEY_SECRET_PREFIX + key_id
|
|
1412
|
+
configured = ENVIRONMENT_SETTING.search(workflow_text)
|
|
1413
|
+
environment = configured[1] if configured else None
|
|
1414
|
+
try:
|
|
1415
|
+
command(
|
|
1416
|
+
[
|
|
1417
|
+
"gh",
|
|
1418
|
+
"secret",
|
|
1419
|
+
"set",
|
|
1420
|
+
name,
|
|
1421
|
+
"--repo",
|
|
1422
|
+
record["repository"],
|
|
1423
|
+
*(["--env", environment] if environment else []),
|
|
1424
|
+
],
|
|
1425
|
+
input=key,
|
|
1426
|
+
env={**os.environ, "GH_TOKEN": github_access()["token"]},
|
|
1427
|
+
)
|
|
1428
|
+
except (ValueError, RuntimeError, OSError, subprocess.SubprocessError):
|
|
1429
|
+
progress(
|
|
1430
|
+
f"Could not store your Bitfab API key as the GitHub secret {name}, which needs permission to manage the repository's Actions secrets, {shared}"
|
|
1431
|
+
)
|
|
1432
|
+
return {}
|
|
1433
|
+
record["keySecret"] = {"name": name, "environment": environment}
|
|
1434
|
+
progress(
|
|
1435
|
+
f"Replaying with the Bitfab API key from this machine, stored as the GitHub secret {name} until the replay starts"
|
|
1436
|
+
)
|
|
1437
|
+
return {KEY_INPUT: key_id}
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def remove_key(record):
|
|
1441
|
+
secret = record.get("keySecret")
|
|
1442
|
+
if not secret or secret.get("removed"):
|
|
1443
|
+
return
|
|
1444
|
+
try:
|
|
1445
|
+
command(
|
|
1446
|
+
[
|
|
1447
|
+
"gh",
|
|
1448
|
+
"secret",
|
|
1449
|
+
"delete",
|
|
1450
|
+
secret["name"],
|
|
1451
|
+
"--repo",
|
|
1452
|
+
record["repository"],
|
|
1453
|
+
*(["--env", secret["environment"]] if secret["environment"] else []),
|
|
1454
|
+
],
|
|
1455
|
+
env={**os.environ, "GH_TOKEN": github_access()["token"]},
|
|
1456
|
+
)
|
|
1457
|
+
except (ValueError, RuntimeError, OSError, subprocess.SubprocessError) as error:
|
|
1458
|
+
if getattr(error, "http_status", None) != 404:
|
|
1459
|
+
progress(
|
|
1460
|
+
f"Could not delete the GitHub secret {secret['name']}; delete it at {secrets_page(record['repository'], secret['environment'])}"
|
|
1461
|
+
)
|
|
1462
|
+
return
|
|
1463
|
+
secret["removed"] = True
|
|
1342
1464
|
|
|
1343
1465
|
|
|
1344
1466
|
def declared_inputs(text):
|
|
@@ -1446,7 +1568,8 @@ def submit_as_is(root, repo, workflow, parsed, path):
|
|
|
1446
1568
|
preflight(
|
|
1447
1569
|
repo, workflow, pushes=False, full_output=request.get("fullOutput") is True
|
|
1448
1570
|
)
|
|
1449
|
-
|
|
1571
|
+
text = branch_workflow(repo, workflow, sha)
|
|
1572
|
+
declared = declared_inputs(text)
|
|
1450
1573
|
if "request" not in declared:
|
|
1451
1574
|
raise ValueError(
|
|
1452
1575
|
f"{WORKFLOW_DIRECTORY}/{workflow} on {branch} declares no request input, so it cannot take a replay; add request (type: string, required: false) under on.workflow_dispatch.inputs and land it on {branch}"
|
|
@@ -1466,9 +1589,11 @@ def submit_as_is(root, repo, workflow, parsed, path):
|
|
|
1466
1589
|
progress(
|
|
1467
1590
|
f"Replaying {branch} at {sha[:12]} as it is on GitHub, without local changes. Execution {execution_id}; resume with --cloud-watch {execution_id}"
|
|
1468
1591
|
)
|
|
1592
|
+
key_inputs = store_key(root, record, declared, text)
|
|
1593
|
+
save(path, record)
|
|
1469
1594
|
return dispatch(
|
|
1470
1595
|
record,
|
|
1471
|
-
{"execution_id": execution_id, "request": json.dumps(request)},
|
|
1596
|
+
{"execution_id": execution_id, "request": json.dumps(request), **key_inputs},
|
|
1472
1597
|
declared,
|
|
1473
1598
|
run_details=True,
|
|
1474
1599
|
)
|
|
@@ -1477,10 +1602,11 @@ def submit_as_is(root, repo, workflow, parsed, path):
|
|
|
1477
1602
|
def report_steps(record, shown):
|
|
1478
1603
|
job = replay_job(record)
|
|
1479
1604
|
for step in (job or {}).get("steps", []):
|
|
1480
|
-
if (
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1605
|
+
if step.get("status") not in ("in_progress", "completed"):
|
|
1606
|
+
continue
|
|
1607
|
+
if "--cloud-execute" in (step.get("name") or ""):
|
|
1608
|
+
remove_key(record)
|
|
1609
|
+
if step.get("name") not in shown:
|
|
1484
1610
|
shown.add(step.get("name"))
|
|
1485
1611
|
progress(step.get("name"))
|
|
1486
1612
|
|
|
@@ -1701,6 +1827,11 @@ def execute():
|
|
|
1701
1827
|
request = runner_request(root, commit)
|
|
1702
1828
|
directory = within(root, request["cwd"])
|
|
1703
1829
|
check_secrets(root)
|
|
1830
|
+
if os.environ.get(API_KEY_ENV, "").strip():
|
|
1831
|
+
print(
|
|
1832
|
+
"Replaying with the Bitfab API key of the person who started this run",
|
|
1833
|
+
flush=True,
|
|
1834
|
+
)
|
|
1704
1835
|
environment = request.get("env") or {}
|
|
1705
1836
|
if environment:
|
|
1706
1837
|
workflow_environment_conflicts(root, environment)
|
|
@@ -2376,12 +2507,13 @@ def workflow_document(steps, run, directory, env, *, runs_on, environment):
|
|
|
2376
2507
|
]
|
|
2377
2508
|
return {
|
|
2378
2509
|
"name": "Bitfab replay",
|
|
2379
|
-
"run-name":
|
|
2510
|
+
"run-name": RUN_NAME,
|
|
2380
2511
|
"on": {
|
|
2381
2512
|
"workflow_dispatch": {
|
|
2382
2513
|
"inputs": {
|
|
2383
2514
|
"execution_id": {"type": "string", "required": False},
|
|
2384
2515
|
"request": {"type": "string", "required": False},
|
|
2516
|
+
KEY_INPUT: {"type": "string", "required": False},
|
|
2385
2517
|
}
|
|
2386
2518
|
}
|
|
2387
2519
|
},
|
|
@@ -2390,10 +2522,141 @@ def workflow_document(steps, run, directory, env, *, runs_on, environment):
|
|
|
2390
2522
|
}
|
|
2391
2523
|
|
|
2392
2524
|
|
|
2393
|
-
def
|
|
2525
|
+
def indentation(line):
|
|
2526
|
+
return len(line) - len(line.lstrip(" \t"))
|
|
2527
|
+
|
|
2528
|
+
|
|
2529
|
+
def block_end(lines, start, indent):
|
|
2530
|
+
end = start + 1
|
|
2531
|
+
while end < len(lines) and (
|
|
2532
|
+
not lines[end].strip() or indentation(lines[end]) > indent
|
|
2533
|
+
):
|
|
2534
|
+
end += 1
|
|
2535
|
+
while end > start + 1 and not lines[end - 1].strip():
|
|
2536
|
+
end -= 1
|
|
2537
|
+
return end
|
|
2538
|
+
|
|
2539
|
+
|
|
2540
|
+
def add_input(lines, name):
|
|
2541
|
+
for index, line in enumerate(lines):
|
|
2542
|
+
if not re.fullmatch(r'[ \t]*"?request"?[ \t]*:[ \t]*', line):
|
|
2543
|
+
continue
|
|
2544
|
+
indent = indentation(line)
|
|
2545
|
+
child = " " * (
|
|
2546
|
+
indentation(lines[index + 1])
|
|
2547
|
+
if block_end(lines, index, indent) > index + 1
|
|
2548
|
+
else indent + 2
|
|
2549
|
+
)
|
|
2550
|
+
end = block_end(lines, index, indent - 1)
|
|
2551
|
+
lines[end:end] = [
|
|
2552
|
+
f"{line[:indent]}{name}:",
|
|
2553
|
+
f"{child}type: string",
|
|
2554
|
+
f"{child}required: false",
|
|
2555
|
+
]
|
|
2556
|
+
return True
|
|
2557
|
+
return False
|
|
2558
|
+
|
|
2559
|
+
|
|
2560
|
+
def add_run_name(lines):
|
|
2561
|
+
named = next(
|
|
2562
|
+
(
|
|
2563
|
+
index
|
|
2564
|
+
for index, line in enumerate(lines)
|
|
2565
|
+
if re.match(r'"?name"?[ \t]*:', line)
|
|
2566
|
+
),
|
|
2567
|
+
-1,
|
|
2568
|
+
)
|
|
2569
|
+
lines.insert(named + 1, f"run-name: {json.dumps(RUN_NAME)}")
|
|
2570
|
+
|
|
2571
|
+
|
|
2572
|
+
def replay_step_key(lines):
|
|
2573
|
+
for index, line in enumerate(lines):
|
|
2574
|
+
if "--cloud-execute" not in line:
|
|
2575
|
+
continue
|
|
2576
|
+
for above in range(index, -1, -1):
|
|
2577
|
+
match = re.match(r"([ \t]*)(- +)?run[ \t]*:", lines[above])
|
|
2578
|
+
if match and indentation(lines[above]) <= indentation(line):
|
|
2579
|
+
return above, len(match[1]) + len(match[2] or "")
|
|
2580
|
+
return None
|
|
2581
|
+
|
|
2582
|
+
|
|
2583
|
+
def add_key_env(lines):
|
|
2584
|
+
found = replay_step_key(lines)
|
|
2585
|
+
if found is None:
|
|
2586
|
+
return False
|
|
2587
|
+
run, key = found
|
|
2588
|
+
start = run
|
|
2589
|
+
while start > 0 and not re.match(r"[ \t]*- ", lines[start]):
|
|
2590
|
+
start -= 1
|
|
2591
|
+
end = block_end(lines, run, key - 1)
|
|
2592
|
+
entry = f'{API_KEY_ENV}: "{KEY_SECRET_REFERENCE}"'
|
|
2593
|
+
for index in range(start, end):
|
|
2594
|
+
line = lines[index]
|
|
2595
|
+
if not re.fullmatch(r"[ \t]*(- +)?env[ \t]*:[ \t]*", line):
|
|
2596
|
+
continue
|
|
2597
|
+
if len(line) - len(line.lstrip(" \t-")) != key:
|
|
2598
|
+
continue
|
|
2599
|
+
entries = block_end(lines, index, key)
|
|
2600
|
+
child = indentation(lines[index + 1]) if entries > index + 1 else key + 2
|
|
2601
|
+
lines.insert(entries, " " * child + entry)
|
|
2602
|
+
return True
|
|
2603
|
+
lines[end:end] = [" " * key + "env:", " " * (key + 2) + entry]
|
|
2604
|
+
return True
|
|
2605
|
+
|
|
2606
|
+
|
|
2607
|
+
def update_workflow(root, path):
|
|
2608
|
+
target = within(root, path)
|
|
2609
|
+
text = target.read_text()
|
|
2610
|
+
lines = text.split("\n")
|
|
2611
|
+
updated, by_hand = [], []
|
|
2612
|
+
declared = declared_inputs(text)
|
|
2613
|
+
for name in ("execution_id", KEY_INPUT):
|
|
2614
|
+
if name in declared:
|
|
2615
|
+
continue
|
|
2616
|
+
if add_input(lines, name):
|
|
2617
|
+
updated.append(f"Added the {name} input")
|
|
2618
|
+
else:
|
|
2619
|
+
by_hand.append(
|
|
2620
|
+
f"{name} (type: string, required: false) under on.workflow_dispatch.inputs"
|
|
2621
|
+
)
|
|
2622
|
+
if not re.search(r'^"?run-name"?[ \t]*:', text, re.MULTILINE):
|
|
2623
|
+
add_run_name(lines)
|
|
2624
|
+
updated.append("Named each run after its execution")
|
|
2625
|
+
if API_KEY_ENV not in text:
|
|
2626
|
+
if add_key_env(lines):
|
|
2627
|
+
updated.append(f"Added {API_KEY_ENV} to the replay step's env")
|
|
2628
|
+
else:
|
|
2629
|
+
by_hand.append(
|
|
2630
|
+
f'{API_KEY_ENV}: "{KEY_SECRET_REFERENCE}" in the replay step\'s env'
|
|
2631
|
+
)
|
|
2632
|
+
if updated:
|
|
2633
|
+
target.write_text("\n".join(lines))
|
|
2634
|
+
steps = []
|
|
2635
|
+
if updated:
|
|
2636
|
+
steps.append(
|
|
2637
|
+
f"Commit {path}. A --cloud replay runs the workflow in its own snapshot, so the next one uses it; --cloud-ref replays need it on the branch they replay"
|
|
2638
|
+
)
|
|
2639
|
+
if by_hand:
|
|
2640
|
+
steps.append(
|
|
2641
|
+
"Cloud replay could not place these lines, so add them by hand: "
|
|
2642
|
+
+ "; ".join(by_hand)
|
|
2643
|
+
)
|
|
2644
|
+
if not steps:
|
|
2645
|
+
steps.append(
|
|
2646
|
+
"Already set up. Edit the workflow directly to change its install steps, secrets, runner, or Environment"
|
|
2647
|
+
)
|
|
2648
|
+
return {
|
|
2649
|
+
"files": [path] if updated else [],
|
|
2650
|
+
"workflow": path,
|
|
2651
|
+
"updated": updated,
|
|
2652
|
+
"next": steps,
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
|
|
2656
|
+
def initialize(argv, name="--cloud-init"):
|
|
2394
2657
|
parser = argparse.ArgumentParser(
|
|
2395
|
-
prog="bitfab-replay
|
|
2396
|
-
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.
|
|
2658
|
+
prog=f"bitfab-replay {name}",
|
|
2659
|
+
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 BITFAB_REPLAY_BITFAB_API_KEY) and lists what it changed; edit it directly for anything else.",
|
|
2397
2660
|
)
|
|
2398
2661
|
parser.add_argument(
|
|
2399
2662
|
"--secret",
|
|
@@ -2425,13 +2688,7 @@ def initialize(argv):
|
|
|
2425
2688
|
refuse_old_setup(root)
|
|
2426
2689
|
existing = find_workflow(root)
|
|
2427
2690
|
if existing is not None:
|
|
2428
|
-
return {
|
|
2429
|
-
"files": [],
|
|
2430
|
-
"workflow": f"{WORKFLOW_DIRECTORY}/{existing}",
|
|
2431
|
-
"next": [
|
|
2432
|
-
"Already set up. Edit the workflow directly to change its install steps, secrets, runner, or Environment"
|
|
2433
|
-
],
|
|
2434
|
-
}
|
|
2691
|
+
return update_workflow(root, f"{WORKFLOW_DIRECTORY}/{existing}")
|
|
2435
2692
|
prefix = args.secret_prefix
|
|
2436
2693
|
if prefix and not re.fullmatch(r"[A-Z][A-Z0-9_]*_", prefix):
|
|
2437
2694
|
raise ValueError(
|
|
@@ -2450,6 +2707,7 @@ def initialize(argv):
|
|
|
2450
2707
|
'Pass --run with the command that starts your registry program on the runner, such as --run "go run ./cmd/registry"'
|
|
2451
2708
|
)
|
|
2452
2709
|
env = {name: "${{ secrets." + prefix + name + " }}" for name in names}
|
|
2710
|
+
env[API_KEY_ENV] = KEY_SECRET_REFERENCE
|
|
2453
2711
|
if args.check:
|
|
2454
2712
|
env[CHECK_COMMAND_ENV] = args.check
|
|
2455
2713
|
path = f"{WORKFLOW_DIRECTORY}/{WORKFLOW}"
|
|
@@ -2506,8 +2764,8 @@ def print_json(value):
|
|
|
2506
2764
|
def main():
|
|
2507
2765
|
argv = sys.argv[1:]
|
|
2508
2766
|
try:
|
|
2509
|
-
if argv[:1]
|
|
2510
|
-
print_json(initialize(argv[1:]))
|
|
2767
|
+
if argv[:1] in (["--cloud-init"], ["--cloud-update"]):
|
|
2768
|
+
print_json(initialize(argv[1:], argv[0]))
|
|
2511
2769
|
return 0
|
|
2512
2770
|
if argv[:1] == ["--cloud-secrets"]:
|
|
2513
2771
|
print_json(configure_secrets(argv[1:]))
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
serializeValue,
|
|
18
18
|
toJsonSafe,
|
|
19
19
|
toJsonSafeReport
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-QZFVHNQE.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-I3LSDFMN.js";
|
|
40
40
|
import {
|
|
41
41
|
__privateAdd,
|
|
42
42
|
__privateGet,
|
|
@@ -3376,6 +3376,9 @@ function draftsInGenerationOrder(assertions, assertionIds) {
|
|
|
3376
3376
|
function traceAssertionsPath(traceId, suffix = "") {
|
|
3377
3377
|
return `/api/sdk/traces/${encodeURIComponent(traceId)}/assertions${suffix}`;
|
|
3378
3378
|
}
|
|
3379
|
+
function traceScenarioPath(traceId) {
|
|
3380
|
+
return `/api/sdk/traces/${encodeURIComponent(traceId)}/scenario`;
|
|
3381
|
+
}
|
|
3379
3382
|
var TracesClient = class {
|
|
3380
3383
|
constructor(httpClient) {
|
|
3381
3384
|
this.httpClient = httpClient;
|
|
@@ -3439,6 +3442,22 @@ var TracesClient = class {
|
|
|
3439
3442
|
);
|
|
3440
3443
|
return response.archived;
|
|
3441
3444
|
}
|
|
3445
|
+
async getScenario(traceId) {
|
|
3446
|
+
const response = await this.httpClient.get(
|
|
3447
|
+
traceScenarioPath(traceId)
|
|
3448
|
+
);
|
|
3449
|
+
return response.scenario;
|
|
3450
|
+
}
|
|
3451
|
+
async saveScenario({
|
|
3452
|
+
traceId,
|
|
3453
|
+
...edit
|
|
3454
|
+
}) {
|
|
3455
|
+
const response = await this.httpClient.request(
|
|
3456
|
+
traceScenarioPath(traceId),
|
|
3457
|
+
edit
|
|
3458
|
+
);
|
|
3459
|
+
return response.scenario;
|
|
3460
|
+
}
|
|
3442
3461
|
};
|
|
3443
3462
|
|
|
3444
3463
|
// src/tracing.ts
|
|
@@ -4325,6 +4344,7 @@ function readEnv2(name) {
|
|
|
4325
4344
|
return void 0;
|
|
4326
4345
|
}
|
|
4327
4346
|
var CAPTURE_ENABLED_ENV = "BITFAB_CAPTURE_ENABLED";
|
|
4347
|
+
var REPLAY_API_KEY_ENV = "BITFAB_REPLAY_BITFAB_API_KEY";
|
|
4328
4348
|
var TRUE_ENV_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes"]);
|
|
4329
4349
|
var FALSE_ENV_VALUES = /* @__PURE__ */ new Set(["0", "false", "no"]);
|
|
4330
4350
|
function readBooleanEnv(name) {
|
|
@@ -4839,6 +4859,10 @@ var Bitfab = class {
|
|
|
4839
4859
|
* early resolve that found nothing never poisons a later one.
|
|
4840
4860
|
*/
|
|
4841
4861
|
findApiKey(callsKeyFunction) {
|
|
4862
|
+
const replayKey = readEnv2(REPLAY_API_KEY_ENV);
|
|
4863
|
+
if (replayKey && replayKey.trim() !== "") {
|
|
4864
|
+
return replayKey;
|
|
4865
|
+
}
|
|
4842
4866
|
if (this.resolvedApiKey !== void 0) {
|
|
4843
4867
|
return this.resolvedApiKey;
|
|
4844
4868
|
}
|
|
@@ -6283,7 +6307,7 @@ var Bitfab = class {
|
|
|
6283
6307
|
await runWithSeedContext({ traceId }, async () => target(...args));
|
|
6284
6308
|
} finally {
|
|
6285
6309
|
try {
|
|
6286
|
-
const { flushTraces: flushTraces2 } = await import("./http-
|
|
6310
|
+
const { flushTraces: flushTraces2 } = await import("./http-DAQLSTUN.js");
|
|
6287
6311
|
await flushTraces2(3e4);
|
|
6288
6312
|
} finally {
|
|
6289
6313
|
unrecorded = activeTraceStates.delete(traceId);
|
|
@@ -6342,7 +6366,7 @@ var Bitfab = class {
|
|
|
6342
6366
|
`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.`
|
|
6343
6367
|
);
|
|
6344
6368
|
}
|
|
6345
|
-
const { replay: doReplay } = await import("./replay-
|
|
6369
|
+
const { replay: doReplay } = await import("./replay-FFNEMPGO.js");
|
|
6346
6370
|
return doReplay(
|
|
6347
6371
|
this.httpClient,
|
|
6348
6372
|
this.serviceUrl,
|
|
@@ -6688,7 +6712,7 @@ async function seedFromRegistry(registry, pipeline, cases, options = {}) {
|
|
|
6688
6712
|
sessionId: seedCase.sessionId
|
|
6689
6713
|
})
|
|
6690
6714
|
);
|
|
6691
|
-
const { flushTraces: flushTraces2 } = await import("./http-
|
|
6715
|
+
const { flushTraces: flushTraces2 } = await import("./http-DAQLSTUN.js");
|
|
6692
6716
|
await flushTraces2(3e4);
|
|
6693
6717
|
return { pipeline, traceFunctionKey, traceIds };
|
|
6694
6718
|
}
|
|
@@ -6733,4 +6757,4 @@ export {
|
|
|
6733
6757
|
reseedFromRegistry,
|
|
6734
6758
|
seedFromRegistry
|
|
6735
6759
|
};
|
|
6736
|
-
//# sourceMappingURL=chunk-
|
|
6760
|
+
//# sourceMappingURL=chunk-G5BYSZLD.js.map
|