bitfab 0.64.17 → 0.64.18

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: 8a86045717d7c97884ea2005ef2e965cf02e53afcc57b4ae9681b5d75d64303f
4
- data.tar.gz: 9f4903a7ad1771622b2608838667441d4d5ea8b3fcab14597f7af9a70b16882a
3
+ metadata.gz: 9b213b2d758ad1fbcafc95b9f3704d6fa06c5b288be373dfd9c97565c699eaa4
4
+ data.tar.gz: 67f58161475956d24a8bac5592e080b42892b277fc0fd132e9a1b679254fddbc
5
5
  SHA512:
6
- metadata.gz: 267f523ef41019703bc9c35c4c1c608d07583b7e1a50b4108ca7379cbf9d93dce08b057cccb723ca18504b794b7d460a4e0653cc1a64aee8476fb1c78a3fb862
7
- data.tar.gz: 75a9dec7bf4ff1c8c8a2140f3ebd9a2a1fc04737ea691fc9560711fe035872d3990a31d2216a720e2da25581ec93988a60dadcf962854ff16a8bad078a3045a0
6
+ metadata.gz: 54b089f5d3a87132403ec8bff156d0a2bcb596480956c14b9bdb8f158c877589a33a30a8ee1eafaed363ff1ac97b61638997abed639aeb487f16eaba6fd90c81
7
+ data.tar.gz: c34da0a8b44cb56dad4d788188940fb11197b8377155ff614fb7dd930141dde8911f4bdbdb83480c0a1a4cca73748e2b296459430c3641002a8e1a4a8fe9b6c8
@@ -137,6 +137,11 @@ RENAMED_FLAGS = {
137
137
  SEED_FLAGS = ("--seed", "--cases", "--from-trace", "--run")
138
138
  PATH_FLAGS = ("--registry", "--params", "--code-change")
139
139
  CODE_CHANGE_FLAGS = ("--code-change", "--no-code-change")
140
+ CHECKOUT_DEPTH = 2
141
+ CODE_CHANGE_PATH_ENV = "BITFAB_CODE_CHANGE_PATH"
142
+ CODE_CHANGE_MAX_FILES = 60
143
+ CODE_CHANGE_MAX_FILE_BYTES = 500_000
144
+ CODE_CHANGE_MAX_TOTAL_BYTES = 2_000_000
140
145
  CHECK_ARGUMENTS_SECONDS = 300
141
146
  CHECK_ARGUMENTS_FLAG = "--check-arguments"
142
147
  REPLAY_ARGUMENTS_FLAG = "--replay-arguments"
@@ -165,6 +170,8 @@ Every replay option works as it does locally, including --dry-run and --fail-on-
165
170
  and your SDK checks them before anything is uploaded. The runner replays a snapshot of
166
171
  your working tree (changed files, plus new files git does not ignore) from the same
167
172
  directory, and this command prints the replay's output and exits with its exit code.
173
+ The experiment records your branch, the commit the snapshot was taken on, and the
174
+ snapshot's changed files as its code change, as it would for a local replay.
168
175
  Credential-like files that are not on GitHub yet are refused unless you name them with
169
176
  --cloud-allow-file. Requires git and Python 3.10+ on macOS or Linux, with a github.com
170
177
  origin.
@@ -887,6 +894,13 @@ def replay_request(root, parsed):
887
894
  return request, files
888
895
 
889
896
 
897
+ def current_branch(root):
898
+ try:
899
+ return git(root, "symbolic-ref", "--short", "-q", "HEAD") or None
900
+ except CommandError:
901
+ return None
902
+
903
+
890
904
  def sensitive(path):
891
905
  parts = Path(path).parts
892
906
  name = Path(path).name.lower()
@@ -1300,6 +1314,9 @@ def allowed_files(root, parsed):
1300
1314
  def submit(root, repo, workflow, parsed, path):
1301
1315
  execution_id = parsed["id"]
1302
1316
  request, files = replay_request(root, parsed)
1317
+ branch = current_branch(root)
1318
+ if branch:
1319
+ request["branch"] = branch
1303
1320
  check_arguments(parsed["replay"])
1304
1321
  allowed = allowed_files(root, parsed)
1305
1322
  if "--cloud-preview" in parsed["switches"]:
@@ -1817,6 +1834,111 @@ def runner_request(root, commit):
1817
1834
  return dispatched_request()
1818
1835
 
1819
1836
 
1837
+ def check_out_snapshot_base(root, commit, branch):
1838
+ try:
1839
+ base = git(root, "rev-parse", "--verify", "-q", f"{commit}^")
1840
+ except CommandError:
1841
+ print(
1842
+ "This checkout does not include the commit the snapshot was taken on, so the experiment records no code change; set fetch-depth: 2 on the workflow's actions/checkout step, or run bitfab-replay --cloud-update",
1843
+ flush=True,
1844
+ )
1845
+ return None
1846
+ if isinstance(branch, str) and BRANCH_NAME.fullmatch(branch):
1847
+ try:
1848
+ git(root, "update-ref", f"refs/heads/{branch}", base)
1849
+ git(root, "symbolic-ref", "HEAD", f"refs/heads/{branch}")
1850
+ return base
1851
+ except CommandError:
1852
+ pass
1853
+ git(root, "reset", "-q", "--soft", base)
1854
+ return base
1855
+
1856
+
1857
+ def replay_name(args):
1858
+ for index, value in enumerate(args):
1859
+ if value.startswith("--name="):
1860
+ return value.partition("=")[2]
1861
+ if value == "--name" and index + 1 < len(args):
1862
+ return args[index + 1]
1863
+ return None
1864
+
1865
+
1866
+ def git_blob(root, ref, path):
1867
+ try:
1868
+ size = int(git(root, "cat-file", "-s", f"{ref}:{path}"))
1869
+ except (CommandError, ValueError):
1870
+ return None
1871
+ if size > CODE_CHANGE_MAX_FILE_BYTES:
1872
+ return None
1873
+ result = subprocess.run(
1874
+ ["git", "-C", str(root), "show", f"{ref}:{path}"],
1875
+ capture_output=True,
1876
+ check=False,
1877
+ )
1878
+ if result.returncode or b"\x00" in result.stdout[:8000]:
1879
+ return None
1880
+ return result.stdout.decode("utf-8", "replace").replace("\r\n", "\n")
1881
+
1882
+
1883
+ def changed_paths(root, base, commit):
1884
+ raw = git(
1885
+ root,
1886
+ "diff",
1887
+ "--name-status",
1888
+ "--find-renames",
1889
+ "-z",
1890
+ base,
1891
+ commit,
1892
+ "--",
1893
+ ":!.bitfab",
1894
+ )
1895
+ parts = [part for part in raw.split("\x00") if part]
1896
+ index = 0
1897
+ while index + 1 < len(parts):
1898
+ status, before_path = parts[index][0], parts[index + 1]
1899
+ index += 2
1900
+ path = before_path
1901
+ if status in "RC" and index < len(parts):
1902
+ path = parts[index]
1903
+ index += 1
1904
+ yield status, before_path, path
1905
+
1906
+
1907
+ def snapshot_code_change(root, base, commit, name):
1908
+ files, total = [], 0
1909
+ for status, before_path, path in changed_paths(root, base, commit):
1910
+ if len(files) >= CODE_CHANGE_MAX_FILES:
1911
+ break
1912
+ before = "" if status == "A" else git_blob(root, base, before_path)
1913
+ after = "" if status == "D" else git_blob(root, commit, path)
1914
+ if before is None or after is None or before == after:
1915
+ continue
1916
+ size = len(before.encode()) + len(after.encode())
1917
+ if total + size > CODE_CHANGE_MAX_TOTAL_BYTES:
1918
+ continue
1919
+ total += size
1920
+ files.append({"path": path, "before": before, "after": after})
1921
+ if not files:
1922
+ return None
1923
+ head = (name or "").strip() or git(root, "log", "-1", "--format=%s", base)
1924
+ word = "file" if len(files) == 1 else "files"
1925
+ return {
1926
+ "description": f"{head or 'Working-tree change'} ({len(files)} {word} changed uncommitted (vs HEAD))",
1927
+ "files": files,
1928
+ }
1929
+
1930
+
1931
+ def write_code_change(change):
1932
+ handle, path = tempfile.mkstemp(
1933
+ prefix="bitfab-code-change-",
1934
+ suffix=".json",
1935
+ dir=os.environ.get("RUNNER_TEMP") or None,
1936
+ )
1937
+ with os.fdopen(handle, "w") as out:
1938
+ json.dump(change, out)
1939
+ return path
1940
+
1941
+
1820
1942
  def execute():
1821
1943
  if os.environ.get("GITHUB_RUN_ATTEMPT") != "1":
1822
1944
  raise ValueError("Submit a new replay instead of rerunning an Actions job")
@@ -1825,6 +1947,11 @@ def execute():
1825
1947
  if git(root, "rev-parse", "HEAD") != commit:
1826
1948
  raise ValueError("Runner checkout does not match the dispatched commit")
1827
1949
  request = runner_request(root, commit)
1950
+ base = (
1951
+ check_out_snapshot_base(root, commit, request.get("branch"))
1952
+ if os.environ.get("GITHUB_REF_NAME", "").startswith(PREFIX)
1953
+ else None
1954
+ )
1828
1955
  directory = within(root, request["cwd"])
1829
1956
  check_secrets(root)
1830
1957
  if os.environ.get(API_KEY_ENV, "").strip():
@@ -1837,10 +1964,19 @@ def execute():
1837
1964
  workflow_environment_conflicts(root, environment)
1838
1965
  print(f"Setting {', '.join(sorted(environment))} from the request", flush=True)
1839
1966
  names = {option_name(value) for value in request["args"]}
1967
+ explicit_change = bool(names & set(CODE_CHANGE_FLAGS))
1968
+ change = (
1969
+ None
1970
+ if explicit_change or base is None
1971
+ else snapshot_code_change(root, base, commit, replay_name(request["args"]))
1972
+ )
1973
+ run_environment = dict(environment)
1974
+ if change:
1975
+ run_environment[CODE_CHANGE_PATH_ENV] = write_code_change(change)
1840
1976
  args = [
1841
1977
  *replay_command(),
1842
1978
  *request["args"],
1843
- *([] if names & set(CODE_CHANGE_FLAGS) else ["--no-code-change"]),
1979
+ *([] if explicit_change or change else ["--no-code-change"]),
1844
1980
  ]
1845
1981
  if dry_run(request["args"]):
1846
1982
  run_check_command(directory, environment)
@@ -1854,7 +1990,7 @@ def execute():
1854
1990
  request.get("timeoutMinutes"),
1855
1991
  experiment,
1856
1992
  summarize=request.get("fullOutput") is not True,
1857
- environment=environment,
1993
+ environment=run_environment,
1858
1994
  )
