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 +4 -4
- data/lib/bitfab/cloudReplay.py +202 -3
- data/lib/bitfab/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b213b2d758ad1fbcafc95b9f3704d6fa06c5b288be373dfd9c97565c699eaa4
|
|
4
|
+
data.tar.gz: 67f58161475956d24a8bac5592e080b42892b277fc0fd132e9a1b679254fddbc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54b089f5d3a87132403ec8bff156d0a2bcb596480956c14b9bdb8f158c877589a33a30a8ee1eafaed363ff1ac97b61638997abed639aeb487f16eaba6fd90c81
|
|
7
|
+
data.tar.gz: c34da0a8b44cb56dad4d788188940fb11197b8377155ff614fb7dd930141dde8911f4bdbdb83480c0a1a4cca73748e2b296459430c3641002a8e1a4a8fe9b6c8
|
data/lib/bitfab/cloudReplay.py
CHANGED
|
@@ -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
|
|
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=
|
|
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
|
-
{
|
|
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 = []
|
data/lib/bitfab/version.rb
CHANGED