Skip to content

chore(docker): pin FALLOW_VERSION 3.11.0 with refreshed checksums #4814

chore(docker): pin FALLOW_VERSION 3.11.0 with refreshed checksums

chore(docker): pin FALLOW_VERSION 3.11.0 with refreshed checksums #4814

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
merge_group:
# Deny all permissions by default, grant per-job as needed
permissions: {}
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
rust: ${{ steps.filter.outputs.rust }}
windows-rust: ${{ steps.filter.outputs.windows-rust }}
vscode: ${{ steps.filter.outputs.vscode }}
zed: ${{ steps.filter.outputs.zed }}
npm: ${{ steps.filter.outputs.npm }}
actions: ${{ steps.filter.outputs.actions }}
docker: ${{ steps.filter.outputs.docker }}
miri: ${{ steps.filter.outputs.miri }}
ci-scripts: ${{ steps.filter.outputs.ci-scripts }}
npm-package: ${{ steps.filter.outputs.npm-package }}
self-analyze: ${{ steps.filter.outputs.self-analyze }}
skills: ${{ steps.filter.outputs.skills }}
action-current: ${{ steps.filter.outputs.action-current }}
viz: ${{ steps.filter.outputs.viz }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
id: filter
with:
filters: |
# The `rust` filter gates the `check` job, which runs two drift
# gates:
# - `cargo test/clippy --features schema-emit` for the
# Rust-source -> docs/output-schema.json chain (output schema)
# - `cargo test --workspace --lib --bins --tests --examples`
# for the FallowConfig -> schema.json chain (config-input
# schema, #440), via a `#[cfg(test)] mod config_schema_drift`
# test in `crates/cli/src/init.rs`.
# JsonSchema-deriving structs are spread across multiple crates;
# keeping the filter at `crates/**` (rather than a narrower
# per-crate list) ensures edits to ANY of the following subtrees
# trigger the gate:
# - crates/types/src/ (analysis and base payload types)
# - crates/output/src/ (health, action, and envelope wire shapes)
# - crates/api/src/ (programmatic and attribution wrappers)
# - crates/cli/src/bin/schema_emit.rs (derived definition registry and schema assembly)
# - crates/config/src/config/ (FallowConfig + RulesConfig + presets, source of schema.json)
# Both `docs/output-schema.json` and `schema.json` are included so
# PR edits that ONLY touch a committed schema (without a matching
# Rust change) still trigger the drift gate on the PR rather than
# slipping through to a push-time failure on main. Refs #416, #440.
# Generated npm and editor contract files are included for the same
# reason: the Ubuntu check job runs the full `generate:contracts:check`
# bundle now, covering output schema, capability metadata, TS
# contracts, NAPI types, and agent docs in one place.
rust:
- 'crates/**'
- 'tests/**'
- 'docs/output-schema.json'
- 'schema.json'
- 'plugin-schema.json'
- 'rule-pack-schema.json'
- 'npm/fallow/schema.json'
- 'npm/fallow/capabilities.json'
- 'npm/fallow/issue-registry.json'
- 'npm/fallow/types/output-contract.d.ts'
- 'npm/fallow/skills/fallow/**'
- 'editors/vscode/src/generated/output-contract.d.ts'
- 'editors/vscode/src/generated/lsp-initialization-options.d.ts'
- 'editors/vscode/src/generated/issue-types.ts'
- 'editors/vscode/package.json'
- 'tools/type-aware-sidecar/src/generated-protocol.mjs'
- 'scripts/generate-all.mjs'
- 'scripts/generation-transaction.mjs'
- 'scripts/contract-surfaces.mjs'
- 'scripts/generate-agent-docs.mjs'
- 'Cargo.toml'
- 'Cargo.lock'
- 'deny.toml'
- '.clippy.toml'
- 'rust-toolchain.toml'
windows-rust:
- 'crates/core/src/discover/walk.rs'
- 'crates/core/src/plugins/manifest_entries.rs'
- 'crates/core/tests/integration_test.rs'
- 'crates/core/tests/integration_test/symlink_root_containment.rs'
- 'crates/engine/src/changed_files.rs'
- 'crates/engine/src/churn.rs'
- 'crates/engine/src/repo_refs.rs'
- 'crates/engine/Cargo.toml'
- 'crates/cli/src/signal/**'
- 'crates/cli/src/type_aware.rs'
- 'crates/cli/Cargo.toml'
- 'crates/lsp/**'
- 'crates/mcp/src/tools/code_mode_subprocess.rs'
- 'crates/mcp/src/tools/mod.rs'
- 'crates/mcp/src/tools/process_tree.rs'
- 'crates/mcp/Cargo.toml'
- '.github/actions/setup-rust/**'
- '.github/workflows/ci.yml'
- 'scripts/workflow-policy.test.mjs'
- 'Cargo.toml'
- 'Cargo.lock'
- '.clippy.toml'
- 'rust-toolchain.toml'
vscode:
- 'editors/vscode/**'
- 'crates/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- '.github/workflows/ci.yml'
zed:
- 'editors/zed/**'
npm:
- 'npm/**'
actions:
- '.github/**'
docker:
- 'Dockerfile'
- 'Dockerfile.dev'
- '.dockerignore'
- 'examples/docker/**'
- '.github/dependabot.yml'
- '.github/workflows/ci.yml'
miri:
- 'crates/graph/**'
- 'crates/extract/**'
- 'crates/types/**'
- 'crates/config/**'
ci-scripts:
- 'ci/**'
- 'action/**'
npm-package:
- 'npm/fallow/**'
- 'schema.json'
- '.github/scripts/**'
- '.github/workflows/ci.yml'
self-analyze:
- '.github/workflows/ci.yml'
- 'editors/vscode/**'
- 'npm/fallow/**'
# `skills` gates the public skill contract: the public plugin must
# match the source under npm/fallow/skills after its one declared
# frontmatter transform.
skills:
- 'npm/fallow/skills/**'
- 'scripts/vendor-skills.mjs'
- '.github/workflows/ci.yml'
action-current:
- 'action/**'
- 'action.yml'
- 'crates/**'
- 'tests/fixtures/basic-project/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- '.github/actions/setup-rust/**'
- '.github/workflows/ci.yml'
- 'scripts/workflow-policy.test.mjs'
viz:
- 'viz-frontend/**'
- 'crates/cli/viz-assets/**'
docker:
name: Docker
needs: changes
if: needs.changes.outputs.docker == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build image
run: docker build -t fallow:ci .
- name: Run version
# Asserts the image reports the version pinned in the Dockerfile's own
# ARG default, not just that the binary runs. This is what makes this
# required check a real end-to-end proof for the release's Docker pin
# commit (fallow-release skill step 13): the new pins download,
# checksum-verify, and report the just-released version instead of
# merely not crashing.
shell: bash
run: |
set -euo pipefail
PINNED_VERSION="$(grep -m1 '^ARG FALLOW_VERSION=' Dockerfile | cut -d '=' -f2)"
OUTPUT="$(docker run --rm fallow:ci --version)"
echo "${OUTPUT}"
echo "${OUTPUT}" | grep -qF "${PINNED_VERSION}"
- name: Check exit code propagation
shell: bash
run: |
set -euo pipefail
tmp="$(mktemp -d)"
mkdir -p "$tmp/dirty/src" "$tmp/clean/src"
printf '%s\n' '{"name":"dirty","type":"module","main":"src/index.ts","dependencies":{},"devDependencies":{}}' > "$tmp/dirty/package.json"
printf '%s\n' '{"name":"clean","type":"module","main":"src/index.ts","dependencies":{},"devDependencies":{}}' > "$tmp/clean/package.json"
printf '%s\n' 'export const entry = 1;' > "$tmp/dirty/src/index.ts"
printf '%s\n' 'export const unused = 1;' > "$tmp/dirty/src/unused.ts"
printf '%s\n' 'export const entry = 1;' > "$tmp/clean/src/index.ts"
set +e
docker run --rm -v "$tmp/dirty:/workspace" --user "$(id -u):$(id -g)" fallow:ci dead-code --format json --quiet > "$tmp/dirty.json"
dirty_code="$?"
docker run --rm -v "$tmp/clean:/workspace" --user "$(id -u):$(id -g)" fallow:ci dead-code --format json --quiet > "$tmp/clean.json"
clean_code="$?"
set -e
test "$dirty_code" -eq 1
test "$clean_code" -eq 0
- name: Check Compose example
shell: bash
run: |
set -euo pipefail
docker tag fallow:ci fallow:local
tmp="$(mktemp -d)"
mkdir -p "$tmp/dirty/src" "$tmp/clean/src"
printf '%s\n' '{"name":"dirty","type":"module","main":"src/index.ts","dependencies":{},"devDependencies":{}}' > "$tmp/dirty/package.json"
printf '%s\n' '{"name":"clean","type":"module","main":"src/index.ts","dependencies":{},"devDependencies":{}}' > "$tmp/clean/package.json"
printf '%s\n' 'export const entry = 1;' > "$tmp/dirty/src/index.ts"
printf '%s\n' 'export const unused = 1;' > "$tmp/dirty/src/unused.ts"
printf '%s\n' 'export const entry = 1;' > "$tmp/clean/src/index.ts"
cp examples/docker/compose.yaml "$tmp/dirty/compose.yaml"
cp examples/docker/compose.yaml "$tmp/clean/compose.yaml"
env "UID=$(id -u)" "GID=$(id -g)" docker compose -f "$tmp/dirty/compose.yaml" run --rm fallow --version
set +e
env "UID=$(id -u)" "GID=$(id -g)" docker compose -f "$tmp/dirty/compose.yaml" run --rm fallow dead-code --format json --quiet > "$tmp/dirty-compose.json"
dirty_code="$?"
env "UID=$(id -u)" "GID=$(id -g)" docker compose -f "$tmp/clean/compose.yaml" run --rm fallow dead-code --format json --quiet > "$tmp/clean-compose.json"
clean_code="$?"
set -e
test "$dirty_code" -eq 1
test "$clean_code" -eq 0
check:
name: Check
needs: changes
if: needs.changes.outputs.rust == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./.github/actions/setup-rust
with:
components: clippy, rustfmt
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
with:
version: 10.33.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- name: Install type-aware sidecar dependencies
run: npm ci --prefix tools/type-aware-sidecar --no-audit --no-fund --ignore-scripts
- name: Run tests
# Benchmark targets exceed this job's timeout and are covered by the
# dedicated benchmark workflows.
run: cargo test --workspace --lib --bins --tests --examples
- name: Run runtime-coverage integration tests (feature-gated stub sidecar)
run: cargo test -p fallow-cli --features test-sidecar-key --test runtime_coverage_tests
- name: Run schema drift gate (feature-gated schema-emit binary)
run: cargo test -p fallow-cli --features schema-emit --bin fallow-schema-emit
- name: Install contract bundle dependencies
run: cd editors/vscode && pnpm install --frozen-lockfile --ignore-scripts
- name: Run staged subgenerator integration tests
run: node --test scripts/subgenerator-staging.test.mjs
- name: Run contract bundle drift gate
run: CI=true npm run generate:contracts:check
- name: Clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Clippy (test-sidecar-key feature)
run: cargo clippy -p fallow-cli --features test-sidecar-key --all-targets -- -D warnings
- name: Clippy (schema-emit feature)
run: cargo clippy -p fallow-cli --features schema-emit --bin fallow-schema-emit --tests -- -D warnings
- name: Format
run: cargo fmt --all -- --check
- name: Install NAPI package dependencies
run: cd crates/napi && npm ci --omit=optional --ignore-scripts
- name: Build NAPI package
run: cd crates/napi && npx napi build --platform --profile napi-release --no-js && npm run publish:prepare
- name: Run NAPI smoke test
run: cd crates/napi && npm test
- name: Check for uncommitted changes
run: git diff --exit-code
windows-rust:
name: Windows path and subprocess handling
needs: changes
if: needs.changes.outputs.windows-rust == 'true'
runs-on: windows-latest
# The suite plus the platform clippy pass takes around 14 minutes of real
# work on a warm cache, so a 15-minute budget cancels the job during
# cache-save cleanup even though every step succeeded.
timeout-minutes: 25
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-rust
with:
components: clippy
cache-key: pr-windows-rust
- name: Test changed-file path handling
run: cargo test -p fallow-engine changed_files::tests
- name: Test churn path handling
run: cargo test -p fallow-engine churn::tests
- name: Test repository reference handling
run: cargo test -p fallow-engine repo_refs::tests
- name: Test source and manifest symlink containment
run: cargo test -p fallow-core symlink
- name: Test LSP diagnostic URI handling
run: cargo test -p fallow-lsp windows_initialization_publishes_uri_safe_diagnostics
- name: Test MCP subprocess cleanup
run: cargo test -p fallow-mcp completed_success_cleans_descendant_process_tree
- name: Test CLI subprocess cleanup
run: cargo test -p fallow-cli windows_job_object_terminates_descendants_without_taskkill_lookup
- name: Clippy platform-specific paths
run: cargo clippy -p fallow-cli -p fallow-core -p fallow-engine -p fallow-lsp -p fallow-mcp --all-targets -- -D warnings
skills-vendor:
name: Public skills contract
needs: changes
if: needs.changes.outputs.skills == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The released product contract lives in this repository. The companion
# skills repo packages it for agent hosts and may add host interface
# files plus the declared plugin frontmatter transform.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: fallow-rs/fallow-skills
path: .fallow-skills-src
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- name: Check public skill matches the Fallow source contract
env:
FALLOW_SKILLS_DIR: ${{ github.workspace }}/.fallow-skills-src
run: node scripts/vendor-skills.mjs --check
doc:
name: Documentation
needs: changes
if: needs.changes.outputs.rust == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./.github/actions/setup-rust
- name: Check documentation
env:
RUSTDOCFLAGS: "-D warnings"
run: cargo doc --workspace --no-deps --document-private-items
typos:
name: Typos
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1
- name: Hidden unicode
run: python3 scripts/scan-hidden-unicode.py --mode committed
- name: Hidden-unicode scanner self-test
run: bash scripts/test-scan-hidden-unicode.sh
js-lint:
name: JS Lint and Format
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Check out public documentation contract
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: fallow-rs/docs
path: .companion/fallow-docs
persist-credentials: false
- name: Check out public skill contract
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: fallow-rs/fallow-skills
path: .companion/fallow-skills
persist-credentials: false
- name: Documentation discoverability
uses: Hedde/trigger_tree@78a801629f0b11f403737e0ae9e5c2bfa827a6bb # v1.23.2
with:
min-score: "100"
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- run: npm ci --no-audit --no-fund --ignore-scripts
- name: oxlint
run: npm run lint:js
- name: oxfmt --check
run: npm run fmt:js:check
- name: Narrator comment guard
run: node scripts/check-comment-quality.mjs --all
- name: Script tests
run: node --test scripts/*.test.mjs
- name: Install type-aware sidecar dependencies
run: npm ci --prefix tools/type-aware-sidecar --no-audit --no-fund --ignore-scripts
- name: Type-aware sidecar tests
run: node --test tools/type-aware-sidecar/test/*.test.mjs
- name: Type-aware sidecar package check
working-directory: tools/type-aware-sidecar
run: npm pack --dry-run --json
- name: Knowledge architecture
run: npm run check:knowledge-architecture
- name: Cross-repository telemetry documentation parity
env:
FALLOW_DOCS_DIR: ${{ github.workspace }}/.companion/fallow-docs
FALLOW_SKILLS_DIR: ${{ github.workspace }}/.companion/fallow-skills
run: python3 scripts/check_telemetry_doc_sync.py
- name: Agent adapter drift
run: npm run check:agent-adapters
npm-package:
name: npm Package
needs: changes
if: needs.changes.outputs.npm-package == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- name: Install locked repository tools
run: npm ci --no-audit --no-fund --ignore-scripts
- name: Validate bundled Agent Skill
run: npx --no-install intent validate npm/fallow/skills
- name: Check npm package contents
run: |
cp schema.json npm/fallow/schema.json
# Document the cp invariant: npm/fallow/schema.json is the root
# schema.json verbatim. A future refactor that drops or moves the
# cp will fail loud here instead of silently shipping a stale
# schema to npm consumers. Refs #440.
diff schema.json npm/fallow/schema.json
npm pack --dry-run --json ./npm/fallow > /tmp/fallow-pack.json
node <<'NODE'
const fs = require('fs');
const [pack] = JSON.parse(fs.readFileSync('/tmp/fallow-pack.json', 'utf8'));
const files = new Set(pack.files.map((file) => file.path));
const required = [
'package.json',
'README.md',
'skills/fallow/SKILL.md',
'skills/fallow/references/cli-reference.md',
'skills/fallow/references/gotchas.md',
'skills/fallow/references/patterns.md',
'schema.json',
'capabilities.json',
'issue-registry.json',
];
const missing = required.filter((file) => !files.has(file));
if (missing.length > 0) {
throw new Error(`npm package is missing expected files: ${missing.join(', ')}`);
}
NODE
- name: Test release packaging gate
run: node --test .github/scripts/verify-pack-contents.test.mjs .github/scripts/update-dockerfile-pins.test.mjs
- name: Run npm wrapper unit tests
run: npm --prefix npm/fallow test
- name: Smoke that no postinstall runs on install
# Phase 2 of RFC 868 will block postinstall hooks by default. fallow
# must not regress to depending on postinstall. Pack the candidate
# tarball and install with --ignore-scripts; confirm the bin wrappers
# land and the postinstall file is gone.
shell: bash
run: |
set -euo pipefail
mkdir -p /tmp/fallow-tarball
npm pack ./npm/fallow --pack-destination /tmp/fallow-tarball > /tmp/fallow-pack.log
shopt -s nullglob
tarballs=(/tmp/fallow-tarball/fallow-*.tgz)
if [ ${#tarballs[@]} -eq 0 ]; then
echo "::error::npm pack produced no tarball"
exit 1
fi
TARBALL="${tarballs[0]}"
mkdir -p /tmp/fallow-smoke && cd /tmp/fallow-smoke
printf '%s' '{"name":"fallow-smoke","version":"0.0.0","private":true}' > package.json
# Skip the optional platform-binary fetch: the smoke checks install
# layout, not binary exec. Avoids unnecessary network traffic and
# registry rate-limit risk on busy CI windows.
npm install --ignore-scripts --omit=optional "$TARBALL" 2>&1 | tee /tmp/fallow-install.log
# Match only npm's lifecycle-script announcement for fallow itself
# (`> fallow@<version> postinstall`), not the substring "postinstall"
# in transitive-dep announcements.
if grep -qE "^> fallow@[0-9].*(postinstall|preinstall|install)" /tmp/fallow-install.log; then
echo "::error::npm install ran an install lifecycle script for fallow; the package must be zero-script after RFC 868 prep"
exit 1
fi
test -x node_modules/fallow/bin/fallow
test -x node_modules/fallow/bin/fallow-lsp
test -x node_modules/fallow/bin/fallow-mcp
test -x node_modules/.bin/fallow-lsp
test -x node_modules/.bin/fallow-mcp
PLATFORM_DIR=node_modules/@fallow-cli/linux-x64-gnu
mkdir -p "$PLATFORM_DIR"
printf '%s' '{"name":"@fallow-cli/linux-x64-gnu","version":"3.5.0"}' > "$PLATFORM_DIR/package.json"
cat > "$PLATFORM_DIR/fallow" <<'NODE'
#!/usr/bin/env node
require('node:fs').writeFileSync(process.env.FALLOW_TEST_ARGS, JSON.stringify(process.argv.slice(2)));
NODE
chmod +x "$PLATFORM_DIR/fallow"
FALLOW_SKIP_BINARY_VERIFY=1 FALLOW_TEST_ARGS=/tmp/fallow-lsp-args.json node_modules/.bin/fallow-lsp --stdio
FALLOW_SKIP_BINARY_VERIFY=1 FALLOW_TEST_ARGS=/tmp/fallow-mcp-args.json node_modules/.bin/fallow-mcp --transport stdio
node - <<'NODE'
const fs = require('node:fs');
const assert = require('node:assert/strict');
assert.deepEqual(JSON.parse(fs.readFileSync('/tmp/fallow-lsp-args.json', 'utf8')), ['lsp-server', '--stdio']);
assert.deepEqual(JSON.parse(fs.readFileSync('/tmp/fallow-mcp-args.json', 'utf8')), ['mcp-server', '--transport', 'stdio']);
NODE
if [ -f node_modules/fallow/scripts/postinstall.js ]; then
echo "::error::postinstall.js still ships in the npm tarball; remove from npm/fallow/scripts/"
exit 1
fi
test -f node_modules/fallow/scripts/lazy-verify.js
test -f node_modules/fallow/scripts/sentinel-path.js
test -f node_modules/fallow/scripts/run-binary.js
action-current:
name: Action with current binary
needs: changes
if: needs.changes.outputs.action-current == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: ./.github/actions/setup-rust
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- name: Build current fallow binary
run: cargo build --bin fallow
- name: Put current fallow binary first on PATH
run: printf '%s\n' "${GITHUB_WORKSPACE}/target/debug" >> "$GITHUB_PATH"
- name: Run Action shell tests with current binary
env:
FALLOW_BIN: ${{ github.workspace }}/target/debug/fallow
run: bash action/tests/run.sh
- name: Run checked-in Action JSON smoke
id: current_action
uses: ./
env:
FALLOW_BIN: ${{ github.workspace }}/target/debug/fallow
FALLOW_SKIP_BINARY_VERIFY: "1"
with:
command: dead-code
root: tests/fixtures/basic-project
format: json
annotations: false
fail-on-issues: false
auto-changed-since: false
- name: Verify current-binary JSON contract
env:
RESULTS_PATH: ${{ steps.current_action.outputs.results }}
run: |
test -f "$RESULTS_PATH"
jq empty "$RESULTS_PATH"
jq -e '.total_issues >= 0 and (.unused_files | type == "array")' "$RESULTS_PATH"
fallow-self-analyze:
name: Fallow Self Analysis
needs: changes
if: needs.changes.outputs.self-analyze == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 2
- uses: ./.github/actions/setup-rust
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
with:
version: 11.10.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
cache-dependency-path: editors/vscode/pnpm-lock.yaml
- name: Install VS Code extension dependencies
run: cd editors/vscode && pnpm install --frozen-lockfile --ignore-scripts
- name: Build fallow binary
run: cargo build --bin fallow
- name: Dead-code dogfood VS Code extension
run: ./target/debug/fallow dead-code --root editors/vscode --format json --quiet
- name: Health dogfood VS Code extension
run: ./target/debug/fallow health --root editors/vscode --format json --quiet --score --targets --top 10 --report-only
- name: Duplication dogfood VS Code extension
run: ./target/debug/fallow dupes --root editors/vscode --format json --quiet --top 10
- name: Dead-code dogfood npm wrapper package
run: ./target/debug/fallow dead-code --root npm/fallow --format json --quiet
- name: Health dogfood npm wrapper package
run: ./target/debug/fallow health --root npm/fallow --format json --quiet --score --targets --top 10 --report-only
- name: Duplication dogfood npm wrapper package
run: ./target/debug/fallow dupes --root npm/fallow --format json --quiet --top 10
- name: Audit dogfood smoke
run: |
base=HEAD
if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
base=HEAD~1
fi
set +e
./target/debug/fallow audit --base "$base" --format json --quiet > /tmp/fallow-self-audit.json
code=$?
set -e
test -s /tmp/fallow-self-audit.json
if [ "$code" -gt 1 ]; then
exit "$code"
fi
vscode:
name: VS Code Extension
needs: changes
if: needs.changes.outputs.vscode == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: ./.github/actions/setup-rust
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6
with:
version: 11.10.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
cache: pnpm
cache-dependency-path: editors/vscode/pnpm-lock.yaml
- name: Cache VS Code test download
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v5
with:
path: /tmp/fallow-vscode-test-cache
key: vscode-test-${{ runner.os }}-${{ runner.arch }}-vscode-1.96.0
- run: cd editors/vscode && pnpm install --frozen-lockfile --ignore-scripts
- run: cd editors/vscode && pnpm audit --prod
- name: Verify generated extension contracts
run: cd editors/vscode && pnpm run check:contracts
- run: cd editors/vscode && pnpm lint
- name: Build current multicall binary
run: cargo build -p fallow-multicall --bin fallow-multicall
- name: Run VS Code extension-host integration tests
run: cd editors/vscode && xvfb-run -a pnpm test:integration
- name: Run VS Code real CLI and LSP contract smoke
env:
FALLOW_BIN: ${{ github.workspace }}/target/debug/fallow-multicall
run: xvfb-run -a pnpm --dir editors/vscode run test:integration:real
- run: cd editors/vscode && pnpm test:unit
- run: cd editors/vscode && pnpm package
zed:
name: Zed Extension
needs: changes
if: needs.changes.outputs.zed == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./.github/actions/setup-rust
with:
components: rustfmt
targets: wasm32-wasip2
- name: Run tests
run: cargo test --manifest-path editors/zed/Cargo.toml
- name: Build wasm extension
run: cargo build --target wasm32-wasip2 --manifest-path editors/zed/Cargo.toml
- name: Format
run: cargo fmt --check --manifest-path editors/zed/Cargo.toml
test-gitlab-ci:
name: Test GitLab CI scripts
needs: changes
if: needs.changes.outputs.ci-scripts == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Run GitLab CI jq unit tests
run: bash ci/tests/run.sh
audit:
name: Security Audit
needs: changes
if: needs.changes.outputs.rust == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0
with:
tool: cargo-audit
# RUSTSEC-2026-0097: `rand` 0.9.2 unsoundness with a custom logger via
# `rand::rng()`. Reaches the tree only through the `proptest` dev-dependency
# (test-only, never shipped in a fallow binary) and fallow does not use the
# affected pattern; no patched `rand` release exists. cargo-deny (the primary
# advisory gate, deny.toml) already treats this informational/unsound advisory
# as non-blocking. Remove once a fixed `rand` ships or proptest drops it.
- run: cargo audit --ignore RUSTSEC-2026-0097
deny:
name: Cargo Deny
needs: changes
if: needs.changes.outputs.rust == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2
shear:
name: Unused Dependencies
needs: changes
if: needs.changes.outputs.rust == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: taiki-e/install-action@a6b2e2dcd845ddd7f509ce4f3ed3d922b80cc5d9 # v2.84.0
with:
tool: cargo-shear
- run: cargo shear
zizmor:
name: Actions Security
needs: changes
if: needs.changes.outputs.actions == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
enable-cache: false
# Scope to repo's own workflows + composite action. tests/fixtures/ contains
# intentionally lax CI YAML used as fallow analysis fixtures, not real workflows.
# Pin zizmor so a new release adding/strengthening an audit cannot redden
# CI on main with no fallow change (v1.26.1 added the adhoc-packages audit,
# suppressed for ci.yml/release.yml in .github/zizmor.yml). Bump deliberately.
- run: uvx zizmor@1.26.1 --config .github/zizmor.yml --min-confidence medium --format plain .github/ action.yml
msrv:
name: MSRV (1.92)
needs: changes
if: needs.changes.outputs.rust == 'true' || github.event_name == 'push' || github.event_name == 'merge_group'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
env:
RUSTUP_TOOLCHAIN: 1.92.0
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ./.github/actions/setup-rust
with:
toolchain: '1.92.0'
cache-key: msrv
- name: Verify published crate MSRV metadata
run: |
cargo metadata --no-deps --format-version 1 | jq -e --arg msrv "1.92" '
[.packages[]
| select((.publish | type) == "null" or (.publish | length) > 0)
| .rust_version]
| length > 0 and all(. == $msrv)
'
- name: Check compilation
run: cargo check --workspace
miri:
name: Miri
needs: changes
# PR-only and path-filtered to crates/{graph,extract,types,config}: catches contributor PRs
# that add tempdir / fs-IO tests without inheriting #[cfg_attr(miri, ignore)] from sibling
# tests. Does not run on push events (the .githooks/pre-push hook covers that locally for
# maintainers who opt in via `git config core.hooksPath .githooks`).
if: (needs.changes.outputs.miri == 'true' && github.event_name == 'pull_request') || github.event_name == 'merge_group'
runs-on: ubuntu-latest
# 60, not 45: the fallow-extract step shares this budget with types+graph and
# the React/JSX structural-extraction additions (larger ModuleInfo + per-file
# JSX harvesting) grew the extract suite's interpreter runtime past the old
# 45-min ceiling. cargo test passes fast; this is Miri-interpreter cost only.
timeout-minutes: 60
permissions:
contents: read
env:
MIRI_TOOLCHAIN: nightly-2026-03-20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Miri toolchain
run: |
rustup toolchain install "$MIRI_TOOLCHAIN" --component miri
cargo +"$MIRI_TOOLCHAIN" miri setup
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: miri
- name: fallow-types
run: cargo +"$MIRI_TOOLCHAIN" miri test -p fallow-types --lib --tests -- --skip proptests
- name: fallow-graph
run: cargo +"$MIRI_TOOLCHAIN" miri test -p fallow-graph --lib --tests -- --skip proptests
# Only suppress, CSS helpers, and visitor helpers run under Miri (pure data logic).
# Parser-heavy modules have no unsafe code and are too slow under Miri interpretation.
#
# Tree Borrows for this crate only: the css analytics path parses via
# lightningcss (alpha), whose @layer string handling reconstructs a &str
# from raw parts in a way Miri's Stacked Borrows flags as a benign
# (still-experimental-rules) violation. Tree Borrows models the same
# aliasing safety but accepts lightningcss's reconstructed-pointer pattern.
# types/graph stay on Stacked Borrows (default).
- name: fallow-extract
env:
MIRIFLAGS: -Zmiri-tree-borrows
run: |
cargo +"$MIRI_TOOLCHAIN" miri test -p fallow-extract --lib --tests css:: -- --skip sfc_css::
cargo +"$MIRI_TOOLCHAIN" miri test -p fallow-extract --lib --tests css_classes::
cargo +"$MIRI_TOOLCHAIN" miri test -p fallow-extract --lib --tests css_metrics::
cargo +"$MIRI_TOOLCHAIN" miri test -p fallow-extract --lib --tests suppress::
cargo +"$MIRI_TOOLCHAIN" miri test -p fallow-extract --lib --tests visitor::helpers::
viz-frontend:
name: Viz frontend
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.viz == 'true'
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '22'
- run: npm ci
working-directory: viz-frontend
- run: npm run lint
working-directory: viz-frontend
- run: npm run test
working-directory: viz-frontend
- run: npm run build
working-directory: viz-frontend
- name: Bundle drift
run: git diff --exit-code crates/cli/viz-assets
ci-ok:
name: CI
if: always()
needs: [check, windows-rust, doc, typos, js-lint, npm-package, action-current, fallow-self-analyze, vscode, zed, test-gitlab-ci, audit, deny, shear, zizmor, msrv, miri, skills-vendor, viz-frontend]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions: {}
steps:
- name: All checks passed
run: |
results='${{ toJSON(needs.*.result) }}'
for result in $(echo "$results" | jq -r '.[]'); do
if [ "$result" != "success" ] && [ "$result" != "skipped" ]; then
echo "::error::Job failed with result: $result"
exit 1
fi
done