Skip to content
Open
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
50 changes: 50 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,56 @@ jobs:
find target/debug/deps -maxdepth 1 -type f -perm -111 ! -name '*.so' -delete
done

# ---------------------------------------------------------------------------
# Gap-suite smoke (roadmap I-01)
#
# AOT-compiles every test-files/test_gap_*.ts and diffs it byte-for-byte
# against `node --experimental-strip-types` via scripts/run_gap_tests.sh
# (a thin wrapper over run_parity_tests.sh --filter test_gap_, so it reuses
# the one normalizer + skip-list). The gap suite is the highest-signal-
# per-second test Perry has, and until now it had no committed runner and no
# CI gate — a contributor who regressed a single feature got a green build.
#
# INFORMATIONAL for now (continue-on-error): the first runs surface which gap
# tests currently fail on the Linux CI image so they can be triaged into
# test-parity/known_failures.json. Once curated + reliably green, drop
# `continue-on-error` and add `smoke-parity` to the branch-protection
# required checks (staged rollout: informational -> required).
# Oracle = node 22, matching the legacy parity job the gap suite is already
# green under. Node-version-sensitive output (v8/perf_hooks/process internals)
# would otherwise diff against a newer Node and mask real Perry regressions.
# (The node-suite regression guard uses node 26 against its frozen baseline —
# a different mechanism.)
# ---------------------------------------------------------------------------
smoke-parity:
continue-on-error: true
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
with:
# Read-only job (build + test); keep the GITHUB_TOKEN out of the
# local git config (least privilege — OWASP / CodeRabbit).
persist-credentials: false

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2
with:
shared-key: "${{ runner.os }}-perry"
save-if: ${{ github.ref == 'refs/heads/main' }}

