Skip to content

chore: scaffold standalone stackable-odbc-core repository #13

chore: scaffold standalone stackable-odbc-core repository

chore: scaffold standalone stackable-odbc-core repository #13

Workflow file for this run

---
name: Build and Test
permissions:
contents: read
on:
push:
branches:
- main
pull_request:
merge_group:
# Supersede in-flight runs on the same ref. Never cancel in a merge queue: a
# cancelled merge_group run reports failure and evicts the PR from the queue.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
RUST_TOOLCHAIN_VERSION: "1.95.0"
jobs:
# Formatting, clippy (which is what enforces the unwrap_used / unwrap_in_result
# / panic denies from Cargo.toml), cargo-deny and cargo-sort. This lives here
# rather than in its own workflow because `needs:` cannot cross workflows, and
# a lint gate the required check does not observe is not a gate.
pre-commit:
name: pre-commit
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# The cargo-test pre-commit hook links libodbc via odbc-sys.
- name: Install host dependencies
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
packages: unixodbc-dev
version: ubuntu-latest
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Install Rust ${{ env.RUST_TOOLCHAIN_VERSION }} toolchain
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # 1.95.0
with:
toolchain: ${{ env.RUST_TOOLCHAIN_VERSION }}
components: rustfmt, clippy
- name: Setup Rust Cache
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: Install cargo-deny and cargo-sort
uses: taiki-e/install-action@97a5807a604e12de3a13b52d868ebecaeeea757c # v2.75.4
with:
tool: cargo-deny,cargo-sort
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
unit-tests:
name: Unit Tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
# One platform failing should not hide the others: the point of the
# matrix is to learn which platforms are broken, not just that one is.
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
# odbc-sys links against libodbc/libodbcinst, so the unixODBC dev
# libraries must be present to link the test binaries (no running Driver
# Manager is needed — only the libraries). Windows needs nothing: odbc32
# and odbccp32 ship with the platform SDK on the runner image.
- name: Install host dependencies (Linux)
if: runner.os == 'Linux'
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
packages: unixodbc-dev
version: ubuntu-latest
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: Install Rust ${{ env.RUST_TOOLCHAIN_VERSION }} toolchain
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # 1.95.0
with:
toolchain: ${{ env.RUST_TOOLCHAIN_VERSION }}
- name: Setup Rust Cache
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
# --locked so CI tests the dependency versions Cargo.lock pins and that
# cargo publish would use, and so a Cargo.toml change without a matching
# lockfile update fails here instead of drifting.
- name: Run unit tests
run: cargo test --locked
# The benchmark is its own crate (see bench/Cargo.toml), so nothing in
# the root build touches it and bench rot could otherwise merge unnoticed.
# harness = false means it is never run as a test either. Compile it; do
# not run it.
- name: Compile benchmarks
run: cargo build --locked --benches
working-directory: bench
miri:
name: Miri (undefined behaviour + leaks)
runs-on: ubuntu-latest
timeout-minutes: 30
needs: [unit-tests]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
# Miri requires a nightly toolchain; it cannot run on the pinned stable
# version used everywhere else in this workflow.
- name: Install nightly toolchain with Miri
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # nightly
with:
toolchain: nightly
components: miri
- name: Setup Rust Cache
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
key: miri
# stackable-odbc-core is pure Rust and holds all the raw-pointer
# marshalling, so it is where the undefined-behaviour risk lives.
#
# Proptests are skipped because they take hours under Miri; they run on
# stable in the unit-tests job.
#
# -Zmiri-disable-isolation is needed for the clock/filesystem access the
# test harness performs. Leak reporting is left ON: it is what catches
# handle and descriptor allocations that a teardown path forgets to free.
# `+nightly` is required: rust-toolchain.toml pins 1.95.0, and a bare
# `cargo miri` respects that file regardless of which toolchain was
# installed above.
- name: Run Miri
env:
MIRIFLAGS: -Zmiri-disable-isolation
run: cargo +nightly miri test --locked -p stackable-odbc-core --lib -- --skip proptest
fuzz:
name: Fuzz (ASAN smoke)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: [unit-tests]
steps:
# The fuzz binaries link stackable-odbc-core, which links libodbc.
- name: Install host dependencies
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.6.0
with:
packages: unixodbc-dev
version: ubuntu-latest
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
# cargo-fuzz builds with libFuzzer + AddressSanitizer, which require a
# nightly toolchain. The fuzz crate is its own Cargo workspace so the
# pinned stable root build never touches it.
- name: Install nightly toolchain
uses: dtolnay/rust-toolchain@b3b07ba8b418998c39fb20f53e8b695cdcc8de1b # nightly
with:
toolchain: nightly
# fuzz/ declares its own [workspace], so its build artifacts land in
# fuzz/target, not the root target/. Without this the cache stores an
# empty directory and every run rebuilds nightly + ASAN from scratch.
- name: Setup Rust Cache
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
key: fuzz
workspaces: "fuzz -> target"
- name: Install cargo-fuzz
uses: taiki-e/install-action@97a5807a604e12de3a13b52d868ebecaeeea757c # v2.75.4
with:
tool: cargo-fuzz
# A short smoke run per target: long enough to shake out an ASAN overrun
# in the pointer-marshalling paths, short enough for per-PR CI.
#
# --target is pinned to the gnu triple explicitly: newer cargo-fuzz
# defaults to x86_64-unknown-linux-musl, whose statically linked libc is
# incompatible with AddressSanitizer ("sanitizer is incompatible with
# statically linked libc"). gnu uses a dynamic libc and ships with the
# nightly toolchain.
- name: Fuzz utf16
run: cargo +nightly fuzz run utf16 --target x86_64-unknown-linux-gnu -- -max_total_time=30
- name: Fuzz column_value
run: cargo +nightly fuzz run column_value --target x86_64-unknown-linux-gnu -- -max_total_time=30
# Verifies the crate can actually be packaged, without publishing anything.
#
# `cargo package` is not covered by `cargo build`: it applies `exclude`,
# re-resolves the result as a standalone crate and compiles it from the
# tarball. That is what catches a source file excluded by accident, a
# declared-but-unpackaged target, or a path dependency with no version --
# each of which only shows up at publish time otherwise.
#
# `--locked` so a manifest change without a lockfile update fails here.
# Warnings are promoted to failures: the packaging step is short enough that
# a standing warning would be read as normal and hide the next one.
package:
name: Package (publish dry run)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install host dependencies
run: sudo apt-get update && sudo apt-get install -y unixodbc-dev
- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2
with:
key: package
# The log goes to RUNNER_TEMP, not the workspace: `cargo package` refuses
# to run against a dirty tree, so a log file written next to Cargo.toml
# is itself the uncommitted change that fails the step.
- name: Build the package and compile it from the tarball
run: |
set -o pipefail
cargo package --locked 2>&1 | tee "${RUNNER_TEMP}/package.log"
if grep -q '^warning' "${RUNNER_TEMP}/package.log"; then
echo "::error::cargo package emitted warnings; see above"
exit 1
fi
# The published tarball must carry the licence and the notice, and must
# not carry the toolchain pin, shipped, it pins consumers to this
# crate's Rust version.
- name: Check the tarball's contents
run: |
list=$(cargo package --locked --list)
for required in LICENSE NOTICE README.md CHANGELOG.md; do
grep -qx "$required" <<<"$list" || {
echo "::error::$required missing from the package"; exit 1; }
done
for forbidden in rust-toolchain.toml AGENTS.md CLAUDE.md; do
if grep -qx "$forbidden" <<<"$list"; then
echo "::error::$forbidden must not be published"; exit 1
fi
done
echo "$list"
# Single required check for branch protection rules.
finished:
name: Finished Build and Test
if: always()
needs:
- pre-commit
- unit-tests
- miri
- fuzz
- package
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Derived from needs.* rather than a hand-written list of job names: a job
# added to `needs` above but forgotten here would otherwise be silently
# non-blocking, which is exactly how the lint gate went unenforced.
- name: Check job results
env:
RESULTS: ${{ join(needs.*.result, ' ') }}
run: |
for result in $RESULTS; do
if [[ "$result" != "success" ]]; then
echo "One or more jobs did not succeed: $RESULTS"
exit 1
fi
done
echo "All jobs passed: $RESULTS"