Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/actions/setup-node/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Set up Node with safe Windows ARM64 extraction
description: Keep the pinned installer while avoiding the hosted ARM64 PowerShell 7 ZIP crash.
inputs:
node-version:
description: Exact Node version to install.
required: true
cache:
description: Package manager cache passed to setup-node.
cache-dependency-path:
description: Dependency lockfile paths passed to setup-node.
package-manager-cache:
description: Enable setup-node's automatic package manager cache.
default: 'true'
runs:
using: composite
steps:
- name: Select the Windows ARM64 archive extractor
if: runner.os == 'Windows' && runner.arch == 'ARM64'
id: extractor
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
# Node-spawned pwsh can abort before tool-cache's ZIP fallback runs:
# https://github.com/actions/runner-images/issues/15107
# Hide every pwsh directory only from this setup-node invocation. The
# existing tool-cache fallback then uses Windows PowerShell instead.
Get-Command powershell.exe -CommandType Application -ErrorAction Stop | Out-Null
$path = (($env:PATH -split ';') | Where-Object {
$directory = $_.Trim().Trim('"')
$directory -and -not (Test-Path -LiteralPath (Join-Path $directory 'pwsh.exe'))
}) -join ';'
$overrides = @{ PATH = $path }
$json = ConvertTo-Json -InputObject $overrides -Compress
"env=$json" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- name: Set up the pinned Node runtime
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
env: ${{ fromJSON(steps.extractor.outputs.env || '{}') }}
with:
node-version: ${{ inputs.node-version }}
cache: ${{ inputs.cache }}
cache-dependency-path: ${{ inputs.cache-dependency-path }}
package-manager-cache: ${{ inputs.package-manager-cache }}
9 changes: 9 additions & 0 deletions .github/maturin-curl/.curlrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Used only by the Python SDK's manylinux containers, via explicit CURL_HOME.
# Do not pass HTTP error pages to the action's curl | tar installer.
fail
retry = 4
retry-delay = 2
retry-max-time = 120
retry-connrefused
connect-timeout = 30
max-time = 180
32 changes: 18 additions & 14 deletions .github/workflows/node-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ on:
push:
branches: [main, development]
tags: ['npm-v*']
paths: ['javascript/**', 'crates/**', 'Cargo.toml', 'Cargo.lock', 'LICENSE', 'NOTICE', 'THIRD_PARTY_NOTICES', 'third-party/kingfisher-vectorscan/**', 'scripts/third-party-notices.cjs', 'scripts/tests/third-party-notices.test.cjs', 'scripts/license-overrides/**', 'Makefile', '.gitattributes', 'docs/JAVASCRIPT.md', 'scripts/tests/test_node_sdk*.py', '.github/actions/vectorscan-prebuilt/**', 'scripts/fetch-vectorscan.py', 'scripts/tests/test_fetch_vectorscan.py', '.github/workflows/node-sdk.yml']
paths: ['javascript/**', 'crates/**', 'Cargo.toml', 'Cargo.lock', 'LICENSE', 'NOTICE', 'THIRD_PARTY_NOTICES', 'third-party/kingfisher-vectorscan/**', 'scripts/third-party-notices.cjs', 'scripts/tests/third-party-notices.test.cjs', 'scripts/license-overrides/**', 'Makefile', '.gitattributes', 'docs/JAVASCRIPT.md', 'scripts/tests/test_node_sdk*.py', '.github/actions/setup-node/**', '.github/actions/vectorscan-prebuilt/**', 'scripts/fetch-vectorscan.py', 'scripts/tests/test_fetch_vectorscan.py', '.github/workflows/node-sdk.yml']
pull_request:
paths: ['javascript/**', 'crates/**', 'Cargo.toml', 'Cargo.lock', 'LICENSE', 'NOTICE', 'THIRD_PARTY_NOTICES', 'third-party/kingfisher-vectorscan/**', 'scripts/third-party-notices.cjs', 'scripts/tests/third-party-notices.test.cjs', 'scripts/license-overrides/**', 'Makefile', '.gitattributes', 'docs/JAVASCRIPT.md', 'scripts/tests/test_node_sdk*.py', '.github/actions/vectorscan-prebuilt/**', 'scripts/fetch-vectorscan.py', 'scripts/tests/test_fetch_vectorscan.py', '.github/workflows/node-sdk.yml']
paths: ['javascript/**', 'crates/**', 'Cargo.toml', 'Cargo.lock', 'LICENSE', 'NOTICE', 'THIRD_PARTY_NOTICES', 'third-party/kingfisher-vectorscan/**', 'scripts/third-party-notices.cjs', 'scripts/tests/third-party-notices.test.cjs', 'scripts/license-overrides/**', 'Makefile', '.gitattributes', 'docs/JAVASCRIPT.md', 'scripts/tests/test_node_sdk*.py', '.github/actions/setup-node/**', '.github/actions/vectorscan-prebuilt/**', 'scripts/fetch-vectorscan.py', 'scripts/tests/test_fetch_vectorscan.py', '.github/workflows/node-sdk.yml']
workflow_dispatch:

