bitfab 0.64.13 → 0.64.15

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44a3233601b36a74eec1e40a2ebcea100a0501f5cbb94a3009862c4ee0b05923
4
- data.tar.gz: be71d868cd14c69b00ee6be5de3a92e67c50202ae1dac3321cf3ba6f96e0a570
3
+ metadata.gz: 4ae38a6da8055dd696639f719a5fd0910e97818aee2f1ec0e74c0d918beb8a3a
4
+ data.tar.gz: 6004d03970985aa1381ef57ae4c7481195c50c904bc672677155b786e0f060f5
5
5
  SHA512:
6
- metadata.gz: 6452180b7db74604f10a57611036bf787308a491126c3159311f6eb5402e5c647b6d89b8cf24d242e3c76466e59959e16964a70ec68e5b94a9a39dfc873b2b66
7
- data.tar.gz: d29e63513f3e2e0002b06e24848664858cf40853b69e36d8ab434e26a975e49a7edf3d4411bb9d3d36f93dca21a7602f40ecdbbd7c16eebf2e4c25c45afb0423
6
+ metadata.gz: 6c7bc1dcf702f9d3c831df3a68a566c7186f352247bf7faa157d164237257874692d4334ade0da3ab90c3ec23293769a7610ed982527993173cd9c1d8622806e
7
+ data.tar.gz: 54fa2229794f023b7eaaae39be309c474a5e561baf55e7e63b2e2545c7279ec2859d6753e5a7658f69fd6c1a2bc40a6a13cc30202ccdca01fe8bba374e4cf51a
data/lib/bitfab/client.rb CHANGED
@@ -69,9 +69,6 @@ module Bitfab
69
69
  CommitRef.start_resolution
70
70
  end
71
71
 
72
- # The configured API key (a proc is resolved on read). Reflects what was
73
- # passed to the client; the ENV["BITFAB_API_KEY"] fallback applied during
74
- # actual tracing is not surfaced here, and reading this never warns.
75
72
  def api_key
76
73
  @api_key_config.respond_to?(:call) ? @api_key_config.call : @api_key_config
77
74
  end
@@ -788,28 +785,27 @@ module Bitfab
788
785
  @api_key_config.respond_to?(:call) || @api_key_config.to_s.strip.empty?
789
786
  end
790
787
 
791
- # Resolve the API key lazily, the first time a span actually needs it.
792
- #
793
- # The key is intentionally NOT read at construction: a client built when a
794
- # file is required can run before the app loads its env, so the key would be
795
- # empty at construction even though it is set moments later. Resolution
796
- # order: the configured value (a proc is called while still unresolved),
797
- # then a fallback read of ENV["BITFAB_API_KEY"]. Once a non-empty key is
798
- # found it is cached.
799
- def resolve_api_key
800
- return @resolved_api_key unless @resolved_api_key.nil?
788
+ def lookup_api_key
789
+ replay_key = ENV[REPLAY_API_KEY_ENV]
790
+ return replay_key unless blank_api_key?(replay_key)
791
+
792
+ @resolved_api_key ||= configured_or_env_api_key
793
+ end
801
794
 
795
+ def configured_or_env_api_key
802
796
  from_config = @api_key_config.respond_to?(:call) ? @api_key_config.call : @api_key_config
803
- candidate = if from_config && !from_config.to_s.strip.empty?
804
- from_config
805
- else
806
- ENV["BITFAB_API_KEY"]
807
- end
808
- key = (candidate && !candidate.to_s.strip.empty?) ? candidate : nil
809
- if key
810
- @resolved_api_key = key
811
- return key
812
- end
797
+ candidate = blank_api_key?(from_config) ? ENV["BITFAB_API_KEY"] : from_config
798
+ blank_api_key?(candidate) ? nil : candidate
799
+ end
800
+
801
+ def blank_api_key?(value)
802
+ !value || value.to_s.strip.empty?
803
+ end
804
+
805
+ def resolve_api_key
806
+ key = lookup_api_key
807
+ return key if key
808
+
813
809
  if @strict
814
810
  raise "Bitfab: no API key resolved. Set BITFAB_API_KEY or pass api_key: to " \
815
811
  "Bitfab.configure. If a script loads env later, load it before the first " \
@@ -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
- declared = declared_inputs(
1339
- within(root, f"{WORKFLOW_DIRECTORY}/{workflow}").read_text()
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
- return dispatch(record, {"execution_id": execution_id, "request": "-"}, declared)
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
- declared = declared_inputs(branch_workflow(repo, workflow, sha))
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
- step.get("status") in ("in_progress", "completed")
1482
- and step.get("name") not in shown
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": "Bitfab replay ${{ inputs.execution_id || github.ref_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 initialize(argv):
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 --cloud-init",
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. An existing workflow is left alone; edit it directly to change it.",
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] == ["--cloud-init"]:
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:]))
@@ -3,6 +3,8 @@
3
3
  module Bitfab
