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
76 changes: 51 additions & 25 deletions .github/scripts/check-language-parity.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@
#
# Language parity check.
#
# command-stream ships two implementations that must stay in lock-step: the
# JavaScript library under js/src/** and the Rust library under rust/src/**.
# This script fails when a pull request changes one language's source without
# touching the other's, so that behavioral changes are always made in both
# languages (see issue #155 review feedback).
# command-stream ships two implementations that must stay in lock-step. Source
# changes and benchmark changes are checked independently, so a token benchmark
# edit cannot satisfy a behavioral source change (or vice versa). This keeps
# both the implementation and its measured claims available in both languages.
#
# Escape hatch: add the `parity-exempt` label to the PR for changes that are
# legitimately single-language (the workflow skips this check when the label is
Expand Down Expand Up @@ -41,35 +40,62 @@ echo "Comparing against ${BASE} (merge-base ${MERGE_BASE})"
echo "Changed files:"
echo "${CHANGED}" | sed 's/^/ /'

js_changed=false
rust_changed=false
js_source_changed=false
rust_source_changed=false
js_benchmarks_changed=false
rust_benchmarks_changed=false
while IFS= read -r f; do
[ -z "${f}" ] && continue
case "${f}" in
js/src/*) js_changed=true ;;
rust/src/*) rust_changed=true ;;
js/src/*) js_source_changed=true ;;
rust/src/*) rust_source_changed=true ;;
js/benchmarks/* | js/tests/benchmark-*) js_benchmarks_changed=true ;;
rust/benchmarks/*) rust_benchmarks_changed=true ;;
esac
done <<EOF
${CHANGED}
EOF

echo "js/src changed: ${js_changed}"
echo "rust/src changed: ${rust_changed}"
echo "js/src changed: ${js_source_changed}"
echo "rust/src changed: ${rust_source_changed}"
echo "js benchmarks changed: ${js_benchmarks_changed}"
echo "rust benchmarks changed: ${rust_benchmarks_changed}"

if [ "${js_changed}" = "true" ] && [ "${rust_changed}" != "true" ]; then
echo "::error::JavaScript source (js/src/**) changed but Rust source (rust/src/**) did not."
echo "command-stream keeps the JavaScript and Rust implementations in parity."
echo "Please make the equivalent change under rust/src/**, or add the"
echo "'parity-exempt' label to this PR if the change is intentionally JS-only."
exit 1
fi
check_pair() {
local js_changed="$1"
local rust_changed="$2"
local js_scope="$3"
local rust_scope="$4"
local category="$5"

if [ "${rust_changed}" = "true" ] && [ "${js_changed}" != "true" ]; then
echo "::error::Rust source (rust/src/**) changed but JavaScript source (js/src/**) did not."
echo "command-stream keeps the JavaScript and Rust implementations in parity."
echo "Please make the equivalent change under js/src/**, or add the"
echo "'parity-exempt' label to this PR if the change is intentionally Rust-only."
exit 1
fi
if [ "${js_changed}" = "true" ] && [ "${rust_changed}" != "true" ]; then
echo "::error::JavaScript ${category} (${js_scope}) changed but Rust ${category} (${rust_scope}) did not."
echo "command-stream keeps the JavaScript and Rust implementations in parity."
echo "Please make the equivalent change under ${rust_scope}, or add the"
echo "'parity-exempt' label to this PR if the change is intentionally JavaScript-only."
exit 1
fi

if [ "${rust_changed}" = "true" ] && [ "${js_changed}" != "true" ]; then
echo "::error::Rust ${category} (${rust_scope}) changed but JavaScript ${category} (${js_scope}) did not."
echo "command-stream keeps the JavaScript and Rust implementations in parity."
echo "Please make the equivalent change under ${js_scope}, or add the"
echo "'parity-exempt' label to this PR if the change is intentionally Rust-only."
exit 1
fi
}

check_pair \
"${js_source_changed}" \
"${rust_source_changed}" \
'js/src/**' \
'rust/src/**' \
'source'
check_pair \
"${js_benchmarks_changed}" \
"${rust_benchmarks_changed}" \
'js/benchmarks/** or js/tests/benchmark-*' \
'rust/benchmarks/**' \
'benchmarks'

echo "Language parity check passed."
250 changes: 250 additions & 0 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
name: Language benchmarks

on:
push:
branches: [main]
paths:
- 'js/benchmarks/**'
- 'js/tests/benchmark-suite.test.mjs'
- 'js/tests/competitor-*'
- 'js/package.json'
- 'js/package-lock.json'
- 'js/bun.lock'
- 'rust/benchmarks/**'
- 'rust/tests/competitor_*'
- 'rust/Cargo.toml'
- 'rust/Cargo.lock'
- '.github/workflows/benchmarks.yml'
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'js/benchmarks/**'
- 'js/tests/benchmark-suite.test.mjs'
- 'js/tests/competitor-*'
- 'js/package.json'
- 'js/package-lock.json'
- 'js/bun.lock'
- 'rust/benchmarks/**'
- 'rust/tests/competitor_*'
- 'rust/Cargo.toml'
- 'rust/Cargo.lock'
- '.github/workflows/benchmarks.yml'
schedule:
- cron: '23 4 * * 1'
workflow_dispatch:
inputs:
profile:
description: 'Benchmark profile'
required: true
type: choice
default: full
options:
- smoke
- full

permissions:
contents: read

jobs:
javascript:
name: JavaScript (${{ github.event_name == 'pull_request' && 'smoke' || inputs.profile || 'full' }})
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: check-${{ github.workflow }}-${{ github.ref }}-javascript
cancel-in-progress: true
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false

- name: Simulate a fresh merge with the base branch
if: github.event_name == 'pull_request'
shell: bash
env:
BASE_REF: ${{ github.base_ref }}
run: bash .github/scripts/simulate-fresh-merge.sh

- uses: actions/setup-node@v6
with:
node-version: '24.x'

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest

- name: Install pinned dependencies
working-directory: js
run: bun install --frozen-lockfile

- name: Test benchmark infrastructure and adapters
working-directory: js
run: bun run benchmark:test

- name: Benchmark the pull request base
if: github.event_name == 'pull_request'
id: baseline
shell: bash
env:
BASE_REF: ${{ github.base_ref }}
run: |
set -euo pipefail
if ! git cat-file -e "origin/$BASE_REF:js/benchmarks/cli.mjs"; then
echo 'available=false' >> "$GITHUB_OUTPUT"
echo 'The base branch predates the benchmark suite; no comparison is available yet.'
exit 0
fi

base_directory="$(mktemp -d)"
cleanup() {
git worktree remove --force "$base_directory" || true
}
trap cleanup EXIT
git worktree add --detach "$base_directory" "origin/$BASE_REF"
(
cd "$base_directory/js"
bun install --frozen-lockfile
bun benchmarks/cli.mjs --smoke --output "$GITHUB_WORKSPACE/js/benchmarks/baseline"
)
echo 'available=true' >> "$GITHUB_OUTPUT"

- name: Run benchmark profile
working-directory: js
env:
BENCHMARK_PROFILE: ${{ github.event_name == 'pull_request' && 'smoke' || inputs.profile || 'full' }}
run: |
if [[ "$BENCHMARK_PROFILE" == 'smoke' ]]; then
bun run benchmark:smoke
else
bun run benchmark
fi

- name: Compare base and pull request measurements
if: steps.baseline.outputs.available == 'true'
working-directory: js
run: |
bun benchmarks/compare-results.mjs \
benchmarks/baseline/benchmark-results.json \
benchmarks/results/benchmark-results.json \
benchmarks/results

- name: Upload JSON and HTML reports
uses: actions/upload-artifact@v7
with:
name: command-stream-javascript-benchmarks-${{ github.run_id }}-${{ github.run_attempt }}
path: |
js/benchmarks/baseline/benchmark-results.json
js/benchmarks/results/benchmark-results.json
js/benchmarks/results/benchmark-report.html
js/benchmarks/results/benchmark-regressions.json
js/benchmarks/results/benchmark-regressions.md
if-no-files-found: error
retention-days: 30

rust:
name: Rust (${{ github.event_name == 'pull_request' && 'smoke' || inputs.profile || 'full' }})
runs-on: ubuntu-latest
timeout-minutes: 30
concurrency:
group: check-${{ github.workflow }}-${{ github.ref }}-rust
cancel-in-progress: true
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: false

- name: Simulate a fresh merge with the base branch
if: github.event_name == 'pull_request'
shell: bash
env:
BASE_REF: ${{ github.base_ref }}
run: bash .github/scripts/simulate-fresh-merge.sh

- name: Setup Rust
uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable branch @ 2026-09-03
with:
components: rustfmt, clippy

- name: Cache Cargo dependencies
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
rust/benchmarks/target
key: ${{ runner.os }}-rust-benchmarks-${{ hashFiles('rust/benchmarks/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-rust-benchmarks-

- name: Test and lint benchmark infrastructure
working-directory: rust
run: |
cargo fmt --manifest-path benchmarks/Cargo.toml -- --check
cargo clippy --manifest-path benchmarks/Cargo.toml --locked --all-targets -- -D warnings
cargo test --manifest-path benchmarks/Cargo.toml --locked --all-targets

- name: Benchmark the pull request base
if: github.event_name == 'pull_request'
id: rust-baseline
shell: bash
env:
BASE_REF: ${{ github.base_ref }}
run: |
set -euo pipefail
if ! git cat-file -e "origin/$BASE_REF:rust/benchmarks/Cargo.toml"; then
echo 'available=false' >> "$GITHUB_OUTPUT"
echo 'The base branch predates the Rust benchmark suite; no comparison is available yet.'
exit 0
fi

base_directory="$(mktemp -d)"
cleanup() {
git worktree remove --force "$base_directory" || true
}
trap cleanup EXIT
git worktree add --detach "$base_directory" "origin/$BASE_REF"
(
cd "$base_directory/rust"
cargo run --release --locked --manifest-path benchmarks/Cargo.toml -- \
--smoke \
--output "$GITHUB_WORKSPACE/rust/benchmarks/baseline"
)
echo 'available=true' >> "$GITHUB_OUTPUT"

- name: Run benchmark profile
working-directory: rust
env:
BENCHMARK_PROFILE: ${{ github.event_name == 'pull_request' && 'smoke' || inputs.profile || 'full' }}
run: |
if [[ "$BENCHMARK_PROFILE" == 'smoke' ]]; then
cargo run --release --locked --manifest-path benchmarks/Cargo.toml -- \
--smoke --output benchmarks/results
else
cargo run --release --locked --manifest-path benchmarks/Cargo.toml -- \
--output benchmarks/results
fi

- name: Compare base and pull request measurements
if: steps.rust-baseline.outputs.available == 'true'
working-directory: rust
run: |
cargo run --release --locked --manifest-path benchmarks/Cargo.toml --bin compare -- \
--baseline benchmarks/baseline/benchmark-results.json \
--current benchmarks/results/benchmark-results.json \
--output benchmarks/results

- name: Upload JSON and HTML reports
uses: actions/upload-artifact@v7
with:
name: command-stream-rust-benchmarks-${{ github.run_id }}-${{ github.run_attempt }}
path: |
rust/benchmarks/baseline/benchmark-results.json
rust/benchmarks/results/benchmark-results.json
rust/benchmarks/results/benchmark-report.html
rust/benchmarks/results/benchmark-comparison.json
rust/benchmarks/results/benchmark-comparison.md
if-no-files-found: error
retention-days: 30
10 changes: 5 additions & 5 deletions .github/workflows/parity.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Language parity check

# Ensure behavioral changes are made in both the JavaScript (js/src/**) and the
# Rust (rust/src/**) implementations. A PR that changes one without the other
# fails this check unless it carries the `parity-exempt` label.
# Ensure source and benchmark changes are made in both the JavaScript and Rust
# implementations. Each category is paired independently. A deliberately
# single-language PR must carry the `parity-exempt` label.
#
# See issue #155 review feedback: "double check that all features that are
# supported in JavaScript are fully supported in Rust and we have CI/CD rules,
Expand All @@ -17,7 +17,7 @@ permissions:

jobs:
parity:
name: JS/Rust source parity
name: JS/Rust implementation parity
runs-on: ubuntu-latest
timeout-minutes: 10
# Skip entirely when the PR is explicitly marked as a single-language change.
Expand All @@ -32,7 +32,7 @@ jobs:
# Read-only job: the parity script only diffs the checked-out history.
persist-credentials: false

- name: Check JavaScript/Rust source parity
- name: Check JavaScript/Rust implementation parity
env:
BASE_REF: ${{ github.base_ref }}
run: bash .github/scripts/check-language-parity.sh
6 changes: 4 additions & 2 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -226,9 +226,11 @@ jobs:
with:
tool: cargo-audit@0.22.2

- name: Audit the committed Cargo.lock
- name: Audit the committed Cargo lockfiles
working-directory: rust
run: cargo audit --file Cargo.lock
run: |
cargo audit --file Cargo.lock
cargo audit --file benchmarks/Cargo.lock

secret-scan:
name: Scan for committed secrets
Expand Down
Loading
Loading