bitfab 0.72.1 → 0.73.1

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: 750acbbb2c4ff46221148021fd6cfe9d159dbfc0d4c1c7020c705b6f703a09c1
4
- data.tar.gz: 7d6d64ec6afc5e8d78df539fa1f25cf4633d07469d59e75a33f2aa0118f4f3ea
3
+ metadata.gz: 23076d3ce8682540fa7b9104949899c4292d5ac8aab6cd3b8a8adb77ff379f5e
4
+ data.tar.gz: 1464557fde7f948ac8aeb34ab4c66d9c947dd30b352d8a2862d74a835ca3d061
5
5
  SHA512:
6
- metadata.gz: 68bed12848716050b8a5a9229af72f7c946e126adae28588207069731750cdeab6850ce15f6a0e5e77fa0a29c7df7321b96465fd5f31c6fac879eaaed03e28d2
7
- data.tar.gz: 8d436252fb328617a0b40e4e26e6d6a3641baf9b64faf1884cf806d0554d313bd3f720210cb4e9567e7b1c0e33827335593e52291946ca56e2690de712c67796
6
+ metadata.gz: 652fd2e9a0285090fff5055b615f04ba48d48b49de4ba27fd4256f59f660d58ea2d4afb6d128eaf9b4f50cbe0a61d0ca022cc90ab59db76d1e0206f5ac11c3c7
7
+ data.tar.gz: b40ea515566dcae4643ad52b9732244b6a8663a43db9b092cebc7af5360de7df95a77e15a91231ce06b5dc1acb801f3933f69a0cb2072ec0c03d74017c79bc05
@@ -152,6 +152,11 @@ CODE_CHANGE_FLAG = "--code-change"
152
152
  CODE_CHANGE_BASE_FLAG = "--code-change-base"
153
153
  BRANCH_CODE_CHANGES = ("last-branch", "default-branch", "ref")
154
154
  GIT_CODE_CHANGES = ("working-tree", *BRANCH_CODE_CHANGES)
155
+ FETCHED_CODE_CHANGES = ("default-branch", "ref")
156
+ CHECKOUT_ONLY_CODE_CHANGES = ("working-tree", "last-branch")
157
+ GIT_TOKEN_ENV = "BITFAB_REPLAY_GIT_TOKEN"
158
+ GIT_TOKEN_VALUE = "${{ github.token }}"
159
+ FETCH_SECONDS = 600
155
160
  DESCRIBED_REF = re.compile(r"[^\x00-\x1f\x7f]{1,200}")
156
161
  CHECKOUT_DEPTH = 2
157
162
  CODE_CHANGE_PATH_ENV = "BITFAB_CODE_CHANGE_PATH"
@@ -223,9 +228,12 @@ Options added by --cloud:
223
228
  checkout of the repository, with GitHub access that can
224
229
  read it and run Actions (a job token needs contents: read
225
230
  and actions: write, passed as GH_TOKEN), and a workflow on
226
- BRANCH that declares a request input. With no snapshot
227
- there is nothing to diff against, so it takes
228
- --code-change skip or manual, as does a dispatched run.
231
+ BRANCH that declares a request input. With no snapshot,
232
+ --code-change default-branch and ref fetch their base on
233
+ the runner with the run's GitHub token, which the
234
+ workflow passes as BITFAB_REPLAY_GIT_TOKEN and the
235
+ replay never sees; working-tree and last-branch are
236
+ refused, as they are on a dispatched run.
229
237
 
230
238
  A workflow can also dispatch bitfab-replay.yml itself on any branch other than a
231
239
  bitfab-replay/ snapshot branch, passing the replay as JSON in the request input:
@@ -1048,9 +1056,15 @@ def chosen_base_point(root, chosen):
1048
1056
 
1049
1057
  def refuse_git_code_change(replay, where):
1050
1058
  kind = flag_value(replay, CODE_CHANGE_FLAG)
1051
- if kind in GIT_CODE_CHANGES:
1059
+ if kind in CHECKOUT_ONLY_CODE_CHANGES:
1052
1060
  raise ValueError(
1053
- f"{where} as it is on GitHub, with no snapshot of a checkout, so --code-change {kind} has nothing to compare against there; pass --code-change skip, or --code-change manual with --code-change-file, or start it with bitfab-replay --cloud and no --cloud-ref, which resolves {kind} from your checkout before upload"
1061
+ f"{where} as it is on GitHub, with no snapshot of a checkout, so --code-change {kind} has nothing to compare against there; pass --code-change default-branch, --code-change ref with {CODE_CHANGE_BASE_FLAG}, --code-change skip, or --code-change manual with --code-change-file, or start it with bitfab-replay --cloud and no --cloud-ref, which resolves {kind} from your checkout before upload"
1062
+ )
1063
+ if kind == "ref" and not BRANCH_NAME.fullmatch(
1064
+ flag_value(replay, CODE_CHANGE_BASE_FLAG) or ""
1065
+ ):
1066
+ raise ValueError(
1067
+ f"{where} as it is on GitHub, so --code-change ref needs {CODE_CHANGE_BASE_FLAG} naming a branch or commit SHA on GitHub to fetch there"
1054
1068
  )
