mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
c8e66c704a
SafeOutputTree.create() called fchmod(0o700) on a directory handle and
commit() fsynced a directory handle. Both are POSIX-only: Windows rejects
them with EPERM ("operation not permitted, fchmod"/"fsync"), so every
extract_artifact call failed with a generic io error and produced no output.
Skip the redundant directory chmod and directory fsync on Windows. mkdir
already applies the requested mode, and file contents are synced in write().
Add a Windows regression test and run it, plus the archive-safety test, in
the curated Windows CI lane so the behavior is covered.
410 lines
14 KiB
YAML
410 lines
14 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
|
|
concurrency:
|
|
group: ci-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
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}"
|
|
elif git diff --quiet "${BASE_SHA}" "${HEAD_SHA}" -- \
|
|
. \
|
|
':(exclude)docs/**' \
|
|
':(exclude)AGENTS.md' \
|
|
':(exclude)CONTRIBUTING.md'; then
|
|
echo "required=false" >> "${GITHUB_OUTPUT}"
|
|
else
|
|
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 == '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
|
|
- 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: 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: Package/install integration (${{ matrix.os }})
|
|
needs: [changes, windows-native-build]
|
|
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-14]
|
|
runs-on: ${{ matrix.os }}
|
|
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: windows-native-controls
|
|
path: native/windows/build/
|
|
- run: npm run verify:windows-native:artifact
|
|
- run: npm run verify:package
|
|
|
|
apple-artifacts:
|
|
name: Real Apple artifact CLI/MCP E2E
|
|
needs: changes
|
|
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
|
|
runs-on: macos-14
|
|
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
|
|
- name: Real binary metadata and Interface Builder integration
|
|
run: npm run verify:apple-dispatch && npm run verify:interface-builder
|
|
|
|
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
|
|
|
|
javascript-runtime-observation:
|
|
name: JavaScript runtime observation (${{ matrix.os }})
|
|
needs: changes
|
|
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, windows-latest]
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 10
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version-file: .nvmrc
|
|
cache: npm
|
|
- run: npm ci
|
|
- name: Verify real Node Inspector discovery and observation
|
|
run: npm run verify:inspector
|
|
|
|
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/filesystem/setupUnavailableProvider.test.ts
|
|
tests/boundary/filesystem/asarInventoryFreshness.test.ts
|
|
tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts
|
|
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
|
|
tests/conformance/providers/ghidra/ghidraWindowsFixture.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
|
|
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
|
|
- run: npm run verify:package:windows
|
|
- 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
|