Skip to content

Commit 51c85ed

Browse files
committed
test(ci): pin the npm install command, uncredentialed checkout and esbuild gate order of integrations.yml
1 parent 8738d54 commit 51c85ed

1 file changed

Lines changed: 261 additions & 0 deletions

File tree

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
# SPDX-License-Identifier: MPL-2.0
5+
# Copyright (c) 2026 Den Rozhnovskiy
6+
7+
"""Configuration contract of the npm jobs in ``.github/workflows/integrations.yml``.
8+
9+
CI installs the Node extensions under two separate contracts, and one does not
10+
replace the other:
11+
12+
* configuration, pinned here statically: both npm jobs install with exactly
13+
``npm ci --ignore-scripts --include=optional``, check out without persisted
14+
credentials, the VS Code job runs the esbuild gate between the install and
15+
the build, and the token stays read-only. The YAML is parsed; nothing runs.
16+
* environment: ``extensions/vscode-codeclone/test/verifyToolchain.js`` proves on
17+
the runner that the locked esbuild is actually installed and works. It runs
18+
in CI (or in the container sandbox), never from this module.
19+
20+
``--ignore-scripts`` stops npm lifecycle scripts during the install, and that
21+
is all it does: the explicit ``npm run check`` and ``npm test`` steps still run
22+
dependency code. Nothing here claims those steps are safe.
23+
"""
24+
25+
from __future__ import annotations
26+
27+
import re
28+
import shlex
29+
from collections.abc import Mapping
30+
from pathlib import Path
31+
32+
import pytest
33+
import yaml
34+
35+
_REPO_ROOT = Path(__file__).resolve().parents[1]
36+
_WORKFLOW = _REPO_ROOT / ".github" / "workflows" / "integrations.yml"
37+
38+
_VSCODE_JOB = "vscode-extension"
39+
_NPM_JOBS = (_VSCODE_JOB, "claude-desktop-bundle")
40+
_PINNED_INSTALL = "npm ci --ignore-scripts --include=optional"
41+
_TOOLCHAIN_GATE = "node test/verifyToolchain.js linux-x64"
42+
_TOOLCHAIN_GATE_SCRIPT = Path(shlex.split(_TOOLCHAIN_GATE)[1])
43+
_VSCODE_BUILD = "npm run check"
44+
_CHECKOUT_ACTION = "actions/checkout@"
45+
_READ_ONLY_TOKEN = {"contents": "read"}
46+
47+
# npm subcommands that install packages or rerun their lifecycle scripts, with
48+
# every alias npm accepts for them. Over-matching only fails loudly.
49+
_NPM_INSTALL_SUBCOMMANDS = frozenset(
50+
{
51+
"ci",
52+
"clean-install",
53+
"ic",
54+
"install-clean",
55+
"isntall-clean",
56+
"install",
57+
"add",
58+
"i",
59+
"in",
60+
"ins",
61+
"inst",
62+
"insta",
63+
"instal",
64+
"isnt",
65+
"isnta",
66+
"isntal",
67+
"isntall",
68+
"install-test",
69+
"it",
70+
"install-ci-test",
71+
"cit",
72+
"clean-install-test",
73+
"sit",
74+
"rebuild",
75+
"rb",
76+
}
77+
)
78+
_SHELL_SEPARATORS = frozenset({"&&", "||", ";", "|", "&"})
79+
_ENV_ASSIGNMENT = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
80+
81+
_Node = Mapping[object, object]
82+
_Command = tuple[str, ...]
83+
84+
85+
def _mapping(value: object, what: str) -> _Node:
86+
assert isinstance(value, dict), f"{what} is not a mapping: {value!r}"
87+
return value
88+
89+
90+
def _workflow() -> _Node:
91+
document: object = yaml.safe_load(_WORKFLOW.read_text(encoding="utf-8"))
92+
return _mapping(document, str(_WORKFLOW.relative_to(_REPO_ROOT)))
93+
94+
95+
def _jobs(workflow: _Node) -> _Node:
96+
return _mapping(workflow.get("jobs"), "jobs")
97+
98+
99+
def _steps(jobs: _Node, name: object) -> list[_Node]:
100+
job = _mapping(jobs.get(name), f"job {name!r}")
101+
steps = job.get("steps")
102+
assert isinstance(steps, list) and steps, f"job {name!r} declares no steps"
103+
return [_mapping(step, f"a step of job {name!r}") for step in steps]
104+
105+
106+
def _commands(step: _Node) -> list[_Command]:
107+
"""Every simple shell command a ``run`` step executes, in order."""
108+
script = step.get("run")
109+
if not isinstance(script, str):
110+
return []
111+
commands: list[_Command] = []
112+
for line in script.splitlines():
113+
lexer = shlex.shlex(line, posix=True, punctuation_chars=True)
114+
lexer.whitespace_split = True
115+
current: list[str] = []
116+
for token in lexer:
117+
if token in _SHELL_SEPARATORS:
118+
if current:
119+
commands.append(tuple(current))
120+
current = []
121+
else:
122+
current.append(token)
123+
if current:
124+
commands.append(tuple(current))
125+
return commands
126+
127+
128+
def _program(command: _Command) -> _Command:
129+
"""The command without its leading ``NAME=value`` environment assignments."""
130+
index = 0
131+
while index < len(command) and _ENV_ASSIGNMENT.match(command[index]):
132+
index += 1
133+
return command[index:]
134+
135+
136+
def _is_npm_install(command: _Command) -> bool:
137+
program = _program(command)
138+
return (
139+
bool(program)
140+
and program[0] == "npm"
141+
and any(token in _NPM_INSTALL_SUBCOMMANDS for token in program[1:])
142+
)
143+
144+
145+
def _rendered(command: _Command) -> str:
146+
return " ".join(command)
147+
148+
149+
def _kind(command: _Command) -> str:
150+
if _is_npm_install(command):
151+
return "install"
152+
if _rendered(command) == _TOOLCHAIN_GATE:
153+
return "gate"
154+
return "code"
155+
156+
157+
def _install_commands(jobs: _Node, name: object) -> list[str]:
158+
return [
159+
_rendered(command)
160+
for step in _steps(jobs, name)
161+
for command in _commands(step)
162+
if _is_npm_install(command)
163+
]
164+
165+
166+
def _trigger_events(workflow: _Node) -> set[str]:
167+
# PyYAML follows YAML 1.1, where a bare ``on`` key loads as boolean True.
168+
triggers = workflow.get("on", workflow.get(True))
169+
return {str(event) for event in _mapping(triggers, "the workflow triggers")}
170+
171+
172+
@pytest.mark.parametrize("job_name", _NPM_JOBS)
173+
def test_npm_job_installs_without_lifecycle_scripts_keeping_optional_deps(
174+
job_name: str,
175+
) -> None:
176+
assert _install_commands(_jobs(_workflow()), job_name) == [_PINNED_INSTALL]
177+
178+
179+
def test_every_npm_install_in_the_workflow_is_the_pinned_command() -> None:
180+
jobs = _jobs(_workflow())
181+
unpinned = sorted(
182+
f"{name}: {command}"
183+
for name in jobs
184+
for command in _install_commands(jobs, name)
185+
if command != _PINNED_INSTALL
186+
)
187+
188+
assert unpinned == []
189+
190+
191+
@pytest.mark.parametrize("job_name", _NPM_JOBS)
192+
def test_npm_job_checkout_does_not_persist_credentials(job_name: str) -> None:
193+
checkouts = [
194+
step
195+
for step in _steps(_jobs(_workflow()), job_name)
196+
if str(step.get("uses", "")).startswith(_CHECKOUT_ACTION)
197+
]
198+
assert checkouts, f"job {job_name!r} has no {_CHECKOUT_ACTION} step"
199+
200+
persisted = [
201+
_mapping(step.get("with", {}), "checkout inputs").get("persist-credentials")
202+
for step in checkouts
203+
]
204+
assert persisted == [False] * len(checkouts)
205+
206+
207+
def test_vscode_job_runs_the_esbuild_gate_after_the_install_and_before_the_build() -> (
208+
None
209+
):
210+
sequence = [
211+
(_kind(command), _rendered(command))
212+
for step in _steps(_jobs(_workflow()), _VSCODE_JOB)
213+
for command in _commands(step)
214+
]
215+
kinds = [label for label, _command in sequence]
216+
assert "install" in kinds, sequence
217+
218+
after_install = sequence[kinds.index("install") :]
219+
assert [label for label, _command in after_install[:2]] == ["install", "gate"], (
220+
sequence
221+
)
222+
assert _VSCODE_BUILD in [command for _label, command in after_install[2:]], sequence
223+
224+
225+
def test_vscode_esbuild_gate_runs_the_tracked_script_from_its_directory() -> None:
226+
jobs = _jobs(_workflow())
227+
job = _mapping(jobs.get(_VSCODE_JOB), f"job {_VSCODE_JOB!r}")
228+
defaults = _mapping(job.get("defaults", {}), "defaults")
229+
default_directory = _mapping(defaults.get("run", {}), "defaults.run").get(
230+
"working-directory", "."
231+
)
232+
directories = [
233+
str(step.get("working-directory", default_directory))
234+
for step in _steps(jobs, _VSCODE_JOB)
235+
if _TOOLCHAIN_GATE in map(_rendered, _commands(step))
236+
]
237+
assert directories, f"job {_VSCODE_JOB!r} never runs {_TOOLCHAIN_GATE!r}"
238+
239+
missing = [
240+
directory
241+
for directory in directories
242+
if not (_REPO_ROOT / directory / _TOOLCHAIN_GATE_SCRIPT).is_file()
243+
]
244+
assert missing == []
245+
246+
247+
def test_workflow_token_is_read_only() -> None:
248+
workflow = _workflow()
249+
jobs = {
250+
str(name): _mapping(spec, f"job {name!r}")
251+
for name, spec in _jobs(workflow).items()
252+
}
253+
job_overrides = {
254+
name: job["permissions"] for name, job in jobs.items() if "permissions" in job
255+
}
256+
257+
assert (workflow.get("permissions"), job_overrides) == (_READ_ONLY_TOKEN, {})
258+
259+
260+
def test_workflow_never_runs_on_pull_request_target() -> None:
261+
assert "pull_request_target" not in _trigger_events(_workflow())

0 commit comments

Comments
 (0)