1055
1069
 
1056
1070
 
@@ -2069,14 +2083,29 @@ def snapshot_request(root, commit):
2069
2083
  return request
2070
2084
 
2071
2085
 
2072
- def dispatched_inputs():
2086
+ def dispatch_event():
2073
2087
  path = os.environ.get("GITHUB_EVENT_PATH")
2074
2088
  if not path or not Path(path).is_file():
2075
2089
  return {}
2076
- inputs = json.loads(Path(path).read_text()).get("inputs")
2090
+ event = json.loads(Path(path).read_text())
2091
+ return event if isinstance(event, dict) else {}
2092
+
2093
+
2094
+ def dispatched_inputs():
2095
+ inputs = dispatch_event().get("inputs")
2077
2096
  return inputs if isinstance(inputs, dict) else {}
2078
2097
 
2079
2098
 
2099
+ def event_default_branch():
2100
+ repository = dispatch_event().get("repository")
2101
+ branch = repository.get("default_branch") if isinstance(repository, dict) else None
2102
+ if not isinstance(branch, str) or not BRANCH_NAME.fullmatch(branch):
2103
+ raise ValueError(
2104
+ "The run's event names no default branch, so --code-change default-branch has nothing to compare against; pass --code-change ref with --code-change-base"
2105
+ )
2106
+ return branch
2107
+
2108
+
2080
2109
  def decode_request(text):
2081
2110
  try:
2082
2111
  return json.loads(text)
@@ -2279,6 +2308,72 @@ def runner_code_change(root, base, commit, request):
2279
2308
  return change or {"files": []}
2280
2309
 
2281
2310
 
2311
+ def git_token_environment(token):
2312
+ environment = {**os.environ, "GIT_TERMINAL_PROMPT": "0"}
2313
+ if not token:
2314
+ return environment
2315
+ basic = base64.b64encode(f"x-access-token:{token}".encode()).decode()
2316
+ return {
2317
+ **environment,
2318
+ "GIT_CONFIG_COUNT": "1",
2319
+ "GIT_CONFIG_KEY_0": "http.https://github.com/.extraheader",
2320
+ "GIT_CONFIG_VALUE_0": f"AUTHORIZATION: basic {basic}",
2321
+ }
2322
+
2323
+
2324
+ def fetched_base(root, ref, token):
2325
+ shallow = quiet_git(root, "rev-parse", "--is-shallow-repository") == "true"
2326
+ try:
2327
+ command(
2328
+ [
2329
+ "git",
2330
+ "-C",
2331
+ str(root),
2332
+ "fetch",
2333
+ "-q",
2334
+ "--no-tags",
2335
+ *(["--unshallow"] if shallow else []),
2336
+ "origin",
2337
+ ref,
2338
+ ],
2339
+ env=git_token_environment(token),
2340
+ timeout=FETCH_SECONDS,
2341
+ )
2342
+ except (CommandError, subprocess.TimeoutExpired) as error:
2343
+ hint = (
2344
+ "check that it exists on GitHub"
2345
+ if token
2346
+ else f"the workflow passes no {GIT_TOKEN_ENV} to fetch it with, so run bitfab-replay --cloud-update and commit the workflow it updates"
2347
+ )
2348
+ raise ValueError(
2349
+ f"Could not fetch {ref} from origin to compare against ({error}); {hint}, or pass --code-change skip"
2350
+ ) from None
2351
+ base = quiet_git(root, "merge-base", "HEAD", "FETCH_HEAD")
2352
+ if not base:
2353
+ raise ValueError(
2354
+ f"This branch shares no history with {ref}, so there is nothing to compare against"
2355
+ )
2356
+ return base
2357
+
2358
+
2359
+ def dispatched_code_change(root, commit, request, token):
2360
+ args = request["args"]
2361
+ kind = flag_value(args, CODE_CHANGE_FLAG)
2362
+ if kind not in FETCHED_CODE_CHANGES:
2363
+ return None
2364
+ ref = (
2365
+ flag_value(args, CODE_CHANGE_BASE_FLAG)
2366
+ if kind == "ref"
2367
+ else event_default_branch()
2368
+ )
2369
+ print(f"Fetching {ref} to record the code change against it", flush=True)
2370
+ base = fetched_base(root, ref, token)
2371
+ change = snapshot_code_change(
2372
+ root, base, commit, replay_name(args), f"vs {ref}", commit
2373
+ )
2374
+ return change or {"files": []}
2375
+
2376
+
2282
2377
  def write_code_change(change):
