Skip to content

Order release publication channels #196

Order release publication channels

Order release publication channels #196

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
# Cancel superseded runs on the same ref.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
version:
name: Release version invariant
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: python3 tools/check-version.py
# ---------------------------------------------------------------------------
rust:
name: Rust workspace
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain (per rust-toolchain.toml)
run: |
rustup toolchain install
rustup component add rustfmt clippy
- uses: Swatinem/rust-cache@v2
- name: cargo fmt --check
run: cargo fmt --all -- --check
- name: cargo clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: cargo check
run: cargo check --all-targets --all-features --locked
- name: cargo test
run: cargo test --all-features --locked
- name: cargo build --release (smoke)
run: cargo build --release --locked
- name: coderef binary --version
run: ./target/release/coderef --version
- name: coderef config show <example>
run: ./target/release/coderef config show examples/minimal.coderef.jsonc > /dev/null
- name: coderef list — scan-self smoke
shell: bash
run: |
# Build a tiny temp workspace + config and verify the scanner
# discovers the planted reference.
tmp=$(mktemp -d)
cat > "$tmp/.coderef.jsonc" <<'EOF'
{ "patterns": { "todo": {
"regex": "TODO\\(@(?<user>\\w+)\\)",
"target": "https://x/${user}",
"scope": { "commentsOnly": true }
} } }
EOF
printf 'fn main() { // TODO(@ci) — proves the scanner runs\n}\n' > "$tmp/a.rs"
out=$(./target/release/coderef list "$tmp")
echo "$out"
echo "$out" | grep -q "TODO(@ci)" || { echo "scanner did not find planted reference"; exit 1; }
rm -rf "$tmp"
- name: coderef check — local-path verify smoke
shell: bash
run: |
# Plant a local docref to a real file and one to a missing
# file. Confirm the binary exits 1 (one broken), reports both
# correctly, and that --report json emits valid JSON.
tmp=$(mktemp -d)
cat > "$tmp/.coderef.jsonc" <<'EOF'
{ "patterns": { "docref": {
"regex": "DOCREF\\((?<path>/?[^)\\s#]+)\\)",
"kind": "local",
"target": "${path}",
"scope": { "commentsOnly": true }
} } }
EOF
mkdir -p "$tmp/docs"
echo "real" > "$tmp/docs/exists.md"
echo "// DOCREF(/docs/exists.md)" > "$tmp/a.rs"
echo "// DOCREF(/docs/missing.md)" > "$tmp/b.rs"
set +e
./target/release/coderef check "$tmp"
ec=$?
set -e
if [ "$ec" -ne 1 ]; then
echo "expected exit 1 (one broken ref), got $ec"
exit 1
fi
# And the JSON form must parse + report broken=1.
json=$(./target/release/coderef check --report json "$tmp" || true)
echo "$json" | python3 -c 'import json,sys; r=json.load(sys.stdin); assert r["broken"]==1 and r["ok"]==1, r'
rm -rf "$tmp"
- name: coderef doctor — static checks smoke
shell: bash
run: |
# Plant a config that triggers every v0.1 check category.
tmp=$(mktemp -d)
cat > "$tmp/.coderef.jsonc" <<'EOF'
{
"patterns": {
"ok": { "regex": "OK\\(@(?<u>\\w+)\\)", "target": "x/${u}" },
"noTarget": { "regex": "X" },
"unusedCap": { "regex": "(?<extra>X)", "target": "x" },
"badRegex": { "regex": "(?<u>X", "target": "x" },
"futureNs": { "regex": "X", "target": "x/${git:branch}" },
"configUnknown": { "regex": "X", "target": "${config:missingVar}" }
}
}
EOF
set +e
out=$(./target/release/coderef doctor --no-scan "$tmp")
ec=$?
set -e
echo "$out"
if [ "$ec" -ne 1 ]; then
echo "expected exit 1 (errors present), got $ec"
exit 1
fi
# Must produce at least one diagnostic of each expected kind.
for check in pattern.targetMissing pattern.regexInvalid pattern.captureUnused \
pattern.variableConfigUnknown pattern.variableNamespaceFuture; do
echo "$out" | grep -q "$check" || { echo "missing diagnostic: $check"; exit 1; }
done
json=$(./target/release/coderef doctor --no-scan --report json "$tmp" || true)
echo "$json" | python3 -c 'import json,sys; r=json.load(sys.stdin); assert r["errors"] >= 3, r'
rm -rf "$tmp"
- name: Self-dogfood — coderef doctor + check against this repo
shell: bash
run: |
# The project's own .coderef.jsonc must pass static checks +
# every TODO(@user) / DOCREF(/path) reference it contains must
# resolve. This is what the .pre-commit-config.yaml's
# coderef-doctor / coderef-check hooks enforce locally.
./target/release/coderef doctor --no-scan .
./target/release/coderef check .
- name: Regenerate JSON Schema from Rust types
run: cargo run --bin coderef-gen-schema --locked
- name: Verify committed schema matches generated schema
run: |
if ! git diff --exit-code -- schema/coderef.schema.json; then
echo ""
echo "schema/coderef.schema.json is out of date. Re-run:"
echo " cargo run --bin coderef-gen-schema"
echo "and commit the result."
exit 1
fi
# ---------------------------------------------------------------------------
wasm:
name: WASM build + Node smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain (per rust-toolchain.toml)
run: |
rustup toolchain install
rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
# Separate cache from the host-triple build above; same lockfile.
key: wasm-${{ hashFiles('Cargo.lock') }}
- name: Install wasm-pack
run: |
curl -fsSL https://rustwasm.github.io/wasm-pack/installer/init.sh | sh
wasm-pack --version
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: wasm-pack build (nodejs target)
run: wasm-pack build --release --target nodejs --out-dir pkg crates/coderef-core-wasm
- name: Node smoke — import + version + scan_buffer + doctor_static
shell: bash
run: |
# Tiny end-to-end script: load the wasm-pack output, run each
# exported function on representative input, and assert the
# results match what the host-triple tests already proved.
# The script lives in $GITHUB_WORKSPACE so the relative
# require() to the wasm-pack output dir resolves correctly.
cat > "$GITHUB_WORKSPACE/wasm-smoke.cjs" <<'EOF'
const m = require('./crates/coderef-core-wasm/pkg/coderef_core_wasm.js');
// 1. version()
const v = m.version();
if (typeof v !== 'string' || v.length === 0) {
console.error('version() returned bad value:', v);
process.exit(1);
}
console.log('version:', v);
// 2. parse_config() — exercises every field the extension
// might encounter (description, scope, severity) so a
// Pattern-struct change without a wasm-pack rebuild is
// caught here rather than at extension activation.
const cfgJsonc = `{
"patterns": {
"todo": {
"description": "GitHub @user mention inside a TODO marker",
"regex": "TODO\\\\(@(?<user>\\\\w+)\\\\)",
"target": "https://x/${'$'}{user}",
"scope": { "commentsOnly": true },
"severity": { "pattern.unused": "warning" }
}
}
}`;
const cfg = m.parse_config(cfgJsonc);
const cfgJson = JSON.stringify(cfg);
if (!cfgJson.includes('"todo"')) {
console.error('parse_config missing pattern:', cfgJson);
process.exit(1);
}
if (!cfgJson.includes('GitHub @user mention')) {
console.error('parse_config dropped description (WASM may be stale):', cfgJson);
process.exit(1);
}
// 3. scan_buffer() on a Rust-ish file
const content = '// TODO(@alice) — fix this';
const refs = m.scan_buffer(content, cfgJson, 'rs', 'test.rs');
if (!Array.isArray(refs) || refs.length !== 1) {
console.error('scan_buffer: expected 1 ref, got', refs);
process.exit(1);
}
const r = refs[0];
if (r.pattern_id !== 'todo' || r.captures.user !== 'alice' || r.target !== 'https://x/alice') {
console.error('scan_buffer: bad shape', r);
process.exit(1);
}
console.log('scan_buffer: ok (target=' + r.target + ')');
// 4. doctor_static() on a config with an error
const broken = JSON.stringify({ patterns: { x: { regex: 'X' } } }); // no target
const report = m.doctor_static(broken);
if (report.errors < 1) {
console.error('doctor_static: expected at least 1 error, got', report);
process.exit(1);
}
console.log('doctor_static: ok (errors=' + report.errors + ')');
// 5. explain_text() — drives the extension's
// coderef.explainReference command. The shape returned
// must include `matches` (array) and
// `non_matching_pattern_ids` (array).
const exp = m.explain_text(cfgJson, 'TODO(@bob)');
if (!Array.isArray(exp.matches) || exp.matches.length !== 1) {
console.error('explain_text: expected 1 match, got', exp);
process.exit(1);
}
if (exp.matches[0].target !== 'https://x/bob') {
console.error('explain_text: bad resolved target', exp.matches[0]);
process.exit(1);
}
console.log('explain_text: ok (target=' + exp.matches[0].target + ')');
EOF
(cd "$GITHUB_WORKSPACE" && node ./wasm-smoke.cjs)
rm -f "$GITHUB_WORKSPACE/wasm-smoke.cjs"
- name: WASM package size budget (< 2 MB)
run: |
# v0.1 budget: 2 MB. Current size hovers ~1.6 MB even with
# wasm-opt --enable-bulk-memory; the bulk is fancy-regex (its
# own regex engine) + schemars (drag from coderef-core's
# JsonSchema derives, only used by gen-schema in practice) +
# serde_json. Two follow-up paths to shrink: feature-gate
# `schemars` so the WASM build excludes it; consider regex-lite
# for the WASM target if the JS host doesn't need lookarounds.
# Either is a polish PR, not a v0.1 blocker.
wasm=$(ls crates/coderef-core-wasm/pkg/*.wasm | head -1)
size=$(wc -c < "$wasm")
echo "wasm size: $size bytes"
if [ "$size" -gt 2097152 ]; then
echo "WASM binary exceeded 2 MB budget"
exit 1
fi
# ---------------------------------------------------------------------------
extension:
name: VSCode extension (TS + VSIX)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Rust toolchain + wasm-pack are needed for `npm run build` /
# `vsce package`, which run scripts/bundle-wasm.cjs to produce the
# in-VSIX WASM artefacts (extension/out/wasm/*).
- name: Install Rust toolchain
run: |
rustup toolchain install
rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
key: extension-${{ hashFiles('Cargo.lock') }}
- name: Install wasm-pack
run: curl -fsSL https://rustwasm.github.io/wasm-pack/installer/init.sh | sh
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies (extension)
working-directory: extension
run: npm install --no-audit --no-fund
- name: tsc --noEmit
working-directory: extension
run: npx tsc --noEmit -p ./
- name: eslint
working-directory: extension
run: npx eslint src || true # v0.0.0 has no eslint config yet
- name: Unit tests (node --test, pure-function paths)
working-directory: extension
run: npm test
- name: Runtime tests (@vscode/test-electron in a real VSCode)
working-directory: extension
# @vscode/test-cli launches a real VSCode binary; on the
# ubuntu-latest runner that needs Xvfb because GitHub's image
# is headless.
run: xvfb-run -a npm run test-runtime
- name: Build (compile + bundle-wasm) and package VSIX
working-directory: extension
run: npm run package
- name: Verify VSIX contains the bundled wasm-pack output
working-directory: extension
shell: bash
run: |
# The bundle-wasm script must have copied the wasm-pack output
# into out/wasm/ before vsce zipped the package; the VSIX is
# just a zip so we can inspect it.
vsix=$(ls *.vsix | head -1)
echo "inspecting $vsix"
unzip -l "$vsix" | tee /tmp/vsix-contents.txt
for need in \
"extension/out/extension.js" \
"extension/out/wasm/coderef_core_wasm.js" \
"extension/out/wasm/coderef_core_wasm_bg.wasm"; do
grep -q "$need" /tmp/vsix-contents.txt || {
echo "VSIX is missing $need"
exit 1
}
done
# Source TS, scripts, and tests must NOT ship.
for forbid in \
"extension/src/" \
"extension/scripts/" \
"extension/out/test/" \
"wasmEngine.ts"; do
if grep -q "$forbid" /tmp/vsix-contents.txt; then
echo "VSIX contains $forbid but shouldn't"
exit 1
fi
done
# ---------------------------------------------------------------------------
npm-wrapper:
name: npm wrapper smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain (cargo for the source-build fallback)
run: rustup toolchain install
- uses: Swatinem/rust-cache@v2
with:
key: npm-wrapper-${{ hashFiles('Cargo.lock') }}
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: cargo build --release --bin coderef (sibling binary for install.js to discover)
run: cargo build --release --locked --bin coderef
- name: npm install (runs postinstall → sibling-binary path)
working-directory: npm/coderef
run: npm install --no-audit --no-fund
- name: Wrapper forwards args + exit codes
working-directory: npm/coderef
shell: bash
run: |
# Verify the wrapper found + installed the binary and that
# invoking it through the wrapper produces the same output
# as invoking it directly.
want=$(./bin/coderef --version)
have=$(node ./bin/coderef.js --version)
echo "wrapper: $have"
test "$want" = "$have" || { echo "version mismatch: $want vs $have"; exit 1; }
# Exit code propagation: unknown subcommand → exit 2.
set +e
node ./bin/coderef.js bogus-subcommand
code=$?
set -e
test $code -eq 2 || { echo "expected exit 2 for unknown subcommand, got $code"; exit 1; }
# ---------------------------------------------------------------------------
schema:
name: JSON Schema validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install jsonschema
run: python -m pip install --upgrade pip jsonschema
- name: Validate the schema parses as valid JSON
run: python -c "import json; json.load(open('schema/coderef.schema.json'))"
- name: Validate example configs against the schema
run: |
python3 tools/validate-config.py \
schema/coderef.schema.json \
examples/minimal.coderef.jsonc
# ---------------------------------------------------------------------------
docs:
name: Docs hygiene (table alignment + cross-refs)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Run table aligner (must be idempotent)
run: |
python3 tools/align-md-tables.py \
DESIGN.md README.md AGENTS.md CLAUDE.md \
schema/README.md npm/coderef/README.md extension/README.md docs/README.md
if ! git diff --exit-code -- '*.md' '**/*.md'; then
echo ""
echo "Tables in the above files are not aligned. Re-run:"
echo " python3 tools/align-md-tables.py DESIGN.md README.md ..."
exit 1
fi
- name: Verify §-cross-references in DESIGN.md
shell: bash
run: |
set -euo pipefail
headers=$(grep -oE '^#+ [0-9]+(\.[0-9]+)*' DESIGN.md | sed 's/^#* //')
refs=$(grep -oE '§[0-9]+(\.[0-9]+)*' DESIGN.md | sed 's/^§//' | sort -u)
missing=0
for r in $refs; do
if ! echo "$headers" | grep -qxE "$r"; then
echo "MISSING TARGET: §$r"
missing=1
fi
done
exit $missing
- name: Code-fence balance check
run: |
n=$(grep -c '^```' DESIGN.md)
if [ $((n % 2)) -ne 0 ]; then
echo "Unbalanced code fences in DESIGN.md: $n"
exit 1
fi
# Aggregating gate for branch protection: mark only `done` as the
# required check. Two parts:
#
# 1. Self-check: parse this file with `yq` and fail if any job
# other than `done` itself is absent from `needs:` below.
# Stops a newly added job from silently escaping the gate.
# 2. Failure aggregation: one-line `contains(needs.*.result, ...)`
# guard. GitHub's Actions UI already shows the per-job
# results, so an elaborate result-table step doesn't add
# anything reviewers can't see one click away.
done:
needs: [version, rust, wasm, extension, npm-wrapper, schema, docs]
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Ensure every job is wired into this gate
env:
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
# Parse the workflow with a real YAML reader (yq) and fail if
# any declared job (other than this gate) is absent from
# `needs` above, so a newly added job cannot silently escape
# the required gate.
declared="$(yq '.jobs | keys | .[]' .github/workflows/ci.yml |
grep -vx done | sort)"
wired="$(jq -r 'keys[]' <<<"${NEEDS_JSON}" | sort)"
missing="$(comm -23 <(printf '%s\n' "${declared}") <(printf '%s\n' "${wired}"))"
if [[ -n "${missing}" ]]; then
echo "Jobs declared in the workflow but missing from done.needs:"
while read -r job; do
echo " - ${job}"
echo "::error title=Gate is missing a job::${job} not in done.needs"
done <<<"${missing}"
exit 1
fi
echo "done covers all $(grep -c . <<<"${declared}") workflow jobs."
- name: Fail if any dependency did not succeed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1