Skip to content

deps(deps): bump actions/setup-node from 4 to 6 #317

deps(deps): bump actions/setup-node from 4 to 6

deps(deps): bump actions/setup-node from 4 to 6 #317

Workflow file for this run

name: CI
on:
push:
branches: [main]
# Docs-only changes don't need to spin up the Windows wheel
# matrix. Keep the fast Ubuntu jobs (fmt / clippy / doc / pii)
# for any commit that actually touches code, including tests
# or workflow files. `paths-ignore` is a whole-run filter so
# this skips ALL jobs when only ignored paths changed.
paths-ignore:
- "**.md"
- "docs/**"
- "CHANGELOG.md"
- "LICENSE*"
- ".gitignore"
- ".github/ISSUE_TEMPLATE/**"
- "CITATION.cff"
pull_request:
branches: [main]
# Supersede any in-flight run for the same ref when a new push
# arrives. Prevents #52a → #52b → #52c back-to-back pushes from
# burning 3× runner-minutes on stale Windows matrices nobody will
# look at. `cancel-in-progress: true` is the budget saver; the
# group key is per-ref so concurrent PR + push on different refs
# don't cancel each other.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# Least-privilege at the workflow level. No CI job here modifies the
# repo, opens issues, comments on PRs, or otherwise needs write
# access; `contents: read` is enough. Individual jobs can request
# additional scopes with a per-job `permissions:` block if that
# ever changes.
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
jobs:
fmt:
name: cargo fmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- run: cargo fmt --all -- --check
clippy:
name: cargo clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
- run: cargo clippy --all-targets --all-features -- -D warnings
test:
name: cargo test / ${{ matrix.os }} / ${{ matrix.rust }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
include:
- os: ubuntu-latest
rust: "1.85" # MSRV (Rust 2024 edition minimum)
steps:
# Windows' git default `core.autocrlf=true` mangles binary files
# that aren't explicitly marked `binary` in `.gitattributes`.
# The phi-ag/rvt corpus stores some `.rfa` files outside LFS
# without binary attribution — Windows checkout therefore
# silently CRLF-converts them and CFB parsing fails on affected
# streams. Disable autocrlf globally before any checkout runs.
- name: Disable autocrlf on Windows
if: runner.os == 'Windows'
run: git config --global core.autocrlf false
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
# Fetch the Autodesk-owned `rac_basic_sample_family` corpus from
# phi-ag/rvt. rvt-rs intentionally does not redistribute these
# files (see SECURITY.md §Scope); they're pulled at build time.
- name: Fetch phi-ag/rvt sample corpus
uses: actions/checkout@v4
with:
repository: phi-ag/rvt
path: _corpus
lfs: true
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
- name: Build
run: cargo build --release --all-targets
- name: Unit tests
run: cargo test --release --lib --bins
- name: Doc tests
run: cargo test --release --doc
# Regression-proof the Q5.2 100%-classification claim. Fails if
# any field in any release decodes to FieldType::Unknown, or if
# any expected corpus file is missing (guarantees non-vacuous
# coverage).
- name: Coverage gate — 100% field-type classification
env:
RVT_SAMPLES_DIR: ${{ github.workspace }}/_corpus/examples/Autodesk
RVT_REQUIRE_CORPUS: "1"
run: cargo test --release --test field_type_coverage -- --nocapture
# Broader integration suite (round-trips, part-atom parsing,
# history extraction, stream inventory, etc.) runs against the
# corpus on all three OSes. Previously failed on Windows due to
# a path-separator bug (CFB stream names came back with `\`
# instead of `/` on Windows; fixed in reader::stream_names).
- name: Integration tests (corpus-dependent)
env:
RVT_SAMPLES_DIR: ${{ github.workspace }}/_corpus/examples/Autodesk
run: cargo test --release --test samples
- name: IFC export integration tests
env:
RVT_SAMPLES_DIR: ${{ github.workspace }}/_corpus/examples/Autodesk
run: cargo test --release --test ifc_roundtrip
ifc-smoke:
# IFC-42: run the synthetic-project integration test and assert
# the emitted STEP file has the expected entity counts. Catches
# regressions where the writer drops elements, the bridge layer
# mis-counts openings, or STEP serialisation silently truncates.
# Uses the committed fixture at `tests/fixtures/synthetic-project.ifc`
# as the reference — any writer change that alters counts must
# update this job AND the fixture together.
name: IFC synthetic-project smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
- name: Build + run synthetic-project integration test
run: cargo test --release --test ifc_synthetic_project
- name: Assert entity counts in committed fixture
run: |
set -euo pipefail
f=tests/fixtures/synthetic-project.ifc
expect() {
local pat="$1" want="$2"
local got
got=$(grep -c "$pat" "$f" || true)
if [ "$got" != "$want" ]; then
echo "::error::IFC count drift: $pat got $got, want $want"
exit 1
fi
echo "ok: $pat = $got"
}
expect IFCPROJECT 1
expect IFCBUILDINGSTOREY 3
expect IFCWALL 4
expect IFCSLAB 1
expect IFCDOOR 1
expect IFCWINDOW 2
expect IFCMATERIAL 2
expect IFCPROPERTYSET 2
expect IFCOPENINGELEMENT 1
expect IFCRELVOIDSELEMENT 1
expect IFCRELFILLSELEMENT 1
expect IFCEXTRUDEDAREASOLID 7
ifcopenshell-validate:
# IFC-41: load the committed synthetic-project.ifc fixture in
# IfcOpenShell and assert it opens without errors, carries the
# right spatial hierarchy, and resolves every IfcRel* link. This
# is the independent-validator gate — the Rust-side ifc-smoke
# job above counts substrings, but IfcOpenShell actually parses
# the STEP file through the full IFC4 schema, so errors the
# substring check can't see (dangling refs, wrong attribute
# arity, enum values outside the spec vocabulary) fail here.
name: IfcOpenShell validates synthetic project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install IfcOpenShell (prebuilt wheel)
run: |
python -m pip install --upgrade pip
# ifcopenshell ships prebuilt wheels on PyPI as
# `ifcopenshell-python` for Linux/macOS/Windows x64. Pin
# to a stable 0.8.x series — the 0.7 API has semantic
# differences in entity-by-type iteration.
python -m pip install 'ifcopenshell>=0.8.0,<0.9.0'
- name: Validate synthetic-project fixture
run: |
python - <<'PY'
import ifcopenshell
path = "tests/fixtures/synthetic-project.ifc"
f = ifcopenshell.open(path)
# Schema identifier — we only ship IFC4.
assert f.schema == "IFC4", f"expected IFC4, got {f.schema}"
# Spatial hierarchy: 1 project, 1 site, 1 building, 3 storeys.
assert len(f.by_type("IfcProject")) == 1
assert len(f.by_type("IfcSite")) == 1
assert len(f.by_type("IfcBuilding")) == 1
assert len(f.by_type("IfcBuildingStorey")) == 3, (
f"expected 3 storeys, got {len(f.by_type('IfcBuildingStorey'))}"
)
# Per-element entities.
assert len(f.by_type("IfcWall")) == 4
assert len(f.by_type("IfcSlab")) == 1
assert len(f.by_type("IfcDoor")) == 1
assert len(f.by_type("IfcWindow")) == 2
# Openings + rels must have resolved references (no
# dangling pointers — IfcOpenShell would raise during
# open() if they did; double-check here).
openings = f.by_type("IfcOpeningElement")
assert len(openings) == 1
for op in openings:
voids = [r for r in f.by_type("IfcRelVoidsElement")
if r.RelatedOpeningElement.id() == op.id()]
fills = [r for r in f.by_type("IfcRelFillsElement")
if r.RelatingOpeningElement.id() == op.id()]
assert len(voids) == 1, f"opening {op} has {len(voids)} voids"
assert len(fills) == 1, f"opening {op} has {len(fills)} fills"
print("IfcOpenShell validation passed:")
print(f" schema: {f.schema}")
print(f" entities: {len(list(f))}")
print(f" walls/slabs/doors/windows: 4/1/1/2")
print(f" storeys: 3")
PY
# IFC-44: second fixture — structural framing + shared
# RepresentationMap + faceted-brep furniture. Validates IFC-17 /
# IFC-20 / IFC-21 / IFC-24 / IFC-30 paths that the project
# fixture above doesn't exercise.
- name: Validate synthetic-structural fixture
run: |
python - <<'PY'
import ifcopenshell
path = "tests/fixtures/synthetic-structural.ifc"
f = ifcopenshell.open(path)
assert f.schema == "IFC4"
assert len(f.by_type("IfcColumn")) == 2, "expected 2 columns"
assert len(f.by_type("IfcBeam")) == 2, "expected 2 beams"
assert len(f.by_type("IfcDoor")) == 3, "expected 3 doors"
assert len(f.by_type("IfcFurniture")) == 1, "expected 1 furniture"
# IFC-21: shared representation map, one entity, three
# mapped items referencing it.
reps = f.by_type("IfcRepresentationMap")
assert len(reps) == 1, f"expected 1 IfcRepresentationMap, got {len(reps)}"
mapped_items = f.by_type("IfcMappedItem")
assert len(mapped_items) == 3, (
f"expected 3 IfcMappedItem instances, got {len(mapped_items)}"
)
# All three mapped items must reference the same map.
for mi in mapped_items:
assert mi.MappingSource.id() == reps[0].id(), (
"mapped item points at a different representation map"
)
# IFC-20: one faceted brep for the furniture.
assert len(f.by_type("IfcFacetedBrep")) == 1
# IFC-30: MaterialProfileSet usage on the 2 beams.
assert len(f.by_type("IfcMaterialProfileSetUsage")) >= 1
# IFC-24: I-shape profiles (3 total) + 1 circle profile.
assert len(f.by_type("IfcIShapeProfileDef")) == 3
assert len(f.by_type("IfcCircleProfileDef")) == 1
print("IFC-44 structural fixture validation passed:")
print(f" columns/beams/doors/furniture: 2/2/3/1")
print(f" IfcRepresentationMap/IfcMappedItem: 1/3 (3:1 sharing ratio)")
print(f" IfcFacetedBrep: 1 (tetrahedron)")
PY
# L5B-11.8: real-file walker → IFC end-to-end gate. Emit a
# fresh IFC4 STEP from one of the family samples via `rvt-ifc`,
# then load it in IfcOpenShell and assert: schema is IFC4, the
# IfcProject exists, and every element entity that the walker
# threaded through resolves without dangling references. This
# gate catches regressions that the synthetic-fixture checks
# can't see — e.g. a walker change that silently emits zero
# elements on real files, or breaks step_writer's handling of
# BuildingElement entities appended after the Project.
- name: Build rvt-ifc + walker->IFC real-file gate
run: |
cargo build --release --bin rvt-ifc
# The family corpus resolves via RVT_SAMPLES_DIR (used by
# `tests/common::samples_dir`) — if phi-ag/rvt was checked
# out, point at it; otherwise skip (family samples aren't
# redistributed by this repo).
if [ -d "${RVT_SAMPLES_DIR:-./_corpus/examples/Autodesk}" ]; then
SAMPLE=$(ls "${RVT_SAMPLES_DIR:-./_corpus/examples/Autodesk}"/*.rfa 2>/dev/null | head -1)
fi
if [ -z "$SAMPLE" ]; then
echo "skipping walker->IFC real-file gate: no family sample available"
exit 0
fi
./target/release/rvt-ifc "$SAMPLE" -o /tmp/walker-real-file.ifc
python - <<'PY'
import ifcopenshell
f = ifcopenshell.open("/tmp/walker-real-file.ifc")
assert f.schema == "IFC4", f"schema: {f.schema}"
# Project must exist (metadata-only baseline).
assert len(f.by_type("IfcProject")) == 1, "lost IfcProject"
# Walker may emit zero elements on family files — that's
# acceptable. But if any BuildingElementProxy entities were
# emitted, each must resolve without dangling references
# (IfcOpenShell would have raised during open() otherwise).
proxies = f.by_type("IfcBuildingElementProxy")
print(f"L5B-11.8 walker->IFC gate passed:")
print(f" schema: {f.schema}")
print(f" IfcProject: 1")
print(f" IfcBuildingElementProxy (walker-recovered): {len(proxies)}")
PY
doc:
name: cargo doc
runs-on: ubuntu-latest
env:
RUSTDOCFLAGS: "-D warnings"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
- run: cargo doc --no-deps --lib
# Q-09: benchmark-build regression gate.
#
# Runs `cargo bench --no-run` to compile every benchmark target
# against the current library API. Without this job, a PR that
# renames a public item or changes a function's signature can
# break the benchmarks silently — `cargo test` doesn't build the
# `[[bench]]` targets, and nobody runs benches on every push.
#
# A full runtime regression gate (compare wallclock numbers to a
# baseline, fail on >X% slowdown) would need a dedicated runner
# class with stable performance characteristics — GitHub's shared
# runners have variable load, so wallclock comparisons need wide
# tolerances to avoid false positives. Compile-gate is the
# high-value, low-noise version we can enforce on every push.
bench-build:
name: cargo bench --no-run
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
with:
key: bench-build
# `--no-run` compiles the benches but doesn't execute them.
# This catches API drift (renamed public items, changed
# function signatures, missing traits on types used in
# benches) that `cargo test` doesn't, since benches live in
# their own `[[bench]]` targets.
- run: cargo bench --no-run
# Q-12: reproduce the docs.rs build locally in CI.
# docs.rs runs `cargo doc` on nightly with `--all-features` and the
# `--cfg docsrs` flag set; broken rustdoc (invalid links, missing
# items, syntax errors in code examples) breaks the docs.rs page —
# catching it in CI means every PR validates that rvt-rs will render
# properly on docs.rs before the release cut.
docs-rs-preview:
name: docs.rs build preview
runs-on: ubuntu-latest
env:
# Same flags docs.rs passes, plus -D warnings so this job fails
# instead of silently publishing broken docs.
RUSTDOCFLAGS: "--cfg docsrs -D warnings"
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
# docs.rs builds with nightly — any `#[cfg_attr(docsrs, ...)]`
# attributes + `#![feature(...)]` gates must be available.
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
# Mirror the [package.metadata.docs.rs] settings from Cargo.toml
# (`all-features = true`) — if these drift, update both.
- run: cargo +nightly doc --no-deps --all-features
python-wheel:
name: Python wheel / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
# Windows CFB stream names need the same autocrlf=false guard
# we use for the Rust Windows integration job, or LFS-bound
# corpus bytes get CRLF-mangled. Keeping this idempotent.
- name: Disable autocrlf on Windows
if: runner.os == 'Windows'
run: git config --global core.autocrlf false
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Fetch phi-ag/rvt sample corpus
uses: actions/checkout@v4
with:
repository: phi-ag/rvt
path: _corpus
lfs: true
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
with:
key: python-wheel-${{ matrix.os }}
# Build the abi3 wheel. SEC-12/13: wheel sources live in the
# `rvt-py/` workspace member crate; maturin follows the
# `manifest-path` in pyproject.toml. `--release` produces an
# optimised binary matching what users get via `pip install rvt`.
- name: Build wheel via maturin
uses: PyO3/maturin-action@v1
with:
command: build
args: --release --out dist
# Install into this runner's Python, then run the pytest suite
# we ship in tests/python/. This is the real regression gate —
# if the wheel doesn't actually import or the bindings return
# wrong values, CI fails here.
- name: Install the built wheel
shell: bash
run: |
python3 -m pip install --upgrade pip pytest
python3 -m pip install --force-reinstall dist/*.whl
- name: Run pytest integration tests
shell: bash
env:
RVT_SAMPLES_DIR: ${{ github.workspace }}/_corpus/examples/Autodesk
run: python3 -m pytest tests/python -v
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: rvt-wheel-${{ matrix.os }}
path: dist/*.whl
if-no-files-found: error
# Supply-chain hardening. `cargo-deny check` enforces our deny.toml:
# permissive-license allowlist, RustSec advisory deny, crates.io-only
# source, no wildcards. Keeps the dependency tree auditable as the
# project grows.
deny:
name: cargo deny
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: EmbarkStudios/cargo-deny-action@91bf2b620e09e18d6eb78b92e7861937469acedb # v2.0.17
with:
command: check
# Fails CI on any RustSec advisory published against the current
# dependency set. Complements cargo-deny's coverage — deny has the
# per-advisory `ignore` list for triage flexibility, audit catches
# fresh advisories the moment they land.
audit:
name: cargo audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: rustsec/audit-check@69366f33c96575abad1ee0dba8212993eecbe998 # v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
viewer-dependencies:
name: viewer dependency audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
cache: npm
cache-dependency-path: viewer/package-lock.json
- name: Install viewer dependencies
working-directory: viewer
run: npm ci
- name: Audit viewer npm dependencies
working-directory: viewer
run: npm audit --audit-level=high
pii-guard:
name: PII guard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Scan for PII-shaped leak patterns
run: |
set -e
echo "Scanning repo for patterns that must never be committed..."
# Each regex catches a *class* of PII without spelling out
# any specific sensitive value. The patterns are intentionally
# broad — they're belt-and-suspenders on top of src/redact.rs.
PATTERNS=(
# Absolute home paths (macOS, Linux, Windows).
'/Users/[a-z][a-z0-9._-]+/'
'/home/[a-z][a-z0-9._-]+/'
'C:\\Users\\[a-z][a-z0-9._-]+\\'
# Autodesk employee-owned OneDrive authoring paths.
'OneDrive - Autodesk\\[^\\]*\\Revit - [0-9]{3,}'
# Autodesk build-server paths baked into DLLs.
'F:\\Ship\\[0-9]{4}_'
)
failed=0
for p in "${PATTERNS[@]}"; do
if git grep -I -n -P "$p" -- ':!.github/workflows/' ':!src/redact.rs' 2>/dev/null; then
echo "::error::PII-shaped pattern matched: $p"
failed=1
fi
done
if [ "$failed" -ne 0 ]; then
exit 1
fi
echo "PII guard: clean."