Files
rea/.github/workflows/ci.yml
T
f504c2a351 fix(javascript): decline source-map UNC references with an unusable host (#1372)
* fix(javascript): decline source-map UNC references with an unusable host

A source map can name a Windows share whose host a file URL cannot carry,
such as `\\bad host\share\a.js`. On the Node builds `engines` admits,
`pathToFileURL(path, { windows: true })` fails a native assertion and calls
`abort()` on Node 22.19-22.22, 24.11-24.15 and 26.0, so the process dies
with exit 134 instead of declining the reference. Node 22.23 and 24.16 raise
`ERR_INVALID_URL` instead, and older builds silently drop the host, so the
same reference resolves three different ways depending on the runtime.

Validate the UNC host before handing the path to Node. A host carrying a
forbidden host code point, a percent escape or nothing at all now resolves as
`unresolved` on every admitted build, and `npm test` passes the existing
`\\bad host\share\a.js` row everywhere. Well-formed UNC hosts and
drive-letter paths are untouched.

* fix(javascript): validate UNC hosts through IDNA before conversion

Reject Unicode source-map UNC hosts that URL/IDNA processing cannot use or normalizes to an empty hostname before calling Windows pathToFileURL. Keep the original source identity and preserve usable normalized hosts.

Exercise the built reference resolver in owned subprocesses, including U+00AD and U+200C as source names and source roots. The original PR implementation aborts under Node 22.22.2; the new regression isolates those signals from the test runner. Add this test to the existing Node 22 Windows CI step.

Validation: 64 focused tests pass on Node 22.22.2 and 24.18.0; npm run check and test discovery pass. Local verification ran on macOS; Windows verification remains with CI.

---------

Co-authored-by: Yi-111-a <yi-111-a@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-10 06:38:47 +08:00

499 lines
19 KiB
YAML

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main, "release/*"]
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
# The latest main push verifies the full tree, including earlier changes.
# Publication uses its own release workflow and concurrency group.
cancel-in-progress: true
env:
HUSKY: "0"
jobs:
pr-title:
name: Conventional PR title
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Validate title for Release Please
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: >-
node -e 'const title = process.env.PR_TITLE ?? "";
const conventional = /^(?:feat|fix|docs|refactor|perf|test|build|ci|chore|revert)(?:\([a-z0-9._/-]+\))?!?: .+/u;
if (!conventional.test(title)) {
console.error("PR title must use Conventional Commits, for example: feat: add historical source import");
process.exit(1);
}'
changes:
name: Classify changes
runs-on: ubuntu-latest
outputs:
code: ${{ steps.package.outputs.required }}
package: ${{ steps.package.outputs.required }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Select package and Windows lanes
id: package
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [[ "${GITHUB_EVENT_NAME}" != "pull_request" ]]; then
echo "required=true" >> "${GITHUB_OUTPUT}"
# Compare the PR's changes, excluding unrelated updates on its base.
elif git diff --quiet "${BASE_SHA}...${HEAD_SHA}" -- \
. \
':(exclude)docs/**' \
':(top,exclude,glob)README*.md' \
':(exclude)AGENTS.md' \
':(exclude)CONTRIBUTING.md'; then
echo "required=false" >> "${GITHUB_OUTPUT}"
else
diff_status=$?
# A failed comparison must fail classification, not report a scope.
if [[ "${diff_status}" -ne 1 ]]; then
exit "${diff_status}"
fi
echo "required=true" >> "${GITHUB_OUTPUT}"
fi
static:
name: Static checks
needs: changes
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Require successful change classification
env:
CHANGES_RESULT: ${{ needs.changes.result }}
run: test "${CHANGES_RESULT}" = "success"
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
run: npm run check:ci
- if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
run: npm run verify:test-discovery
- if: github.event_name == 'pull_request' && needs.changes.outputs.code != 'true'
run: npm run format:check
test-shard:
name: Test shard (${{ matrix.shard }})
needs: [changes, build]
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: rea-dist
path: dist
- name: Generate test artifacts from the downloaded runtime
run: |
node scripts/generate-mcp-tool-catalog.mjs
node scripts/generate-skill-metadata.mjs
node scripts/generate-product-catalog.mjs
node scripts/generate-completion-ledger.mjs
- run: npm run test:ci:shard:run -- --shard=${{ matrix.shard }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: vitest-report-${{ strategy.job-index }}
path: .vitest/blob/*
include-hidden-files: true
if-no-files-found: error
overwrite: true
retention-days: 7
build:
name: Build package
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run build:cached
- uses: actions/upload-artifact@v4
with:
name: rea-dist
path: dist
if-no-files-found: error
retention-days: 7
test:
name: Test (coverage + timing)
needs: [changes, test-shard]
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Require successful test shards
env:
CHANGES_RESULT: ${{ needs.changes.result }}
CODE_REQUIRED: ${{ needs.changes.outputs.code }}
SHARD_RESULT: ${{ needs.test-shard.result }}
run: |
test "${CHANGES_RESULT}" = "success"
test "${CODE_REQUIRED}" != "true" || test "${SHARD_RESULT}" = "success"
- uses: actions/checkout@v4
if: needs.changes.outputs.code == 'true' && success()
- uses: actions/setup-node@v4
if: needs.changes.outputs.code == 'true' && success()
with:
node-version-file: .nvmrc
cache: npm
- if: needs.changes.outputs.code == 'true' && success()
run: npm ci
- uses: actions/download-artifact@v4
if: needs.changes.outputs.code == 'true' && success()
with:
path: .vitest/blob
pattern: vitest-report-*
merge-multiple: true
- name: Merge coverage, JUnit, and timing reports
if: needs.changes.outputs.code == 'true' && success()
run: >-
npm run test:ci:merge --
--coverage.reportsDirectory=coverage
--coverage.reporter=text
--coverage.reporter=json-summary
--coverage.reporter=lcov
--reporter=default
--reporter=junit
--reporter=json
--outputFile.junit=test-results.xml
--outputFile.json=test-results.json
- name: Summarize slowest test files
if: needs.changes.outputs.code == 'true' && success()
shell: bash
run: |
node <<'NODE' >> "${GITHUB_STEP_SUMMARY}"
const report = JSON.parse(require("node:fs").readFileSync("test-results.json", "utf8"));
const files = (report.testResults ?? [])
.map((result) => ({
name: result.name,
duration: Math.max(0, (result.endTime ?? result.startTime) - result.startTime),
}))
.sort((left, right) => right.duration - left.duration)
.slice(0, 20);
console.log("## Slowest deterministic test files\n");
console.log("| File | Duration (ms) |");
console.log("| --- | ---: |");
for (const file of files) {
console.log(`| \`${file.name}\` | ${file.duration} |`);
}
NODE
- uses: actions/upload-artifact@v4
if: needs.changes.outputs.code == 'true' && always()
with:
name: test-results
path: |
test-results.xml
test-results.json
coverage/
if-no-files-found: error
retention-days: 30
conformance-fixtures:
name: Source-built conformance fixtures
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Verify fixtures from a clean checkout
run: npm run verify:fixtures
windows-native-build:
name: Build bundled Windows x64 native controls
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
run: |
# Slow Azure archive transfers can exhaust the job before compilation.
# Keep the same signed packages and avoid slow HTTP mirror fallback.
for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources; do
if [ -f "$source" ]; then
sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g; s|http://archive.ubuntu.com|https://archive.ubuntu.com|g' "$source"
fi
done
sudo apt-get update
sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
- run: npm run build:windows-native
- run: npm run verify:windows-native:artifact
- uses: actions/upload-artifact@v4
with:
name: windows-native-controls
path: native/windows/build/
if-no-files-found: error
package-e2e:
name: Native package baseline (${{ matrix.platform }} ${{ matrix.architecture }})
needs: [changes, windows-native-build]
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
strategy:
fail-fast: false
max-parallel: 2
matrix:
include:
- os: ubuntu-latest
platform: linux
architecture: x64
- os: ubuntu-24.04-arm
platform: linux
architecture: arm64
- os: macos-15
platform: darwin
architecture: arm64
- os: macos-15-intel
platform: darwin
architecture: x64
runs-on: ${{ matrix.os }}
timeout-minutes: 20
env:
NODE_OPTIONS: "--max-old-space-size=1024"
UV_THREADPOOL_SIZE: "2"
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- name: Verify native runtime identity
env:
EXPECTED_CI_PLATFORM: ${{ matrix.platform }}
EXPECTED_CI_ARCHITECTURE: ${{ matrix.architecture }}
run: >-
node -e "const assert = require('node:assert/strict');
assert.equal(process.platform, process.env.EXPECTED_CI_PLATFORM);
assert.equal(process.arch, process.env.EXPECTED_CI_ARCHITECTURE);
console.log(JSON.stringify({ platform: process.platform, architecture: process.arch, node: process.version }));"
- run: npm ci
- uses: actions/download-artifact@v4
with:
name: windows-native-controls
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- run: npm run verify:package
- name: Verify real Node Inspector discovery and observation
timeout-minutes: 10
run: npm run verify:inspector
apple-artifacts:
name: Real Apple artifact CLI/MCP E2E
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: macos-15
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run verify:keyed-archive
- run: npm run verify:asset-catalog
- run: npm run verify:macos-bundle
- name: Legacy simulator platform verification
run: npm run verify:legacy-simulator
- name: Native installer and compiled NIB hierarchy regressions
run: npm run test:focused -- tests/boundary/filesystem/installScript.test.ts tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/boundary/filesystem/interfaceBuilderBundle.test.ts
- name: Real binary metadata and Interface Builder integration
run: npm run verify:apple-dispatch && npm run verify:interface-builder
- name: Native plist CLI selection regressions
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
- name: Native Swift demangling CLI and MCP regressions
run: npm run test:focused -- tests/boundary/cli/nativeDemangleCli.test.ts tests/boundary/mcp/nativeDemangleMcp.test.ts
- name: Native signature target binding and CLI/MCP recovery regressions
run: npm run test:focused -- tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts tests/boundary/cli/nativeSignatureCli.test.ts tests/boundary/mcp/nativeSignatureTargetBindingMcp.test.ts
- name: Process filesystem snapshot descriptor regressions
run: npm run test:focused -- src/process/capture/FilesystemSnapshot.test.ts
- name: Native Darwin ownership and process cleanup regressions
run: npm run test:focused -- tests/boundary/process/darwinProcessRunTokenReader.test.ts tests/boundary/process/directAnalysisSignals.test.ts tests/boundary/process/processCaptureLifecycle.test.ts tests/boundary/process/processCli.test.ts tests/boundary/mcp/sessionMcp.test.ts
reconstruction-readiness:
name: Reconstruction readiness
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- name: Run the public CLI/MCP reconstruction journey
env:
REA_READINESS_REPORT_PATH: readiness-report.json
run: |
set -o pipefail
npm run verify:readiness | tee readiness-summary.json
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: reconstruction-readiness
path: |
readiness-summary.json
readiness-report.json
if-no-files-found: error
retention-days: 30
windows-curated:
name: Windows capability and package lane
needs: [changes, windows-native-build]
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
runs-on: windows-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run build:cached
- run: npm run typecheck
- uses: actions/download-artifact@v4
with:
name: windows-native-controls
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- name: Record named Windows host capabilities
run: node scripts/windows-capabilities.mjs | Out-File -Encoding utf8NoBOM windows-capabilities.json
- name: Validate Windows host capability report
run: >-
node -e "const report = JSON.parse(require('node:fs').readFileSync('windows-capabilities.json', 'utf8'));
if (report.platform !== 'win32' || report.architecture !== 'x64')
throw new Error('expected a Windows x64 capability report');"
# Keep this list aligned with tests/process-global/curatedWindowsLane.test.ts,
# which fails when any path below is renamed or removed. Vitest silently
# ignores unmatched paths, so a stale entry would otherwise report green
# while skipping coverage.
#
# The matching native artifact establishes real Job Object and private
# runtime boundaries for these fixtures. Ghidra and Java are not needed;
# real engine claims remain in the separate controlled-provider workflow.
- name: Run curated Windows capability tests
run: >-
npx vitest run --coverage.enabled=false
tests/boundary/filesystem/binaryTarget.test.ts
tests/boundary/filesystem/safeOutputTree.test.ts
tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
tests/boundary/providers/native/artifactProvider.extractionCompleteness.test.ts
tests/boundary/filesystem/setupUnavailableProvider.test.ts
tests/boundary/filesystem/windowsNpxRegistration.test.ts
tests/boundary/filesystem/webScriptManifestPaths.test.ts
tests/boundary/process/ProcessEvidence.host.test.ts
tests/acceptance/webModuleCli.test.ts
tests/acceptance/webSourceLocationCli.test.ts
tests/boundary/cli/referenceSourceImportStatus.test.ts
tests/boundary/filesystem/asarInventoryFreshness.test.ts
tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
src/ghidra/GhidraInstallation.test.ts
tests/boundary/providers/ghidra/ghidraTransport.test.ts
tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts
tests/boundary/providers/ghidra/ghidraLauncher.test.ts
tests/boundary/providers/hopper/hopperClient.cleanupRetry.test.ts
src/windows/WindowsNativeLoader.test.ts
src/windows/WindowsOwnedProcess.test.ts
src/ghidra/GhidraLauncherEnvironment.test.ts
src/process/ProcessOwnership.test.ts
src/process/ProcessOwnership.part2.test.ts
src/process/ProcessOwnership.part3.test.ts
src/process/ProcessOwnership.validation.test.ts
src/application/Update.test.ts
tests/process-global/updateMaintenance.test.ts
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
- run: npm run verify:windows-native
- name: Verify real Node Inspector discovery and observation
timeout-minutes: 10
env:
NODE_OPTIONS: "--max-old-space-size=1024"
UV_THREADPOOL_SIZE: "2"
run: npm run verify:inspector
- run: npm run verify:package:windows
- name: Verify native directory identity on Node 24
run: >-
npx vitest run --coverage.enabled=false
tests/boundary/filesystem/directoryArtifactReaderOpen.test.ts
tests/boundary/filesystem/javascriptPackagePrecedence.test.ts
tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
- uses: actions/setup-node@v4
with:
node-version: 22.19.0
- name: Verify native path boundaries on Node 22
run: >-
npx vitest run --coverage.enabled=false
tests/boundary/filesystem/directoryArtifactReaderOpen.test.ts
tests/boundary/filesystem/javascriptPackagePrecedence.test.ts
tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
tests/boundary/process/sourceMapUncHosts.test.ts
- uses: actions/upload-artifact@v4
if: always()
with:
name: windows-capabilities
path: windows-capabilities.json
retention-days: 30
docs:
name: Docs Generation
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run docs:check
- name: Generation leaves authored files unchanged
run: git diff --exit-code
- uses: actions/upload-artifact@v4
with:
name: generated-docs
path: |
docs/public/product-catalog.json
docs/verification/managed-conformance-*.json
skills/reverse-engineer-anything
if-no-files-found: error