2283
2378
  handle, path = tempfile.mkstemp(
2284
2379
  prefix="bitfab-code-change-",
@@ -2293,14 +2388,16 @@ def write_code_change(change):
2293
2388
  def execute():
2294
2389
  if os.environ.get("GITHUB_RUN_ATTEMPT") != "1":
2295
2390
  raise ValueError("Submit a new replay instead of rerunning an Actions job")
2391
+ git_token = os.environ.pop(GIT_TOKEN_ENV, None)
2296
2392
  root = root_directory()
2297
2393
  commit = os.environ["GITHUB_SHA"]
2298
2394
  if git(root, "rev-parse", "HEAD") != commit:
2299
2395
  raise ValueError("Runner checkout does not match the dispatched commit")
2300
2396
  request = runner_request(root, commit)
2397
+ snapshot = os.environ.get("GITHUB_REF_NAME", "").startswith(PREFIX)
2301
2398
  base = (
2302
2399
  check_out_snapshot_base(root, commit, request.get("branch"))
2303
- if os.environ.get("GITHUB_REF_NAME", "").startswith(PREFIX)
2400
+ if snapshot
2304
2401
  else None
2305
2402
  )
2306
2403
  directory = within(root, request["cwd"])
@@ -2309,7 +2406,12 @@ def execute():
2309
2406
  if environment:
2310
2407
  workflow_environment_conflicts(root, environment)
2311
2408
  print(f"Setting {', '.join(sorted(environment))} from the request", flush=True)
2312
- change = None if base is None else runner_code_change(root, base, commit, request)
2409
+ if snapshot:
2410
+ change = (
2411
+ None if base is None else runner_code_change(root, base, commit, request)
2412
+ )
2413
+ else:
2414
+ change = dispatched_code_change(root, commit, request, git_token)
2313
2415
  run_environment = dict(environment)
2314
2416
  if change is not None:
2315
2417
  run_environment[CODE_CHANGE_PATH_ENV] = write_code_change(change)
@@ -3087,7 +3189,7 @@ def workflow_document(steps, run, directory, env, *, runs_on, environment):
3087
3189
  {
3088
3190
  **({} if directory == "." else {"working-directory": directory}),
3089
3191
  "run": f"{run} --cloud-execute",
3090
- "env": env,
3192
+ "env": {**env, GIT_TOKEN_ENV: GIT_TOKEN_VALUE},
3091
3193
  },
3092
3194
  ]
