mirror of
https://github.com/unslothai/unsloth.git
synced 2026-10-21 22:12:39 +00:00
* Stop three installer tests from inheriting and leaking uv resolver policy Six tests in tests/studio/install fail on main and have since the commits that introduced them. None of them indicts product code. test_setup_fast_path_guard.py assigned os.environ["UV_OFFLINE"] directly inside a 20-iteration loop with no teardown, so it left UV_OFFLINE=2 set process wide. That file sorts before test_woa_wheelhouse_guard_behaviour.py, whose _run passed no env= to pwsh, so Test-WoaResolveReachesPyPI saw UV_OFFLINE=2, took the early return at install.ps1:765 and answered False. That accounts for the failure set exactly, including why env9 (UV_OFFLINE=0) is the one True row that survives: it is the only one that overwrites the leaked value. The same leak was also silently skipping test_transformers_tokenizers_pair.py, which uv rejects outright with "invalid value 2: expected a boolish value". Fixed at both ends. The writer uses monkeypatch.setenv. The reader builds an explicit env with the uv and pip policy variables removed, matching the idiom test_woa_torch_index_persistence.py already uses, and runs under a fixture that asserts no uv.toml or [tool.uv] table sits anywhere above its working directory, since Get-WoaUvConfigIndexPolicy walks to the filesystem root. test_the_pr_only_added_new_arms_to_the_setup_status_blocks diffed the installers against a fixed 2026-09-09 SHA and asserted no removed line matched "step " or "-match". On a PR branch that diff is the PR; on main it is every installer change since, so it indicts whichever unrelated commit last touched such a line. It was never satisfiable on main and has been red since it merged, unnoticed because actions/checkout defaults to fetch-depth 1 and requires_merge_base skips it in CI. Restated as live invariants on the shipped text: exactly two keep arms per script, each guarded by its own shell's construct, the three pre-existing arms present verbatim. tests/studio/install: 6 failed, 6125 passed, 33 skipped oncf335fa47before; 6132 passed, 32 skipped after. Each assertion was checked against a real defect in both directions, so none of them passes merely because the environment is now scrubbed. * Repair the six CI checks that are red on main itself Every one of these reproduces on origin/main with no pull request involved, so they were failing on every open PR at once and reading like each PR's own fault. Grouped by root cause rather than by check name: three commits between #11118 and #10282 account for all six.b535eca2(#10282, Windows on ARM native CUDA) -- four failures tests/test_windows_amd_gpu_scan_fallback.py, two tests. install.ps1's handoff block grew four UNSLOTH_WOA_* saves and restores them from a TABLE walked with Set-Item "Env:$(...)", not from four written-out `$env:NAME = $previousX` arms. The installer is right; this file's reader knew only the written-out spelling, so it read four correct restores as absent. Worse than the noise: a table that really HAD gone missing would have been reported identically, so the check could not tell a correct block from a broken one. The reader now understands both spellings, and a table only counts when something walks it. The four names join _CALLER_ENV, so the runtime cases below actually exercise their restore instead of the file merely claiming to cover them -- dropping one row from the table, or restoring every row from row 0's save, now fails at runtime rather than passing. tests/studio/test_cache_budget_discipline.py. The guard flagged woa-wheelhouse's vcpkg cache as saving on a pull request ref. It cannot: that workflow is workflow_dispatch only, holds signing credentials and is deliberately unreachable from a pull request. The guard was reading `if:` without ever reading `on:`, so it applied a PR-ref rule to a workflow no PR can reach. It now reads the trigger block, the exemption is named with its reason rather than silently skipped, and a second test re-derives that claim from `on:` -- so the day woa-wheelhouse gains a pull_request trigger it comes straight back under the rule. An ungated cache save in a workflow a PR can reach still fails exactly as before. tests/studio/test_pwsh_calls_use_the_shared_runner.py. #10282 added four files under tests/studio/install that start PowerShell through subprocess directly, which opts them back into the shared $XDG_CACHE_HOME/powershell startup-cache race (~1 startup in 500 dies before it reaches the script). Routed through run_pwsh, which is the one-word change the runner was written for.9b1f9317(#11117, localize the prompt queue) -- two failures tests/studio/studiobench/.../test_studiobench_queued_idle_live.py. The selftest pinned the composer's label as the English literal `followUpBehavior === "queue" ? "Queue message" : "Steer response"`, and #11117 correctly replaced it with the two translation keys. A second assertion, on the queue surface's hardcoded English aria-label, was stale for the same reason and was hidden behind the first. Both now read the branch and the shipped en catalog, which is strictly stronger than the literals were: it also checks that dom.js's hardcoded `[aria-label^="Prompt queue,"]` selector still agrees with the catalog, which is the coupling a localization pass could have broken in silence. tests/studio/test_playwright_suites_run_in_ci.py. #11117 added playwright_queue_localization.py and wired it into nothing. It is a gate, not a measurement harness, so it is wired into the composer-compatibility engine loop beside the sibling suite that drives the same fixture -- not added to NOT_IN_CI. Verified green first on chromium 153, firefox 155 and webkit 26.6, so this adds coverage rather than a new red.cf335fa4(#11118) and9b1f9317(#11117) -- one failure tests/test_formatter_fixed_point.py named three files that are not a fixed point of the ruff-format-with-kwargs hook, which fails pre-commit.ci on the next PR to edit any of them. Formatted with the pinned ruff 0.6.9. None of this changes shipped behaviour. studio/backend/main.py is in the diff for formatting only, and install.ps1 is untouched -- it was correct throughout. Before / after, measured the same way both times: workflow guard suites 2 failed, 1961 passed, 44 skipped -> 0 failed, 1976 passed, 44 skipped studiobench selftests 1 failed, 1423 passed, 145 skipped -> 0 failed, 1424 passed, 145 skipped * Scope the cache-save guard by trigger, not by an excused filename Follow-up on review of the previous commit. The first version skipped woa-wheelhouse.yml by name, from a dict carrying the reason. That works and it rots: skipping a whole FILE blinds the guard to every other step in it, so a `setup-python` implicit cache added to that workflow later would have been invisible, and the excuse would keep applying after the reason for it had gone. It is the same shape as a PINNED_BY_DESIGN keyed on filename. The predicate was already there. Now it is the only thing doing the work: a workflow is scanned if and only if `_pull_request_reachable` says a pull request can run it, and there is no list. The day a workflow gains a `pull_request` or `pull_request_target` trigger, every cache save in it is indicted on that same commit with nobody having to remember an entry exists. Composite actions stay indicted unconditionally. An action has no triggers of its own, and one `uses:`d from a pull_request job saves on the PR's ref exactly as an inline step would. Mutation-proved, each applied then restored from a file copy: ungated actions/cache save injected into lint-ci.yml RED ... into consolidated-tests-ci.yml RED ... into studio-frontend-ci.yml RED woa-wheelhouse.yml given pull_request_target, save unchanged RED woa-wheelhouse.yml given pull_request, save unchanged RED woa-wheelhouse.yml given pull_request AND a second offender RED, naming BOTH The last is the one the filename exemption could not do. It reports woa-wheelhouse.yml:build-pyarrow: setup-python implicit post-step save woa-wheelhouse.yml:build-pyarrow: Cache vcpkg packages where the excused-filename version would have named neither. Workflow guard suites: 1975 passed, 44 skipped, 0 failed. * Let the formatter fixed-point guard start on Windows The guard passed every tracked file to one subprocess.run. That is 2656 paths and, under a Windows pytest tmp_path, 324,905 characters against CreateProcess's 32,767 -- 9.9x over -- so on Windows the call died with [WinError 206] The filename or extension is too long before ruff opened a single file. It has never been caught because every job that schedules this file is ubuntu-24.04, where execve's ARG_MAX is about 2 MB and the same argv fits easily. So this is latent, not a current red: nothing fails today, and the guard simply cannot be scheduled anywhere else. Batched, and safely: the hook is per-file, so N calls over disjoint batches do exactly what one call over the union does. A LENGTH budget, not a file count. A count needs re-tuning whenever the tracked set or the path depth moves and silently loses its margin in between -- the first version of this used 200 files, which measured 26,687 characters, only 1.2x under the cap, so a fifth more files would have put it straight back over. Accumulating by length is self-adjusting: 42 batches, widest 7,999 characters, 4.1x headroom, and no number to keep in step with the repo. The protection is a test, not a Windows runner. A permanent Windows leg would tax every future PR through an org queue that has been several hundred deep all day, to protect one guard. test_the_formatter_invocation_fits_in_a_windows_ command_line computes the real command lines from the real file list and asserts the widest stays under the documented Windows cap. It is pure Python, runs in the Linux job that already exists, and fails fast and everywhere. It drives formatter_argvs, the same builder the guard runs, rather than checking the budget constant -- a test that only read the constant would stay green while the call site did something else. Mutation-proved, each applied then restored from a file copy: builder reverted to one call with everything RED "the widest of the 1 ... builds a 324977-character command line" budget raised to 40000, past the cap RED, names the 39998 it built batching drops its final partial batch RED "batching lost or reordered files" cap raised until batching stops mattering RED "batching is no longer load-bearing and this test no longer proves anything" The last is the non-vacuity guard: if the tracked set ever shrinks enough that one call fits, this says so instead of passing on a check that measures nothing. tests/test_formatter_fixed_point.py: 11 passed, was 10. * Read the locale entry, not the line it starts on studio/frontend/tests/pasted-text-attachment.test.ts "every locale keeps the shortcut in the threshold description" fails on origin/main, which takes down "Frontend build + bundle sanity" and "Frontend unit tests (Windows)" on every PR whose merge picks up main. Reported by the lane that hit it on #11070; it is main's test rather than that PR's, so it belongs here. The test did source.indexOf("pastedTextThresholdDescription:"), sliced to the next newline, and asserted "{shortcut}" was on that slice. That asserts about the formatter as much as about the translation: a value Prettier wraps onto the FOLLOWING line leaves the slice holding only the key, and the placeholder is invisible while being right there. Product is correct. All twelve locales carry {shortcut} -- checked by parsing the value out of each file rather than by reading the assertion's own slice. It is also worse than one locale. ELEVEN of the twelve are wrapped that way on main today; only en.ts is not. The test was passing on readdir order alone, and fixing ar.ts by hand would have handed the same failure to de.ts, then es.ts, and so on down the directory. So it now imports the locale module and asserts on the resolved string, which is what the app reads and cannot be moved by layout. The key is found by walking the tree rather than by a fixed path, so reorganising the message tree does not silently drop the check either, and `values.length == 1` keeps it from passing vacuously if the key disappears. Checked the other indexOf-based source assertions in studio/frontend/tests while here: they bound their slices with an explicit end marker ("];", the next symbol name), not with the next newline, so none share this failure mode. Mutation-proved: a locale drops {shortcut} RED "ar.ts keeps {shortcut}" a locale loses the key entirely RED "ja.ts carries the description exactly once" a value wrapped onto the next line GREEN here, and the old line-scoped assertion on the same tree sees only 'pastedTextThresholdDescription:' and fails The third is the regression itself, and it needs no synthetic edit: eleven locales on main are already in that state. studio/frontend: 34 pass, 0 fail (was 33 pass, 1 fail). * Run the queue localization suite for the files it actually reads Wiring the driver into the composer job left it unreachable by the change it guards. The workflow filters on tests/studio/*composer* and tests/studio/*prompt_queue*, and playwright_queue_localization.py carries neither substring, so a pull request that edits only the driver never runs it. Nor does one that edits only a catalog: the driver calls setLocale through src/i18n/index.ts and asserts five hard-coded Japanese strings, and no pattern reached src/i18n at all. A translation-only edit to ja.ts is the exact change this suite exists to catch. tests/studio/test_playwright_suites_run_in_ci.py cannot see this by design; its own docstring says trigger paths say when CI runs, not what it runs. Add the two patterns. Checked against GitHub's filter semantics with the controls in both directions: the driver, each locale catalog and the i18n loader now trigger the job, thread.tsx and playwright_prompt_queue_actions.py still do, and README.md and unsloth/models/llama.py still do not. Also close a hole in _pull_request_reachable, whose docstring promises that anything unrecognised counts as reachable while the code fell through to exempt. merge_group runs on its own refs/heads/gh-readonly-queue ref, so a cache saved there is exactly the pull-request-scoped entry this rule forbids, and the guard would have said nothing. Name the three events that really are default-branch-only and indict the rest. No current workflow changes verdict. * Scrub the resolver policy variables case-folded The scrub list carries one mixed-case name, ProgramData, and it is the one that would survive a literal match on the only platform it matters on. CPython upper-cases every environment key on nt (os.py, _createenviron: encodekey does encode(key).upper(), under the comment Where Env Var Names Must Be UPPERCASE), so on Windows os.environ hands the comprehension PROGRAMDATA, it compares unequal to ProgramData, and the child pwsh keeps reading %PROGRAMDATA%\uv\uv.toml. That file is uv's system-level config and install.ps1 reads it: Get-WoaUvConfigIndexPolicy joins it at install.ps1:737, and a NoIndex or a non-PyPI DefaultIndex there flips Test-WoaResolveReachesPyPI to False. So a row asserting True becomes machine-dependent on exactly the boxes the installer ships to, which is the failure this scrub exists to remove. Fold the comparison, and state the requirement in a test that runs everywhere: a Linux session has no ProgramData to leak, so a scrub that only matches literally passes this suite green on CI and quietly stops working on Windows. The parametrisation feeds the three spellings an environment block can carry. * Correct the argv length this guard is sized from Three places state the number and two of them were stale: the constant's own comment says ~325,000 characters and 9.9x the cap, while the test docstring and the batching comment said 378,000 and 11.5x. Measured with the file's own Windows prefix over the real tracked list: 2656 files, 324,905 characters, 9.9x. The whole argument for the constant is quantitative, so a reader checking it should not land on a figure that is 16 percent out. * Stop the scripts that rewrite tracked files turning them CRLF on Windows (#11158) * Stop the scripts that rewrite tracked files turning them CRLF on Windows Found by the staging Windows run on #11145. With the argv fix in place the formatter guard finally STARTED on windows-latest, and then reported ~1300 of ~2650 tracked files as drifted. None of them had drifted. .gitattributes opens with `*.py text eol=lf`, so tracked Python here is LF on disk and meant to stay that way. Three scripts rewrite tracked files through the same temp-file-then-os.replace shape and all three opened the temp file in text mode with the DEFAULT newline, which translates every "\n" to os.linesep: scripts/enforce_kwargs_spacing.py every tracked .py the hook touches scripts/stamp_studio_release.py studio/backend/utils/_studio_release_build.py scripts/scan_packages.py studio/backend/requirements/*.txt, under --fix On Linux os.linesep is "\n" and the round trip is a no-op, which is why this survived. On Windows it rewrites the file to CRLF. For enforce_kwargs_spacing that is every file a contributor edits: the read side is tokenize.open, which normalises CRLF to LF in memory, so the write alone decides the ending. The symptom is a whole-file diff on files the contributor never touched, produced by running this project's own pre-commit hook. Contributor-facing, not test-only. newline = "\n" rather than newline = "". Both are correct and both land LF here, so this is a readability choice: "\n" states the ending .gitattributes mandates rather than "do not translate", and it stays right even if the read side stops normalising. It is also already the spelling used at unsloth_cli/commands/studio.py:788. scan_packages.py's write had no `encoding` either, so it took the locale codec and a non-ASCII comment round-tripped differently between runners. Fixed on both halves of that call. studio/backend has a guard for exactly the encoding case, studio/backend/tests/test_text_io_encoding.py, but it scans BACKEND_ROOT only, so scripts/ was never covered by it. Surveyed rather than taking the three reported hits on trust: every os.fdopen text-mode write in the repo. The rest are runtime temp files and reports whose on-disk ending is nobody's contract, so they are left alone -- the rule applied here is "rewrites a TRACKED file in place". tests/test_scripts_write_lf.py is two halves, and the split is the point: * the behavioural tests put a CRLF file in and assert LF bytes out, through the real write path. On Windows they exercise the translation for real. On LINUX THEY CANNOT FAIL if the fix is reverted, because os.linesep is already "\n". * test_every_in_place_rewriter_names_its_newline reads the call sites and fails on any platform the moment a `newline =` or `encoding =` goes missing. That is the half that protects the Linux job that actually runs, and it is the same principle as #11145's argv test driving formatter_argvs rather than the budget constant: assert the property where the platform cannot show it. Mutation-proved, all four RED ON LINUX, each restored from a file copy: newline reverted on enforce_kwargs_spacing RED, names file:line and the fix newline reverted on stamp_studio_release RED newline + encoding reverted on scan_packages RED .gitattributes stops pinning *.py to LF RED "this whole file needs re-reading" (the premise check) tests/test_scripts_write_lf.py 7 passed; with test_scan_packages.py and test_formatter_fixed_point.py, 154 passed. studio/backend's own encoding guard still 522 passed. * Stop a leaked torch import error deciding another test's verdict studio/backend/tests/test_torch_cpu_build_on_nvidia_host.py's nameless Intel row went red on one Backend CI shard and passed in every serial run. The cause is order, not the card. hw._has_torch() writes the module global TORCH_IMPORT_ERROR as a side effect, and _torch_reports_an_xpu_runtime, _torch_reports_a_hip_runtime and classify_torch_build all branch on it: set, they read the wheel off disk and never look at sys.modules, so the fake 2.9.0+xpu torch the test installs is invisible, xpu_expected comes back false and the card is dropped. Four tests watch an import fail and none of them put the global back, and the rest shard is the only one holding both files. Reproduced without xdist at all: run test_a_broken_torch_counts_as_no_torch and then the Intel row in one process and the failure is byte-identical to CI, same line. A leak detector over the serial shard names all four writers before this change and none after. Two independent defences. _shared_setup_1 pins TORCH_IMPORT_ERROR to None, so the test states the precondition it depends on rather than inheriting whatever ran before it in the worker; each of the four leaks is fixed at source with a snapshot restore, since nothing there owns the value. A new row proves the pin is load-bearing by setting the global to exactly what a sibling leaves and showing the verdict invert, with its on-disk arm pinned so it does not turn on what torch the host happens to have. Also widens this PR's own LF survey, which looked only at os.fdopen and so missed two writers with the identical default-newline defect. scripts/sync_allow_scripts_pins.py rewrites studio/frontend/package.json, which .gitattributes pins to eol=lf, and runs as a pre-commit hook with --fix, so a Windows contributor bumping a frontend dependency got a whole-file CRLF rewrite from a two-line change. scan_packages._write_baseline is the same shape. The guard now reads Path.write_text too, or a rewriter could drop out of it by swapping one call for the other. * Re-pin the three zoo entries the scan reopened pip scan-packages :: hf-stack went red on three HIGH findings, all in unsloth-zoo's own metaprogramming: compiler.py, saving_utils.py and mlx/loader.py under "Advanced obfuscation (marshal/compile/zlib) + exec/eval". Not this branch's doing -- zoo's main moved. Two of the three are file_sha256 pinned, so they reopened because the FILE changed, not because the matched lines did. That pin exists for danger sitting outside the matched lines, so re-pinning is a review of the file rather than of the evidence, and it was done as one: neither compiler.py nor mlx/loader.py contains any network primitive (urlopen, requests, raw sockets, an http URL that is not a licence header), any payload decoder (base64, marshal, pickle, zlib) or os.system at the current content. The scanner agrees -- neither file appears under any of the CRITICAL checks, which is where secrets-plus-network lives. compiler.py's lone subprocess import is never called. What is flagged remains what was flagged before: __import__ with a computed module name, chr(92) in the banner art, and mlx/loader.py's mx.eval, which evaluates an MLX array and is not Python's eval at all. saving_utils.py is unpinned and its evidence hash moved on its own terms: __import__("transformers", fromlist=[model_class_name]) and the exec of a "from transformers.modeling_utils import (...)" line. Appended rather than replaced, because duplicate keys union their pins: this widens the allowed digests for those two files by exactly one each and reopens nothing. Scan now exits 0 with 0 HIGH.
149 lines
5.6 KiB
Python
149 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Keep `allowScripts` pins in studio/frontend/package.json in sync with
|
|
package-lock.json.
|
|
|
|
`npm approve-scripts` writes version-pinned entries ("pkg@1.2.3": true).
|
|
A dependency bump strands the pin, so the approval (or denial) silently
|
|
stops matching and the package's install scripts fall back to
|
|
"unreviewed". This tool re-pins existing entries to the versions the
|
|
lockfile actually resolves; it never adds or removes entries, so
|
|
approving a brand-new script-bearing package stays a human decision.
|
|
|
|
Usage:
|
|
python scripts/sync_allow_scripts_pins.py --check # CI: exit 1 on drift
|
|
python scripts/sync_allow_scripts_pins.py --fix # rewrite package.json
|
|
|
|
Pinned keys follow npm's allowScripts grammar: "name@1.2.3" or
|
|
"name@1.2.3 || 1.2.4". Bare names (no version) match every version and
|
|
are left alone. Entries whose range is not an exact-version disjunction
|
|
(wildcards, tags) are left alone too.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
DEFAULT_DIR = REPO_ROOT / "studio" / "frontend"
|
|
|
|
EXACT_VERSION_RE = re.compile(r"^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]+)?$")
|
|
|
|
|
|
def split_spec(key: str) -> tuple[str, str | None]:
|
|
"""'@scope/name@1.2.3' -> ('@scope/name', '1.2.3'); bare names -> (key, None)."""
|
|
if key.startswith("@"):
|
|
rest = key[1:]
|
|
if "@" not in rest:
|
|
return key, None
|
|
name, rng = rest.split("@", 1)
|
|
return "@" + name, rng
|
|
if "@" not in key:
|
|
return key, None
|
|
name, rng = key.split("@", 1)
|
|
return name, rng
|
|
|
|
|
|
def is_exact_disjunction(rng: str) -> bool:
|
|
parts = [p.strip() for p in rng.split("||")]
|
|
return all(EXACT_VERSION_RE.match(p) for p in parts) and bool(parts)
|
|
|
|
|
|
def version_sort_key(version: str) -> tuple:
|
|
release = version.split("-", 1)[0].split("+", 1)[0]
|
|
return tuple(int(x) for x in release.split(".")), version
|
|
|
|
|
|
def script_versions_from_lock(lock: dict) -> dict[str, list[str]]:
|
|
"""Map package name -> sorted versions that carry install scripts."""
|
|
out: dict[str, set[str]] = {}
|
|
for path, meta in (lock.get("packages") or {}).items():
|
|
if not path or not meta.get("hasInstallScript"):
|
|
continue
|
|
name = path.rsplit("node_modules/", 1)[-1]
|
|
version = meta.get("version")
|
|
if name and version:
|
|
out.setdefault(name, set()).add(version)
|
|
return {n: sorted(vs, key = version_sort_key) for n, vs in out.items()}
|
|
|
|
|
|
def desired_key(name: str, versions: list[str]) -> str:
|
|
return f"{name}@{' || '.join(versions)}"
|
|
|
|
|
|
def compute_renames(policy: dict, lock_versions: dict[str, list[str]]) -> dict[str, str]:
|
|
renames: dict[str, str] = {}
|
|
for key in policy:
|
|
name, rng = split_spec(key)
|
|
if rng is None or not is_exact_disjunction(rng):
|
|
continue # bare name or non-exact spec: matches by name, never stale
|
|
versions = lock_versions.get(name)
|
|
if not versions:
|
|
continue # package gone or script-free now: stale pin is inert
|
|
want = desired_key(name, versions)
|
|
if key != want:
|
|
renames[key] = want
|
|
return renames
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
ap = argparse.ArgumentParser(description = __doc__)
|
|
mode = ap.add_mutually_exclusive_group(required = True)
|
|
mode.add_argument("--check", action = "store_true", help = "exit 1 if pins are stale")
|
|
mode.add_argument("--fix", action = "store_true", help = "rewrite package.json in place")
|
|
ap.add_argument(
|
|
"--dir",
|
|
type = Path,
|
|
default = DEFAULT_DIR,
|
|
help = "directory holding package.json + package-lock.json",
|
|
)
|
|
args = ap.parse_args(argv)
|
|
|
|
pkg_path = args.dir / "package.json"
|
|
lock_path = args.dir / "package-lock.json"
|
|
if not pkg_path.exists() or not lock_path.exists():
|
|
print(f"sync-allow-scripts: nothing to do ({args.dir} has no package.json + lockfile)")
|
|
return 0
|
|
|
|
pkg = json.loads(pkg_path.read_text(encoding = "utf-8"))
|
|
policy = pkg.get("allowScripts")
|
|
if not isinstance(policy, dict) or not policy:
|
|
print("sync-allow-scripts: no allowScripts policy in package.json, nothing to do")
|
|
return 0
|
|
|
|
lock = json.loads(lock_path.read_text(encoding = "utf-8"))
|
|
renames = compute_renames(policy, script_versions_from_lock(lock))
|
|
|
|
if not renames:
|
|
print(f"sync-allow-scripts: {len(policy)} allowScripts entries in sync with the lockfile")
|
|
return 0
|
|
|
|
for old, new in renames.items():
|
|
print(f' stale pin: "{old}" -> "{new}"')
|
|
|
|
if args.check:
|
|
print(
|
|
"sync-allow-scripts: pins are stale; run "
|
|
"`python scripts/sync_allow_scripts_pins.py --fix` and commit the result"
|
|
)
|
|
return 1
|
|
|
|
pkg["allowScripts"] = {renames.get(k, k): v for k, v in policy.items()}
|
|
# newline = "\n", not the default None, which translates every "\n" to os.linesep. This is a
|
|
# pre-commit hook running --fix on studio/frontend/package.json, and .gitattributes pins that
|
|
# file to LF (`studio/frontend/** text=auto eol=lf`), so on Windows the default turned a
|
|
# two-line pin bump into a whole-file CRLF rewrite. open() rather than write_text(newline =):
|
|
# write_text only grew `newline` in 3.10 and pyproject still says requires-python >= 3.9.
|
|
with open(pkg_path, "w", encoding = "utf-8", newline = "\n") as handle:
|
|
handle.write(json.dumps(pkg, indent = 2, ensure_ascii = False) + "\n")
|
|
print(
|
|
f"sync-allow-scripts: re-pinned {len(renames)} entr{'y' if len(renames) == 1 else 'ies'} in {pkg_path}"
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|