4
4
  DEFAULT_SERVICE_URL = "https://bitfab.ai"
5
5
  CAPTURE_ENABLED_ENV = "BITFAB_CAPTURE_ENABLED"
6
+ REPLAY_API_KEY_ENV = "BITFAB_REPLAY_BITFAB_API_KEY"
7
+ BITFAB_API_KEY_PREFIX = "@@bitfab:api-key "
6
8
  REPLAY_CONTEXT_KEY = :__bitfab_replay_context
7
9
 
8
10
  # Wire prefix the Bitfab plugin scans for on stderr to report live progress
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require "optparse"
4
5
  require_relative "cloud_replay_cli"
5
6
 
@@ -8,6 +9,7 @@ module Bitfab
8
9
  module ReplayCommand
9
10
  CHECK_ARGUMENTS_FLAG = "--check-arguments"
10
11
  USAGE = "Usage: bitfab-replay --registry <path> <pipeline> [replay options]"
12
+ PRINT_API_KEY_FLAG = "--print-api-key"
11
13
  HELP = "#{USAGE}\nRun bitfab-replay --registry <path> --help to list replay options.\n#{CloudReplayCli::HINT}"
12
14
 
13
15
  module_function
@@ -20,6 +22,9 @@ module Bitfab
20
22
  raise ArgumentError, "Expected one process assignment path" unless replay_argv.length == 2
21
23
  return ReplayProcesses.execute_assignment(registry, replay_argv[1], stderr:)
22
24
  end
25
+ if replay_argv.include?(PRINT_API_KEY_FLAG)
26
+ return print_api_key(registry, replay_argv - [PRINT_API_KEY_FLAG], stdout:)
27
+ end
23
28
  ReplayProcesses.with_registry(registry_path, replay_argv, stderr:) do
24
29
  ReplayCli.run(registry, argv: replay_argv, stdout:, stderr:)
25
30
  end
@@ -46,6 +51,13 @@ module Bitfab
46
51
  0
47
52
  end
48
53
 
54
+ def print_api_key(registry, replay_argv, stdout:)
55
+ pipeline = ReplayCli.parse(registry, replay_argv.dup).fetch(:pipeline)
56
+ key = registry.fetch(pipeline).client.send(:lookup_api_key)
57
+ stdout.puts "#{BITFAB_API_KEY_PREFIX}#{JSON.generate({apiKey: key&.to_s})}"
58
+ nil
59
+ end
60
+
49
61
  def parse(argv)
50
62
  values = argv.dup
51
63
  registry_index = values.index("--registry")
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Bitfab
6
+ class Traces
7
+ def get_scenario(trace_id)
8
+ @http_client.get(scenario_path(trace_id))["scenario"]
9
+ end
10
+
11
+ def save_scenario(trace_id, name: nil, description: nil, trigger: nil, data: nil, append_data: nil)
12
+ if !data.nil? && !append_data.nil?
13
+ raise ArgumentError, "Pass data or append_data, not both: data replaces the saved data whole."
14
+ end
15
+
16
+ payload = {}
17
+ payload["name"] = name unless name.nil?
18
+ payload["description"] = description unless description.nil?
19
+ payload["trigger"] = trigger unless trigger.nil?
20
+ payload["data"] = data unless data.nil?
21
+ payload["appendData"] = append_data unless append_data.nil?
22
+ if payload.empty?
23
+ raise ArgumentError, "Pass at least one of name, description, trigger, data, or append_data: there is nothing to save otherwise."
24
+ end
25
+
26
+ @http_client.request(scenario_path(trace_id), payload)["scenario"]
27
+ end
28
+
29
+ private
30
+
31
+ def scenario_path(trace_id)
32
+ "/api/sdk/traces/#{URI.encode_www_form_component(trace_id).gsub("+", "%20")}/scenario"
33
+ end
34
+ end
35
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.64.13"
4
+ VERSION = "0.64.15"
5
5
  end
data/lib/bitfab.rb CHANGED
@@ -19,6 +19,7 @@ require_relative "bitfab/assertion_categories"
19
19
  require_relative "bitfab/organization_members"
20
20
  require_relative "bitfab/traces"
21
21
  require_relative "bitfab/assertions"
22
+ require_relative "bitfab/scenarios"
22
23
  require_relative "bitfab/labels"
23
24
  require_relative "bitfab/graders"
24
25
  require_relative "bitfab/experiments"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitfab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.64.13
4
+ version: 0.64.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team
@@ -188,6 +188,7 @@ files:
188
188
  - lib/bitfab/replay_memory.rb
189
189
  - lib/bitfab/replay_processes.rb
190
190
  - lib/bitfab/replay_registry.rb
191
+ - lib/bitfab/scenarios.rb
191
192
  - lib/bitfab/seed.rb
192
193
  - lib/bitfab/seed_cli.rb
193
194
  - lib/bitfab/seed_context.rb