3093
3195
  return {
@@ -3226,6 +3328,46 @@ def add_checkout_depth(lines):
3226
3328
  return "unchanged"
3227
3329
 
3228
3330
 
3331
+ def has_git_token_env(lines):
3332
+ return any(
3333
+ re.match(rf'[ \t]*(- +)?"?{GIT_TOKEN_ENV}"?[ \t]*:', without_yaml_comment(line))
3334
+ for line in lines
3335
+ )
3336
+
3337
+
3338
+ def add_git_token_env(lines):
3339
+ execute = next(
3340
+ (
3341
+ index
3342
+ for index in range(len(lines) - 1, -1, -1)
3343
+ if "--cloud-execute" in without_yaml_comment(lines[index])
3344
+ ),
3345
+ None,
3346
+ )
3347
+ if execute is None:
3348
+ return False
3349
+ start = execute
3350
+ while start > 0 and not re.match(r"[ \t]*- ", lines[start]):
3351
+ start -= 1
3352
+ key = len(lines[start]) - len(lines[start].lstrip(" \t-"))
3353
+ end = block_end(lines, start, key - 1)
3354
+ entry = f"{GIT_TOKEN_ENV}: {json.dumps(GIT_TOKEN_VALUE)}"
3355
+ for at in range(start, end):
3356
+ env_key = re.fullmatch(
3357
+ r"[ \t]*(- +)?\"?env\"?[ \t]*:(.*)", without_yaml_comment(lines[at])
3358
+ )
3359
+ if not env_key or len(lines[at]) - len(lines[at].lstrip(" \t-")) != key:
3360
+ continue
3361
+ if env_key[2].strip():
3362
+ return False
3363
+ entries = block_end(lines, at, key)
3364
+ child = indentation(lines[at + 1]) if entries > at + 1 else key + 2
3365
+ lines.insert(entries, " " * child + entry)
3366
+ return True
3367
+ lines[end:end] = [" " * key + "env:", " " * (key + 2) + entry]
3368
+ return True
3369
+
3370
+
3229
3371
  def add_id_token_permission(lines):
3230
3372
  for index, line in enumerate(lines):
3231
3373
  if not re.fullmatch(r'"?permissions"?[ \t]*:[ \t]*(#.*)?', line):
@@ -3412,6 +3554,15 @@ def update_workflow(root, path, *, remove_old_key_secrets=False):
3412
3554
  updated.append(
3413
3555
  f"Removed the {API_KEY_ENV} line, which read a secret by a name built from the run's inputs and so gave the runner every secret in the repository; the runner now gets the key from Bitfab"
3414
3556
  )
3557
+ if not has_git_token_env(lines):
3558
+ if add_git_token_env(lines):
3559
+ updated.append(
3560
+ f"Passed the run's GitHub token to the replay step as {GIT_TOKEN_ENV}, so a --cloud-ref or dispatched replay can fetch the branch it compares against for --code-change default-branch or ref; the replay itself never sees it"
3561
+ )
3562
+ else:
3563
+ by_hand.append(
3564
+ f"{GIT_TOKEN_ENV}: {json.dumps(GIT_TOKEN_VALUE)} in the env of the step that runs --cloud-execute"
3565
+ )
3415
3566
  depth = add_checkout_depth(lines)
3416
3567
  if depth == "updated":
3417
3568
  updated.append(
@@ -3450,7 +3601,7 @@ def update_workflow(root, path, *, remove_old_key_secrets=False):
3450
3601
  def initialize(argv, name="--cloud-init"):
3451
3602
  parser = argparse.ArgumentParser(
3452
3603
  prog=f"bitfab-replay {name}",
3453
- 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.",
3604
+ 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, the id-token: write permission, and the BITFAB_REPLAY_GIT_TOKEN line that lets a --cloud-ref or dispatched replay fetch the branch it compares against), 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.",
3454
3605
  )
3455
3606
  parser.add_argument(
3456
3607
  "--secret",
@@ -9,14 +9,22 @@ module Bitfab
9
9
  RECORDS_KEYS_IN_DATA = %w[records sources].freeze
10
10
  private_constant :RECORDS_KEYS_IN_DATA
11
11
 
12
+ # Reads the scenario built from trace_id, in any creation state, and returns its hash. "creationStatus" is
13
+ # "empty" when nothing has started building it, "generating" while Bitfab builds it or waits for the scenario
14
+ # callback to save it, "created" once it is built, and "error" when building failed, with "creationError" saying
15
+ # why. "trigger", "data", "triggerSummary", and "dataSummary" are nil until the scenario is built. After
16
+ # generate_scenario, poll this until "creationStatus" is "created" or "error". A trace with no scenario at all
17
+ # raises an error.
12
18
  def get_scenario(trace_id)
13
19
  @http_client.get(scenario_path(trace_id))["scenario"]
14
20
  end
15
21
 
16
- # Starts building the scenario for trace_id and returns at once, without waiting for it. Read it later with
17
- # get_scenario. Returns {"status", "traceId", "scenarioId"}: "status" is "started" while Bitfab builds it in the
18
- # background, or "already_generated" when the trace already has a scenario, which is left as it is and named by
19
- # "scenarioId". "scenarioId" is nil while the trace has no scenario yet.
22
+ # Starts building the scenario for trace_id and returns at once, without waiting for it. Poll get_scenario until
23
+ # its "creationStatus" is "created" or "error". Returns {"status", "traceId", "scenarioId", "creationStatus"}:
24
+ # "status" is "started" while Bitfab builds it in the background, or "already_generated" when the trace already
25
+ # has a scenario, which is left as it is and named by "scenarioId". A scenario whose build failed is started
26
+ # again. "scenarioId" is nil while the trace has no scenario yet, and "creationStatus" is the scenario's creation
27
+ # status right after the call.
20
28
  #
21
29
  # A replay, a trace outside the organization, or a trace Bitfab cannot build a scenario for (not
22
30
  # noah-agent-turn and no scenario callback) raises an error. Adding a trace to a dataset starts the same build
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.72.1"
4
+ VERSION = "0.73.1"
5
5
  end
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.72.1
4
+ version: 0.73.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team