bitfab 0.64.8 → 0.64.9
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 +121 -12
- package/dist/{chunk-25HVPGSF.js → chunk-7J3XIMC3.js} +3 -3
- package/dist/{chunk-YGDXDDTX.js → chunk-CPRLBQ2W.js} +2 -2
- package/dist/{chunk-YGDXDDTX.js.map → chunk-CPRLBQ2W.js.map} +1 -1
- package/dist/{chunk-2UDEQITO.js → chunk-EJVBVUH4.js} +6 -6
- package/dist/{chunk-2U4Y5FHT.js → chunk-L3LFRGPX.js} +2 -2
- package/dist/{chunk-P2SYAT44.js → chunk-NFMXJT4W.js} +2 -2
- package/dist/{chunk-P2SYAT44.js.map → chunk-NFMXJT4W.js.map} +1 -1
- package/dist/{http-JBQTF36K.js → http-47AVLP6D.js} +2 -2
- package/dist/{http-CWPHM5T3.js → http-UOFHA334.js} +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/node.cjs +1 -1
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +3 -3
- package/dist/{replay-LBALXLOM.js → replay-WLGXE5GZ.js} +3 -3
- package/dist/replayCli.js +2 -2
- package/dist/seedCli.js +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-25HVPGSF.js.map → chunk-7J3XIMC3.js.map} +0 -0
- /package/dist/{chunk-2UDEQITO.js.map → chunk-EJVBVUH4.js.map} +0 -0
- /package/dist/{chunk-2U4Y5FHT.js.map → chunk-L3LFRGPX.js.map} +0 -0
- /package/dist/{http-CWPHM5T3.js.map → http-47AVLP6D.js.map} +0 -0
- /package/dist/{http-JBQTF36K.js.map → http-UOFHA334.js.map} +0 -0
- /package/dist/{replay-LBALXLOM.js.map → replay-WLGXE5GZ.js.map} +0 -0
package/cloudReplay.py
CHANGED
|
@@ -35,7 +35,8 @@ SDK_LANGUAGE_ENV = "BITFAB_SDK_LANGUAGE"
|
|
|
35
35
|
CHECK_COMMAND_ENV = "BITFAB_REPLAY_CHECK"
|
|
36
36
|
EXECUTION_TARGET_ENV = "BITFAB_REPLAY_EXECUTION_TARGET"
|
|
37
37
|
HOSTED_EXECUTION_TARGET = "hosted"
|
|
38
|
-
REQUEST_VERSION =
|
|
38
|
+
REQUEST_VERSION = 4
|
|
39
|
+
PLAIN_REQUEST_VERSION = 3
|
|
39
40
|
OLDEST_DISPATCHED_VERSION = 2
|
|
40
41
|
RESULT_LINE = "bitfab-replay-result "
|
|
41
42
|
RESULT_CHUNK = 4000
|
|
@@ -92,6 +93,13 @@ PUSH_TRIGGER = re.compile(
|
|
|
92
93
|
)
|
|
93
94
|
DISPATCH_INPUTS = ("execution_id", "request")
|
|
94
95
|
BRANCH_NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9._/-]*")
|
|
96
|
+
ENV_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
97
|
+
RESERVED_ENV_PREFIXES = ("GITHUB_", "RUNNER_", "ACTIONS_", "BITFAB_REPLAY_")
|
|
98
|
+
SECRET_ENV_NAME = re.compile(
|
|
99
|
+
r"(?:^|_)(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIALS?)(?:_|$)", re.IGNORECASE
|
|
100
|
+
)
|
|
101
|
+
ENV_FLAG = "--cloud-env"
|
|
102
|
+
URL_PASSWORD = re.compile(r"[A-Za-z][A-Za-z0-9+.-]*://[^/\s:@]*:[^/\s@]+@")
|
|
95
103
|
FOLLOW_FLAGS = (
|
|
96
104
|
"--cloud-status",
|
|
97
105
|
"--cloud-watch",
|
|
@@ -152,6 +160,11 @@ Options added by --cloud:
|
|
|
152
160
|
instead of the summary. Large runs can exceed what the
|
|
153
161
|
GitHub job log holds.
|
|
154
162
|
--cloud-timeout MIN Stop the replay after MIN minutes (1..7200).
|
|
163
|
+
--cloud-env NAME=VALUE Set an environment variable for this replay only; repeat it
|
|
164
|
+
for more. NAME alone copies the value from your shell. The
|
|
165
|
+
value is stored in the snapshot commit on GitHub, so this is
|
|
166
|
+
for settings, never secrets: names that look like secrets,
|
|
167
|
+
and names the workflow reads from a secret, are refused.
|
|
155
168
|
--cloud-request-id ID Recover a submission whose response was lost.
|
|
156
169
|
--cloud-ref BRANCH Replay BRANCH as it is on GitHub, with no snapshot of your
|
|
157
170
|
working tree, and record the experiment on BRANCH. Meant for
|
|
@@ -163,12 +176,13 @@ A workflow can also dispatch bitfab-replay.yml itself on any branch other than a
|
|
|
163
176
|
bitfab-replay/ snapshot branch, passing the replay as JSON in the request input:
|
|
164
177
|
|
|
165
178
|
{"args": ["--registry", "scripts/replay.ts", "classify", "--dataset-ids", "UUID"],
|
|
166
|
-
"cwd": "web", "timeoutMinutes": 60}
|
|
179
|
+
"cwd": "web", "timeoutMinutes": 60, "env": {"LOG_LEVEL": "debug"}}
|
|
167
180
|
|
|
168
181
|
args are the replay options without --cloud. cwd is the directory to replay from,
|
|
169
182
|
relative to the repository root, and defaults to the root. timeoutMinutes and
|
|
170
|
-
fullOutput (true) match --cloud-timeout and --cloud-full-output
|
|
171
|
-
|
|
183
|
+
fullOutput (true) match --cloud-timeout and --cloud-full-output, and env holds the
|
|
184
|
+
values --cloud-env sets. version and id are optional: version names the request
|
|
185
|
+
format (4 when env is set, otherwise 3; 2 is still accepted), and id is the
|
|
172
186
|
execution UUID, which must match an execution_id input when the workflow has one.
|
|
173
187
|
|
|
174
188
|
The runner's job exits with the replay's exit code, so the Actions run fails when
|
|
@@ -595,7 +609,17 @@ def split_arguments(argv):
|
|
|
595
609
|
name = option_name(token)
|
|
596
610
|
if name in RENAMED_FLAGS:
|
|
597
611
|
raise ValueError(f"{name} is gone; use {RENAMED_FLAGS[name]}")
|
|
598
|
-
if name
|
|
612
|
+
if name == ENV_FLAG:
|
|
613
|
+
if "=" in token:
|
|
614
|
+
value = token.split("=", 1)[1]
|
|
615
|
+
index += 1
|
|
616
|
+
elif index + 1 < len(argv):
|
|
617
|
+
value = argv[index + 1]
|
|
618
|
+
index += 2
|
|
619
|
+
else:
|
|
620
|
+
raise ValueError(f"{ENV_FLAG} needs NAME=VALUE or NAME")
|
|
621
|
+
cloud.setdefault(ENV_FLAG, []).append(value)
|
|
622
|
+
elif name in CLOUD_VALUE_FLAGS:
|
|
599
623
|
if "=" in token:
|
|
600
624
|
value = token.split("=", 1)[1]
|
|
601
625
|
index += 1
|
|
@@ -650,6 +674,8 @@ def parse(argv):
|
|
|
650
674
|
raise ValueError(
|
|
651
675
|
f"--cloud-ref takes a branch name such as dev, other than a {PREFIX} snapshot branch"
|
|
652
676
|
)
|
|
677
|
+
if ENV_FLAG in cloud:
|
|
678
|
+
cloud[ENV_FLAG] = replay_environment(cloud[ENV_FLAG])
|
|
653
679
|
operation = "submit"
|
|
654
680
|
execution_id = cloud.get("--cloud-request-id") or str(uuid.uuid4())
|
|
655
681
|
if not UUID.fullmatch(execution_id):
|
|
@@ -675,6 +701,62 @@ def validate_arguments(args):
|
|
|
675
701
|
raise ValueError("Replay options exceed 16000 characters")
|
|
676
702
|
|
|
677
703
|
|
|
704
|
+
def replay_environment(assignments):
|
|
705
|
+
values = {}
|
|
706
|
+
for assignment in assignments:
|
|
707
|
+
name, separator, value = assignment.partition("=")
|
|
708
|
+
if not separator:
|
|
709
|
+
if name not in os.environ:
|
|
710
|
+
raise ValueError(
|
|
711
|
+
f"{ENV_FLAG} {name} copies {name} from your shell, and it is not set there; pass {ENV_FLAG} {name}=VALUE instead"
|
|
712
|
+
)
|
|
713
|
+
value = os.environ[name]
|
|
714
|
+
if name in values:
|
|
715
|
+
raise ValueError(f"{ENV_FLAG} sets {name} twice")
|
|
716
|
+
values[name] = value
|
|
717
|
+
validate_environment(values)
|
|
718
|
+
return values
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def validate_environment(values):
|
|
722
|
+
if not isinstance(values, dict) or not all(
|
|
723
|
+
isinstance(name, str) and isinstance(value, str)
|
|
724
|
+
for name, value in values.items()
|
|
725
|
+
):
|
|
726
|
+
raise ValueError("Replay environment must map names to string values")
|
|
727
|
+
for name, value in values.items():
|
|
728
|
+
if not ENV_NAME.fullmatch(name):
|
|
729
|
+
raise ValueError(
|
|
730
|
+
f"{name!r} is not an environment variable name; use letters, digits, and underscores"
|
|
731
|
+
)
|
|
732
|
+
if name.upper().startswith(RESERVED_ENV_PREFIXES) or name.upper() == "CI":
|
|
733
|
+
raise ValueError(
|
|
734
|
+
f"{name} belongs to GitHub Actions or the cloud runner and cannot be set per replay"
|
|
735
|
+
)
|
|
736
|
+
if SECRET_ENV_NAME.search(name):
|
|
737
|
+
raise ValueError(
|
|
738
|
+
f"{name} looks like a secret, and {ENV_FLAG} values are stored in the snapshot commit on GitHub. Map it in the workflow's replay step env from a GitHub secret instead, then set it with bitfab-replay --cloud-secrets"
|
|
739
|
+
)
|
|
740
|
+
if "\x00" in value or "\n" in value or "\r" in value:
|
|
741
|
+
raise ValueError(f"{name} must be a single-line value")
|
|
742
|
+
if URL_PASSWORD.search(value):
|
|
743
|
+
raise ValueError(
|
|
744
|
+
f"{name} holds a URL with a password, and {ENV_FLAG} values are stored in the snapshot commit on GitHub. Map it in the workflow's replay step env from a GitHub secret instead, then set it with bitfab-replay --cloud-secrets"
|
|
745
|
+
)
|
|
746
|
+
if sum(len(name) + len(value) for name, value in values.items()) > 16000:
|
|
747
|
+
raise ValueError("Replay environment exceeds 16000 characters")
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def workflow_environment_conflicts(root, values):
|
|
751
|
+
path = running_workflow(root)
|
|
752
|
+
mapped = workflow_secrets(path.read_text()) if path.is_file() else {}
|
|
753
|
+
conflicts = sorted(name for name in values if name in mapped)
|
|
754
|
+
if conflicts:
|
|
755
|
+
raise ValueError(
|
|
756
|
+
f"The workflow already reads {', '.join(conflicts)} from a GitHub secret, so a replay cannot set it; update the secret with bitfab-replay --cloud-secrets"
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
|
|
678
760
|
def dry_run(args):
|
|
679
761
|
return "--dry-run" in args
|
|
680
762
|
|
|
@@ -705,7 +787,9 @@ def replay_request(root, parsed):
|
|
|
705
787
|
index += 1
|
|
706
788
|
validate_arguments(args)
|
|
707
789
|
request = {
|
|
708
|
-
"version": REQUEST_VERSION
|
|
790
|
+
"version": REQUEST_VERSION
|
|
791
|
+
if parsed["cloud"].get(ENV_FLAG)
|
|
792
|
+
else PLAIN_REQUEST_VERSION,
|
|
709
793
|
"id": parsed["id"],
|
|
710
794
|
"cwd": working_directory(root),
|
|
711
795
|
"args": args,
|
|
@@ -714,6 +798,8 @@ def replay_request(root, parsed):
|
|
|
714
798
|
request["timeoutMinutes"] = int(parsed["cloud"]["--cloud-timeout"])
|
|
715
799
|
if "--cloud-full-output" in parsed["switches"]:
|
|
716
800
|
request["fullOutput"] = True
|
|
801
|
+
if parsed["cloud"].get(ENV_FLAG):
|
|
802
|
+
request["env"] = parsed["cloud"][ENV_FLAG]
|
|
717
803
|
return request, files
|
|
718
804
|
|
|
719
805
|
|
|
@@ -1372,7 +1458,10 @@ def snapshot_request(root, commit):
|
|
|
1372
1458
|
request = json.loads(body)
|
|
1373
1459
|
except ValueError:
|
|
1374
1460
|
request = None
|
|
1375
|
-
if not isinstance(request, dict) or request.get("version")
|
|
1461
|
+
if not isinstance(request, dict) or request.get("version") not in (
|
|
1462
|
+
PLAIN_REQUEST_VERSION,
|
|
1463
|
+
REQUEST_VERSION,
|
|
1464
|
+
):
|
|
1376
1465
|
version = request.get("version", 0) if isinstance(request, dict) else 0
|
|
1377
1466
|
newer = isinstance(version, int) and version > REQUEST_VERSION
|
|
1378
1467
|
raise ValueError(
|
|
@@ -1385,6 +1474,8 @@ def snapshot_request(root, commit):
|
|
|
1385
1474
|
"The runner replays only the snapshot branch its request names; start replays with bitfab-replay --cloud"
|
|
1386
1475
|
)
|
|
1387
1476
|
validate_arguments(request["args"])
|
|
1477
|
+
if "env" in request:
|
|
1478
|
+
validate_environment(request["env"])
|
|
1388
1479
|
return request
|
|
1389
1480
|
|
|
1390
1481
|
|
|
@@ -1451,6 +1542,9 @@ def dispatched_request():
|
|
|
1451
1542
|
normalized["timeoutMinutes"] = timeout
|
|
1452
1543
|
if request.get("fullOutput") is True:
|
|
1453
1544
|
normalized["fullOutput"] = True
|
|
1545
|
+
if request.get("env"):
|
|
1546
|
+
validate_environment(request["env"])
|
|
1547
|
+
normalized["env"] = request["env"]
|
|
1454
1548
|
return normalized
|
|
1455
1549
|
|
|
1456
1550
|
|
|
@@ -1470,6 +1564,10 @@ def execute():
|
|
|
1470
1564
|
request = runner_request(root, commit)
|
|
1471
1565
|
directory = within(root, request["cwd"])
|
|
1472
1566
|
check_secrets(root)
|
|
1567
|
+
environment = request.get("env") or {}
|
|
1568
|
+
if environment:
|
|
1569
|
+
workflow_environment_conflicts(root, environment)
|
|
1570
|
+
print(f"Setting {', '.join(sorted(environment))} from the request", flush=True)
|
|
1473
1571
|
names = {option_name(value) for value in request["args"]}
|
|
1474
1572
|
args = [
|
|
1475
1573
|
*replay_command(),
|
|
@@ -1477,7 +1575,7 @@ def execute():
|
|
|
1477
1575
|
*([] if names & set(CODE_CHANGE_FLAGS) else ["--no-code-change"]),
|
|
1478
1576
|
]
|
|
1479
1577
|
if dry_run(request["args"]):
|
|
1480
|
-
run_check_command(directory)
|
|
1578
|
+
run_check_command(directory, environment)
|
|
1481
1579
|
experiment = {}
|
|
1482
1580
|
previous = signal.signal(signal.SIGTERM, raise_interrupt)
|
|
1483
1581
|
print(OUTPUT_BEGIN, flush=True)
|
|
@@ -1488,6 +1586,7 @@ def execute():
|
|
|
1488
1586
|
request.get("timeoutMinutes"),
|
|
1489
1587
|
experiment,
|
|
1490
1588
|
summarize=request.get("fullOutput") is not True,
|
|
1589
|
+
environment=environment,
|
|
1491
1590
|
)
|
|
1492
1591
|
finally:
|
|
1493
1592
|
print(OUTPUT_END, flush=True)
|
|
@@ -1537,13 +1636,17 @@ def check_secrets(root):
|
|
|
1537
1636
|
)
|
|
1538
1637
|
|
|
1539
1638
|
|
|
1540
|
-
def run_check_command(directory):
|
|
1639
|
+
def run_check_command(directory, environment=None):
|
|
1541
1640
|
check = os.environ.get(CHECK_COMMAND_ENV, "").strip()
|
|
1542
1641
|
if not check:
|
|
1543
1642
|
return
|
|
1544
1643
|
print(f"Running {CHECK_COMMAND_ENV}: {check}", flush=True)
|
|
1545
1644
|
code = subprocess.run(
|
|
1546
|
-
shlex.split(check),
|
|
1645
|
+
shlex.split(check),
|
|
1646
|
+
cwd=directory,
|
|
1647
|
+
env={**os.environ, **(environment or {})},
|
|
1648
|
+
stdin=subprocess.DEVNULL,
|
|
1649
|
+
check=False,
|
|
1547
1650
|
).returncode
|
|
1548
1651
|
if code:
|
|
1549
1652
|
raise ValueError(f"{CHECK_COMMAND_ENV} exited {code}; its output is above")
|
|
@@ -1772,13 +1875,19 @@ def summary_markdown(result):
|
|
|
1772
1875
|
return text
|
|
1773
1876
|
|
|
1774
1877
|
|
|
1775
|
-
def run_command(
|
|
1878
|
+
def run_command(
|
|
1879
|
+
directory, args, timeout, experiment, *, summarize=True, environment=None
|
|
1880
|
+
):
|
|
1776
1881
|
stopped = None
|
|
1777
1882
|
with tempfile.TemporaryFile() as output:
|
|
1778
1883
|
with subprocess.Popen(
|
|
1779
1884
|
args,
|
|
1780
1885
|
cwd=directory,
|
|
1781
|
-
env={
|
|
1886
|
+
env={
|
|
1887
|
+
**os.environ,
|
|
1888
|
+
**(environment or {}),
|
|
1889
|
+
EXECUTION_TARGET_ENV: HOSTED_EXECUTION_TARGET,
|
|
1890
|
+
},
|
|
1782
1891
|
stdout=output,
|
|
1783
1892
|
stderr=subprocess.PIPE,
|
|
1784
1893
|
stdin=subprocess.DEVNULL,
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
BitfabError,
|
|
3
3
|
warnOnce,
|
|
4
4
|
writeOnce
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-NFMXJT4W.js";
|
|
6
6
|
|
|
7
7
|
// src/codeChange.ts
|
|
8
8
|
var NUL = String.fromCharCode(0);
|
|
@@ -348,7 +348,7 @@ async function seedFromRegistry(registry, pipeline, cases, options = {}) {
|
|
|
348
348
|
sessionId: seedCase.sessionId
|
|
349
349
|
})
|
|
350
350
|
);
|
|
351
|
-
const { flushTraces: flushTraces2 } = await import("./http-
|
|
351
|
+
const { flushTraces: flushTraces2 } = await import("./http-UOFHA334.js");
|
|
352
352
|
await flushTraces2(3e4);
|
|
353
353
|
return { pipeline, traceFunctionKey, traceIds };
|
|
354
354
|
}
|
|
@@ -1042,4 +1042,4 @@ export {
|
|
|
1042
1042
|
runRegistryMain,
|
|
1043
1043
|
runIfEntrypoint
|
|
1044
1044
|
};
|
|
1045
|
-
//# sourceMappingURL=chunk-
|
|
1045
|
+
//# sourceMappingURL=chunk-7J3XIMC3.js.map
|
|
@@ -189,7 +189,7 @@ function encodeRequestBody(body) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
// src/version.generated.ts
|
|
192
|
-
var __version__ = "0.64.
|
|
192
|
+
var __version__ = "0.64.9";
|
|
193
193
|
var __packageName__ = "bitfab";
|
|
194
194
|
|
|
195
195
|
// src/constants.ts
|
|
@@ -2949,4 +2949,4 @@ export {
|
|
|
2949
2949
|
HttpClient,
|
|
2950
2950
|
experimentIdWireFields
|
|
2951
2951
|
};
|
|
2952
|
-
//# sourceMappingURL=chunk-
|
|
2952
|
+
//# sourceMappingURL=chunk-CPRLBQ2W.js.map
|