1859
1995
  finally:
1860
1996
  print(OUTPUT_END, flush=True)
@@ -2497,7 +2633,10 @@ def workflow_document(steps, run, directory, env, *, runs_on, environment):
2497
2633
  if environment:
2498
2634
  job["environment"] = environment
2499
2635
  job["steps"] = [
2500
- {"uses": ACTIONS["checkout"], "with": {"persist-credentials": False}},
2636
+ {
2637
+ "uses": ACTIONS["checkout"],
2638
+ "with": {"persist-credentials": False, "fetch-depth": CHECKOUT_DEPTH},
2639
+ },
2501
2640
  *steps,
2502
2641
  {
2503
2642
  **({} if directory == "." else {"working-directory": directory}),
@@ -2608,6 +2747,57 @@ def add_key_env(lines):
2608
2747
  return True
2609
2748
 
2610
2749
 
2750
+ def raise_checkout_depth(lines, start, end):
2751
+ for at in range(start, end):
2752
+ if not re.match(r'[ \t]*"?fetch-depth"?[ \t]*:', lines[at]):
2753
+ continue
2754
+ depth = re.match(
2755
+ r'([ \t]*"?fetch-depth"?[ \t]*:[ \t]*)["\']?(\d+)["\']?(.*)',
2756
+ lines[at],
2757
+ )
2758
+ if depth is None or int(depth[2]) == 0 or int(depth[2]) >= CHECKOUT_DEPTH:
2759
+ return False
2760
+ lines[at] = f"{depth[1]}{CHECKOUT_DEPTH}{depth[3]}"
2761
+ return True
2762
+ return None
2763
+
2764
+
2765
+ def add_checkout_depth(lines):
2766
+ for index, line in enumerate(lines):
2767
+ match = re.match(
2768
+ r'([ \t]*)(- +)?"?uses"?[ \t]*:[ \t]*"?actions/checkout@',
2769
+ without_yaml_comment(line),
2770
+ )
2771
+ if not match:
2772
+ continue
2773
+ key = len(match[1]) + len(match[2] or "")
2774
+ start = index
2775
+ while start > 0 and not re.match(r"[ \t]*- ", lines[start]):
2776
+ start -= 1
2777
+ end = block_end(lines, start, key - 1)
2778
+ raised = raise_checkout_depth(lines, start, end)
2779
+ if raised is not None:
2780
+ return "updated" if raised else "unchanged"
2781
+ entry = f"fetch-depth: {CHECKOUT_DEPTH}"
2782
+ for at in range(start, end):
2783
+ with_key = re.fullmatch(
2784
+ r"[ \t]*(- +)?with[ \t]*:(.*)", without_yaml_comment(lines[at])
2785
+ )
2786
+ if not with_key:
2787
+ continue
2788
+ if len(lines[at]) - len(lines[at].lstrip(" \t-")) != key:
2789
+ continue
2790
+ if with_key[2].strip():
2791
+ return "by hand"
2792
+ entries = block_end(lines, at, key)
2793
+ child = indentation(lines[at + 1]) if entries > at + 1 else key + 2
2794
+ lines.insert(entries, " " * child + entry)
2795
+ return "updated"
2796
+ lines[end:end] = [" " * key + "with:", " " * (key + 2) + entry]
2797
+ return "updated"
2798
+ return "unchanged"
2799
+
2800
+
2611
2801
  def update_workflow(root, path):
2612
2802
  target = within(root, path)
2613
2803
  text = target.read_text()
@@ -2633,6 +2823,15 @@ def update_workflow(root, path):
2633
2823
  by_hand.append(
2634
2824
  f'{API_KEY_ENV}: "{KEY_SECRET_REFERENCE}" in the replay step\'s env'
2635
2825
  )
2826
+ depth = add_checkout_depth(lines)
2827
+ if depth == "updated":
2828
+ updated.append(
2829
+ "Checked out the commit each snapshot is taken on, so experiments record the code change"
2830
+ )
2831
+ elif depth == "by hand":
2832
+ by_hand.append(
2833
+ f"fetch-depth: {CHECKOUT_DEPTH} in the actions/checkout step's with"
2834
+ )
2636
2835
  if updated:
2637
2836
  target.write_text("\n".join(lines))
2638
2837
  steps = []
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.64.17"
4
+ VERSION = "0.64.18"
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.64.17
4
+ version: 0.64.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team