- name: Setup Node.js
uses: actions/setup-node@v6
Comment on lines +319 to +334

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify unpinned action refs in workflow files.
# Expected after fix: no matches.
rg -nP '^\s*-\s*uses:\s*[^@\s]+@(?![0-9a-fA-F]{40}\b).+$' .github/workflows/*.yml

Repository: PerryTS/perry

Length of output: 4338


🏁 Script executed:

# Extract the job context around lines 279-300 to verify if this is a new smoke-parity job
sed -n '279,300p' .github/workflows/test.yml | cat -n

# Also check if all actions in the snippet (284-295) are unpinned
echo "--- Checking lines 284-295 for unpinned actions ---"
sed -n '284,295p' .github/workflows/test.yml | cat -n

Repository: PerryTS/perry

Length of output: 1241


Pin all GitHub Actions in the smoke-parity job to commit SHAs.

Four actions use mutable version tags instead of commit SHAs:

  • actions/checkout@v6 (line 284)
  • dtolnay/rust-toolchain@stable (line 289)
  • Swatinem/rust-cache@v2 (line 291)
  • actions/setup-node@v6 (line 294)

Pinning to full commit SHAs is required to prevent supply-chain drift and ensure policy compliance.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 284-284: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 284-284: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 287-287: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 289-289: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 295-295: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 289-289: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)


[error] 295-295: runtime artifacts potentially vulnerable to a cache poisoning attack (cache-poisoning): enables caching by default

(cache-poisoning)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 284 - 295, Replace all mutable
version tags with full commit SHAs for the four GitHub Actions used in the
smoke-parity job in the test.yml workflow file. Update actions/checkout from `@v6`
to its commit SHA, dtolnay/rust-toolchain from `@stable` to its commit SHA,
Swatinem/rust-cache from `@v2` to its commit SHA, and actions/setup-node from `@v6`
to its commit SHA. Each action should be specified in the format
owner/repo@<full-commit-sha> to ensure reproducibility and prevent supply-chain
drift.

Source: Linters/SAST tools

with:
# Match the legacy parity job the gap suite is already green under;
# node 26 introduces version-sensitive diffs that aren't Perry bugs.
node-version: '22'

- name: Run gap suite
run: ./scripts/run_gap_tests.sh

# ---------------------------------------------------------------------------
# GC write-barrier stress (optional / non-blocking)
#
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Perry is a native TypeScript compiler written in Rust that compiles TypeScript s

## TypeScript Parity Status

Tracked via the gap test suite (`test-files/test_gap_*.ts`, 28 tests). Compared byte-for-byte against `node --experimental-strip-types`. Run via `/tmp/run_gap_tests.sh` after `cargo build --release -p perry-runtime -p perry-stdlib -p perry`.
Tracked via the gap test suite (`test-files/test_gap_*.ts`, 235 tests). Compared byte-for-byte against `node --experimental-strip-types`. Run via `./scripts/run_gap_tests.sh` (a thin wrapper over `run_parity_tests.sh --filter test_gap_` that builds the compiler itself and gates on no new untriaged failures).

**Last full sweep:** run `./run_parity_tests.sh` for the current snapshot. The umbrella tracker is #793 (Node.js + TypeScript compatibility roadmap); the previously-cited #447–#452 batch closed on 2026-05-04. Currently-open trackers worth knowing about:

Expand Down
78 changes: 78 additions & 0 deletions scripts/run_gap_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# Committed runner for the Perry "gap" suite.
#
# Every test-files/test_gap_*.ts is AOT-compiled by Perry and diffed
# byte-for-byte against `node --experimental-strip-types`. This is a thin
# wrapper over run_parity_tests.sh --filter test_gap_ so it reuses the ONE
# canonical normalizer, the skip-list, the per-test output cap, and the JSON
# report (this shared-normalizer reuse is the seed of roadmap initiative I-14).
#
# Replaces the out-of-repo /tmp/run_gap_tests.sh that CLAUDE.md used to point
# at — the gap suite is the highest-signal-per-second test Perry has and was
# previously dark in CI.
#
# Regression-gate semantics: exits non-zero if any gap test fails parity or
# compilation and is NOT already triaged in test-parity/known_failures.json.
# (run_parity_tests.sh's own exit code only trips below 80% AGGREGATE parity,
# which is far too loose to catch a single-feature regression — exactly the
# "a module silently went to 0 behind a green build" class.)
#
# Requirements:
# - a Rust toolchain (the wrapped run_parity_tests.sh builds target/release/perry)
# - node with --experimental-strip-types
# - jq
#
# Usage: scripts/run_gap_tests.sh
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$ROOT"

# Run-scoped temp dir — fixed /tmp names would let concurrent runs (a second
# PR, local + CI on the same box, or the future node-suite-guard alongside)
# clobber each other's failure lists and produce a false gate result.
WORK="$(mktemp -d "${TMPDIR:-/tmp}/perry-gap.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT

echo "==> Running gap suite (test-files/test_gap_*.ts) via run_parity_tests.sh --filter test_gap_"
# run_parity_tests.sh exits 1 when AGGREGATE parity < 80%. We gate on "no NEW
# untriaged failures" instead (below), so don't let its aggregate exit abort us.
set +e
./run_parity_tests.sh --filter test_gap_
set -e

REPORT="test-parity/reports/latest.json"
KNOWN="test-parity/known_failures.json"
if [[ ! -f "$REPORT" ]]; then
echo "ERROR: parity report not found at $REPORT (did run_parity_tests.sh run?)" >&2
exit 2
fi

# Every failure in this report is a gap test (we filtered on test_gap_), so the
# whole failure set is the gap failure set. Drop empty entries (run_parity_tests.sh
# emits compile: [""] when there are zero compile failures).
jq -r '(.failures.parity // []) + (.failures.compile // []) | .[] | select(. != "")' \
"$REPORT" | sort -u > "$WORK/all_fails.txt"

if [[ -f "$KNOWN" ]]; then
# known_failures.json is keyed by test name; skip the audit-metadata _schema key.
jq -r 'keys[] | select(. != "_schema")' "$KNOWN" | sort -u > "$WORK/known.txt"
else
: > "$WORK/known.txt"
fi

comm -23 "$WORK/all_fails.txt" "$WORK/known.txt" > "$WORK/new.txt"
TOTAL=$(wc -l < "$WORK/all_fails.txt" | tr -d ' ')

if [[ -s "$WORK/new.txt" ]]; then
echo "" >&2
echo "NEW gap failures (not triaged in test-parity/known_failures.json):" >&2
sed 's/^/ - /' "$WORK/new.txt" >&2
echo "" >&2
echo "Fix the regression, or — if the failure is intentional/known — add a" >&2
echo "triaged entry to test-parity/known_failures.json (category + reason)." >&2
exit 1
fi

echo "All ${TOTAL} gap failures (if any) are known/triaged. Gap gate OK."
Loading