permissions:
Expand Down Expand Up @@ -83,7 +83,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false }
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with:
node-version: '22.23.4'
cache: npm
Expand Down Expand Up @@ -122,6 +122,10 @@ jobs:
- uses: ./.github/actions/vectorscan-prebuilt
with:
target: ${{ matrix.target }}
- name: Check the Windows Node archive selector
if: runner.os == 'Windows'
working-directory: .
run: python -X utf8 -m unittest discover -s scripts/tests -p test_node_sdk_release.py -v
- name: Rust formatting and binding lint
run: |
cargo fmt --all --check
Expand All @@ -142,31 +146,31 @@ jobs:
run: |
npm test
npm run test:types
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with: { node-version: '24.22.2' }
- name: Node 24 tests against the same native artifact
run: |
npm test
npm run test:types
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with: { node-version: '22.13.0' }
- name: Minimum Node 22 runtime against the same native artifact
run: |
npm test
npm run test:types
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with: { node-version: '24.0.0' }
- name: Minimum Node 24 runtime against the same native artifact
run: |
npm test
npm run test:types
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with: { node-version: '26.0.0' }
- name: Minimum Node 26 runtime against the same native artifact
run: |
npm test
npm run test:types
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with: { node-version: '26.13.0' }
- name: Node 26 integration and TypeScript API against the same native artifact
run: |
Expand Down Expand Up @@ -230,27 +234,27 @@ jobs:
with:
name: node-sdk-registry-staging
path: javascript/release
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with: { node-version: '22.23.4' }
- name: Clean offline Node 22 consumer of the exact publish archives
run: npm run test:package
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with: { node-version: '24.22.2' }
- name: Clean offline Node 24 consumer of the same archives
run: npm run test:package
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with: { node-version: '22.13.0' }
- name: Clean offline minimum Node 22 consumer of the same archives
run: npm run test:package
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with: { node-version: '24.0.0' }
- name: Clean offline minimum Node 24 consumer of the same archives
run: npm run test:package
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with: { node-version: '26.0.0' }
- name: Clean offline minimum Node 26 consumer of the same archives
run: npm run test:package
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
- uses: ./.github/actions/setup-node
with: { node-version: '26.13.0' }
- name: Clean offline Node 26 consumer of the same archives
run: npm run test:package
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/python-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,14 @@ jobs:
# Published Vectorscan archives require glibc 2.35. Build from the
# vendored source inside manylinux to keep the advertised 2.28 floor.
before-script-linux: yum install -y boost-devel ragel perl-core
docker-options: -e VECTORSCAN_BUILD_FROM_SOURCE=1 -e CARGO_PROFILE_RELEASE_PANIC=unwind -e CARGO_PROFILE_RELEASE_LTO=false
# The action downloads Maturin before before-script-linux runs. Its
# curl | tar installer needs HTTP-error handling and bounded retries;
# the checkout is already mounted at github.workspace in the container.
docker-options: >-
-e VECTORSCAN_BUILD_FROM_SOURCE=1
-e CARGO_PROFILE_RELEASE_PANIC=unwind
-e CARGO_PROFILE_RELEASE_LTO=false
-e CURL_HOME="${{ github.workspace }}/.github/maturin-curl"
# manylinux builds Vectorscan from source to preserve the glibc 2.28 floor.
- uses: ./.github/actions/vectorscan-prebuilt
if: runner.os != 'Linux'
Expand Down
74 changes: 74 additions & 0 deletions scripts/tests/test_node_sdk_release.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"""Check native SDK release guards without GitHub or npm registry writes."""
from pathlib import Path
import json
import os
import re
import shutil
import subprocess
import tempfile
import textwrap
import tomllib
import unittest

Expand Down Expand Up @@ -114,6 +120,74 @@ def test_release_native_runtime_baselines_are_explicit_and_audited(self):
self.assertIn("npm test", minimum)
self.assertIn("npm run test:types", minimum)

def test_all_native_and_sealed_runtime_installs_use_the_safe_wrapper(self):
workflow = (ROOT / ".github/workflows/node-sdk.yml").read_text()
for name in ("build", "verify"):
with self.subTest(job=name):
body = job(workflow, name)
self.assertEqual(body.count("uses: ./.github/actions/setup-node"), 6)
self.assertNotIn("uses: actions/setup-node@", body)
self.assertEqual(re.findall(r"node-version: '([^']+)'", body), [
"22.23.4", "24.22.2", "22.13.0", "24.0.0", "26.0.0", "26.13.0",
])
for name in ("source", "package", "publish"):
self.assertIn("actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38",
job(workflow, name))
self.assertIn("cache: npm", job(workflow, "build"))
self.assertIn("cache-dependency-path: javascript/package-lock.json", job(workflow, "build"))
self.assertIn("if: runner.os == 'Windows'\n working-directory: .\n"
" run: python -X utf8 -m unittest discover -s scripts/tests "
"-p test_node_sdk_release.py -v", job(workflow, "build"))
triggers = workflow.split("permissions:", 1)[0]
self.assertEqual(triggers.count("'.github/actions/setup-node/**'"), 2)

def test_archive_workaround_is_scoped_to_each_arm64_windows_installer(self):
action = (ROOT / ".github/actions/setup-node/action.yml").read_text()
selector, installer = action.split(" - name: Set up the pinned Node runtime\n", 1)
self.assertIn("if: runner.os == 'Windows' && runner.arch == 'ARM64'", selector)
self.assertIn("shell: powershell", selector)
self.assertIn("Get-Command powershell.exe -CommandType Application -ErrorAction Stop", selector)
self.assertIn("$_.Trim().Trim", selector)
self.assertIn("Test-Path -LiteralPath (Join-Path $directory 'pwsh.exe')", selector)
self.assertNotIn("GITHUB_ENV", action)
self.assertNotIn("GITHUB_PATH", action)
self.assertNotIn("continue-on-error", action)
self.assertIn("uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38", installer)
self.assertIn("env: ${{ fromJSON(steps.extractor.outputs.env || '{}') }}", installer)
for name in ("node-version", "cache", "cache-dependency-path", "package-manager-cache"):
self.assertIn(f"{name}: ${{{{ inputs.{name} }}}}", installer)
self.assertIn("default: 'true'", selector)

@unittest.skipUnless(os.name == "nt", "The archive selector uses Windows PowerShell")
def test_windows_selector_preserves_compiler_and_node_paths_and_hides_all_pwsh(self):
powershell = shutil.which("powershell")
self.assertIsNotNone(powershell, "Windows PowerShell is required by tool-cache's fallback")
action = (ROOT / ".github/actions/setup-node/action.yml").read_text()
script = textwrap.dedent(action.split(" run: |\n", 1)[1].split(" - name:", 1)[0])
with tempfile.TemporaryDirectory(prefix="kingfisher Node extractor ") as temporary:
root = Path(temporary)
compiler, node = root / "VS 14.44", root / "Node 22"
pwsh_directories = [root / "PowerShell 7", root / "Other PowerShell"]
for directory in (compiler, node, *pwsh_directories):
directory.mkdir()
for directory in pwsh_directories:
(directory / "pwsh.exe").write_bytes(b"fixture, never executed")
retained = [str(Path(powershell).parent), str(compiler), str(node)]
hidden = [str(pwsh_directories[0]) + "\\", str(pwsh_directories[0]).upper(),
'"' + str(pwsh_directories[1]) + '\\"']
for index, entries in enumerate((retained, retained + hidden)):
with self.subTest(pwsh_present=bool(index)):
output = root / f"output-{index}.txt"
result = subprocess.run([
powershell, "-NoLogo", "-NoProfile", "-NonInteractive",
"-ExecutionPolicy", "Bypass", "-Command", script,
], env={**os.environ, "PATH": ";".join(entries), "GITHUB_OUTPUT": str(output)},
capture_output=True, timeout=30, check=False)
self.assertEqual(result.returncode, 0, result.stderr.decode(errors="replace"))
line = output.read_text(encoding="utf-8-sig").strip()
self.assertTrue(line.startswith("env="))
self.assertEqual(json.loads(line.removeprefix("env=")), {"PATH": ";".join(retained)})

def test_fuzz_lockfile_tracks_workspace_crate_versions(self):
lock = tomllib.loads((ROOT / "fuzz/Cargo.lock").read_text())
for crate in ("core", "rules", "scanner"):
Expand Down
Loading
Loading