Skip to content

descriptor: reject empty enums #1331

descriptor: reject empty enums

descriptor: reject empty enums #1331

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
# Run required checks on merge queue groups so queued PRs can land.
merge_group:
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
# protoc v27+ is required for editions 2023 support (utf8_validation.proto).
# Ubuntu's apt protobuf-compiler is v21.12 — too old. Keep this in sync
# with the conformance tools image (see conformance/Dockerfile.tools).
PROTOC_VERSION: "33.5"
# SHA256 of protoc-${PROTOC_VERSION}-linux-x86_64.zip from the upstream
# protobuf release. Bump together with PROTOC_VERSION.
PROTOC_SHA256: "24e58fb231d50306ee28491f33a170301e99540f7e29ca461e0e80fd1239f8d1"
jobs:
# ── Fast feedback: lint + test on stable ─────────────────────────────────
lint-and-test:
runs-on: ubuntu-latest
# 15 rather than 10: documenting the workspace (buffa-test's generated
# modules are large) adds a few minutes on top of a cold-cache build.
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
# Pinned (not @stable) so rustfmt/clippy match rust-toolchain.toml and
# local runs agree with CI. Bump together with that file.
#
# NOTE: the action only runs `rustup default`, which rust-toolchain.toml
# overrides — so the file is what actually selects the toolchain for
# cargo. The pin here must NAME THE SAME VERSION as the file so the
# components installed by this step belong to the toolchain cargo uses.
- uses: dtolnay/rust-toolchain@1.95
with:
components: clippy, rustfmt
- name: Install protoc
run: |
curl -fsSL "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip" -o /tmp/protoc.zip
echo "${PROTOC_SHA256} /tmp/protoc.zip" | sha256sum -c -
sudo unzip -o /tmp/protoc.zip -d /usr/local
protoc --version
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 (sha-pinned)
- name: Rustfmt
run: cargo fmt --all --check
- name: Clippy
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Tests
run: cargo test --workspace
# Optional features must keep compiling — #88 was a feature-flag-only
# break that escaped the default-features test pass above. cargo check
# is sufficient to catch compile breaks without doubling the test time.
- name: Check all features
run: cargo check --workspace --all-features
# Rustdoc warnings are errors, in generated docs too: buffa-test's
# build include!s codegen output (views, lazy views, json, reflect), so
# documenting the workspace lints emitted doc comments as well as
# hand-written ones. Covers workspace members only — excluded crates
# (benchmarks/, most examples/) are not documented. (#295)
- name: Rustdoc (deny warnings)
run: cargo doc --workspace --all-features --no-deps
env:
RUSTDOCFLAGS: -D warnings
# `gate_impls_on_crate_features` codegen output: generate the buffa-test
# fixture protos with the gate on, then `cargo check` the result with
# each `{json, views, text}` subset. Catches name-resolution bugs the
# string-match tests cannot — a `cfg`-gated item referenced from an
# ungated context survives `syn::parse_file` but fails this. Marked
# `--ignored` so the default workspace test stays fast. (#113)
- name: Feature-gating compile matrix
run: cargo test -p buffa-codegen --test feature_gating_compile -- --ignored
# Shared-pool mode emits each package's descriptor_pool() as a relative
# `super::…::__buffa_fds` path, so the hop count has to match the module
# nesting exactly. Only a real compiled tree proves it resolves; the
# string-level tests check the arithmetic against itself. Also `#[ignore]`d
# because it spawns cargo. (#311)
- name: Shared descriptor pool compile
run: cargo test -p buffa-codegen --test shared_pool_compile -- --ignored
# ── Markdown lint ────────────────────────────────────────────────────────
# Same pinned markdownlint-cli version as `task lint-md`. Node is
# preinstalled on ubuntu-latest so no setup-node step is needed.
lint-markdown:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- run: npx --yes markdownlint-cli@0.47.0 '**/*.md' --ignore target --ignore .claude
# ── Workflow lint ────────────────────────────────────────────────────────
# A malformed workflow does not fail a normal CI job — GitHub records a
# startup failure on the run instead, which is easy to miss and only surfaces
# when the workflow was meant to do something. A duplicate `env:` key reached
# main in publish-crates.yml exactly that way: PyYAML's default loader accepts
# duplicate keys and keeps the last one, so it validated locally while the
# publish workflow silently could not start. actionlint parses the way Actions
# does and rejects it.
lint-workflows:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Install actionlint
env:
ACTIONLINT_VERSION: 1.7.7
# SHA256 of actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz from
# the upstream release's checksums.txt. Bump together with the
# version.
ACTIONLINT_SHA256: 023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757
run: |
curl -fsSL -o /tmp/actionlint.tar.gz \
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
echo "${ACTIONLINT_SHA256} /tmp/actionlint.tar.gz" | sha256sum -c -
mkdir -p "${RUNNER_TEMP}/bin"
tar -xzf /tmp/actionlint.tar.gz -C "${RUNNER_TEMP}/bin" actionlint
echo "${RUNNER_TEMP}/bin" >> "${GITHUB_PATH}"
"${RUNNER_TEMP}/bin/actionlint" --version
- name: Lint workflows
run: actionlint -color
# ── Publish list ─────────────────────────────────────────────────────────
# Adding a publishable crate to the workspace without adding it to
# .github/publish-order.txt means the release silently skips it. That is how
# buffa-remote-derive was announced in the 0.9.0 changelog and never reached
# crates.io. The publish workflow checks this too, but only on a v* tag —
# running it here fails the PR that introduces the crate instead.
check-publish-list:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
# cargo metadata only reads manifests, so the pinned dev toolchain is not
# needed; stable avoids downloading it just for this check.
- name: Check publish coverage
env:
RUSTUP_TOOLCHAIN: stable
run: ./scripts/check-publish-coverage.sh
# ── Changelog check ──────────────────────────────────────────────────────
# CHANGELOG.md is generated from .changes/ via changie. Fail if it has been
# edited directly or is stale relative to the per-version source files.
check-changelog:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- name: Install changie
env:
CHANGIE_VERSION: 1.24.2
# SHA256 of changie_${CHANGIE_VERSION}_linux_amd64.tar.gz from the
# upstream release's checksums.txt. Bump together with the version.
CHANGIE_SHA256: 31535a9d8dc548d6d8f315762bfd5b1fba34e707b7600748c8bb8a609649007d
run: |
curl -fsSL -o /tmp/changie.tar.gz \
"https://github.com/miniscruff/changie/releases/download/v${CHANGIE_VERSION}/changie_${CHANGIE_VERSION}_linux_amd64.tar.gz"
echo "${CHANGIE_SHA256} /tmp/changie.tar.gz" | sha256sum -c -
mkdir -p "${RUNNER_TEMP}/bin"
tar -xzf /tmp/changie.tar.gz -C "${RUNNER_TEMP}/bin" changie
echo "${RUNNER_TEMP}/bin" >> "${GITHUB_PATH}"
"${RUNNER_TEMP}/bin/changie" --version
- name: Verify CHANGELOG.md is in sync
run: |
changie merge
# The message below is single-quoted on purpose: its backticks are
# literal markdown for the reader, not command substitution.
# shellcheck disable=SC2016
git diff --exit-code -- CHANGELOG.md || {
echo '::error file=CHANGELOG.md::CHANGELOG.md is out of sync. Do not edit it directly — add a fragment via `task changelog-new` (see CONTRIBUTING.md), or run `task changelog-merge` if you edited a .changes/ file.'
exit 1
}
# ── MSRV check ───────────────────────────────────────────────────────────
msrv-check:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
# The checked-in Cargo.lock is written by the pinned dev toolchain and
# may resolve transitive deps (e.g. getrandom, clap_lex) to versions
# whose manifests cargo 1.75 cannot parse (edition2024). Regenerate with
# stable cargo's MSRV-aware resolver so transitive deps satisfy
# rust-version = 1.75; cargo writes a v3 lockfile because the workspace
# rust-version is below 1.78.
- name: Regenerate MSRV-compatible lockfile
env:
CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS: fallback
run: rm -f Cargo.lock && cargo generate-lockfile
- uses: dtolnay/rust-toolchain@master
with:
toolchain: '1.75'
- name: Install protoc
run: |
curl -fsSL "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip" -o /tmp/protoc.zip
echo "${PROTOC_SHA256} /tmp/protoc.zip" | sha256sum -c -
sudo unzip -o /tmp/protoc.zip -d /usr/local
protoc --version
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 (sha-pinned)
# rust-toolchain.toml pins the dev toolchain and overrides `rustup
# default`, so RUSTUP_TOOLCHAIN is set explicitly to outrank the file.
- name: Check (MSRV 1.75)
env:
RUSTUP_TOOLCHAIN: '1.75'
run: cargo check --workspace --all-targets --locked
# ── Miri: proves the `set_len`-publishing `unsafe` reads no uninit memory ──
# SizeCache stores its inline slots as `MaybeUninit<u32>` to avoid zeroing the
# whole array on every encode; `consume_next` reads them via `assume_init`.
# `copy_into_spare` (types.rs) fills a Vec's spare capacity chunk by chunk and
# publishes the length afterwards, including on the unwind paths its tests
# drive. Miri tracks per-byte init state, so this job mechanically verifies
# both invariants on every path those two test modules exercise. Scoped to
# them to keep the (interpreted, slow) run fast — under a minute steady state,
# so it runs on every PR as a blocking gate rather than on a nightly schedule.
#
# The nightly is PINNED (not floating): a required check must not be broken by
# an unrelated bad nightly. Bump MIRI_TOOLCHAIN occasionally; the sysroot
# cache key follows it. `cargo +<toolchain>` overrides rust-toolchain.toml.
miri:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
MIRI_TOOLCHAIN: nightly-2026-02-27
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.MIRI_TOOLCHAIN }}
components: miri
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 (sha-pinned)
# The Miri std sysroot (~100 MB) is the only slow part of a cold run
# (~1-3 min to build); rust-cache does not cover it. Cache it keyed on the
# pinned toolchain so steady-state runs skip the build.
- name: Cache Miri sysroot
uses: actions/cache@v4
with:
path: ~/.cache/miri
key: miri-sysroot-${{ runner.os }}-${{ env.MIRI_TOOLCHAIN }}
- name: Miri setup (build/restore sysroot)
run: cargo +${{ env.MIRI_TOOLCHAIN }} miri setup
- name: Miri (size_cache and copy_into_spare soundness)
run: cargo +${{ env.MIRI_TOOLCHAIN }} miri test -p buffa -- size_cache copy_into_spare
# OwnedView: the view borrows from the Bytes it is stored beside, with
# forged 'static lifetimes hidden behind a MaybeDangling wrapper. Miri
# turns a wrong field order (a dangling borrow while the view's
# destructor runs), a second drop of the view on the into_bytes unwind
# path, or a missing MaybeDangling (a protector on the forged borrows
# when an OwnedView is passed by value) into a hard error.
# `owned_view_lifetime_parametric_contract` adds the Debug, Clone,
# PartialEq, reborrow and to_owned_message paths on a hand-written view.
- name: Miri (OwnedView soundness)
run: >-
cargo +${{ env.MIRI_TOOLCHAIN }} miri test -p buffa --
owned_view_drop owned_view_into_bytes owned_view_lifetime_parametric_contract
# ── no_std and 32-bit compilation checks ─────────────────────────────────
check-nostd:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
# Must name the same version as rust-toolchain.toml (which selects the
# toolchain cargo uses) so the cross-targets installed here belong to
# that toolchain. With @stable, the targets land on a toolchain the
# file then bypasses, and the bare-metal check fails with E0463.
- uses: dtolnay/rust-toolchain@1.95
with:
targets: i686-unknown-linux-gnu, thumbv7em-none-eabihf
- name: Install protoc
run: |
curl -fsSL "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip" -o /tmp/protoc.zip
echo "${PROTOC_SHA256} /tmp/protoc.zip" | sha256sum -c -
sudo unzip -o /tmp/protoc.zip -d /usr/local
protoc --version
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 (sha-pinned)
- name: Check buffa no_std (host)
run: cargo check -p buffa --no-default-features
- name: Check buffa no_std (bare-metal ARM)
run: cargo check -p buffa --no-default-features --target thumbv7em-none-eabihf
- name: Check buffa-types no_std
run: cargo check -p buffa-types --no-default-features
- name: Check buffa-types no_std + chrono
run: cargo check -p buffa-types --no-default-features --features chrono
- name: Check buffa-descriptor no_std
run: cargo check -p buffa-descriptor --no-default-features
- name: Check workspace 32-bit
run: cargo check --workspace --target i686-unknown-linux-gnu
# ── Verify checked-in generated code is up to date ───────────────────────
check-generated-code:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install protoc
run: |
curl -fsSL "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip" -o /tmp/protoc.zip
echo "${PROTOC_SHA256} /tmp/protoc.zip" | sha256sum -c -
sudo unzip -o /tmp/protoc.zip -d /usr/local
protoc --version
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 (sha-pinned)
- name: Regenerate bootstrap descriptor types
# Proto sources are vendored in buffa-descriptor/protos/ (pinned),
# so this is independent of the installed protoc's bundled includes.
run: scripts/gen-bootstrap-types.sh
- name: Generate descriptor set (WKTs)
run: |
# WKT protos are vendored in buffa-types/protos (NOT read from
# /usr/local/include) so the checked-in code is pinned to a
# specific proto source, not whatever protoc version ships.
protoc --descriptor_set_out=/tmp/wkt.pb --include_imports \
--include_source_info \
-I buffa-types/protos \
google/protobuf/any.proto \
google/protobuf/api.proto \
google/protobuf/duration.proto \
google/protobuf/empty.proto \
google/protobuf/field_mask.proto \
google/protobuf/source_context.proto \
google/protobuf/struct.proto \
google/protobuf/timestamp.proto \
google/protobuf/type.proto \
google/protobuf/wrappers.proto
- name: Regenerate WKT types
run: cargo run -p buffa-codegen --bin gen_wkt_types -- /tmp/wkt.pb buffa-types/src/generated
- name: Check for differences
run: |
if ! git diff --exit-code buffa-descriptor/src/generated/ buffa-types/src/generated/; then
echo "::error::Checked-in generated code is stale. Run 'task gen-wkt-types' and/or regenerate bootstrap types, then commit."
exit 1
fi
# ── Conformance test suite ───────────────────────────────────────────────
# Builds conformance binaries on the runner (cached via rust-cache) and
# extracts the conformance_test_runner + proto files from the tools image.
conformance:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 (sha-pinned)
with:
workspaces: conformance
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 (sha-pinned)
- name: Build tools image (cached)
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 (sha-pinned)
with:
context: .
file: conformance/Dockerfile.tools
load: true
tags: buffa/tools:local
cache-from: type=gha,scope=tools-image
# Only main pushes save the cache. The cache service already
# isolates branches (a PR-written entry is not restorable from
# main), so this is quota hygiene plus defense in depth rather
# than the poisoning boundary itself: PR and merge-queue saves
# count against the repo's shared 10GB cache and can LRU-evict
# the main-scoped entries every PR restores from — merge_group
# writes land on an ephemeral queue ref nothing can ever restore.
# Cost: a PR that edits Dockerfile.tools rebuilds cold on every
# push instead of only the first; such PRs are rare.
cache-to: ${{ github.event_name == 'push' && 'type=gha,scope=tools-image,mode=max' || '' }}
- name: Extract tools from image
run: |
CID=$(docker create buffa/tools:local /dev/null)
docker cp "$CID:/conformance_test_runner" /usr/local/bin/conformance_test_runner
docker cp "$CID:/protoc" /usr/local/bin/protoc
docker cp "$CID:/protos/." conformance/protos/
docker rm "$CID"
chmod +x /usr/local/bin/conformance_test_runner /usr/local/bin/protoc
# Both builds use the same target dir so build-script artifacts
# (buffa-codegen, buffa-build, syn, etc.) are compiled only once.
# The std binary is copied out before the no_std build overwrites it.
- name: Determine host target
id: host
run: echo "triple=$(rustc -vV | grep '^host:' | cut -d' ' -f2)" >> "$GITHUB_OUTPUT"
- name: Build conformance binary (std)
run: >-
cargo build --release --manifest-path conformance/Cargo.toml
--target ${{ steps.host.outputs.triple }}
- name: Save std binary
run: cp conformance/target/${{ steps.host.outputs.triple }}/release/conformance /tmp/buffa-conformance-std
- name: Build conformance binary (no_std)
run: >-
cargo build --release --manifest-path conformance/Cargo.toml
--no-default-features --target ${{ steps.host.outputs.triple }}
- name: Run conformance tests (std)
run: >-
conformance_test_runner
--failure_list conformance/known_failures.txt
--maximum_edition 2024
/tmp/buffa-conformance-std
- name: Run conformance tests (no_std)
run: >-
conformance_test_runner
--failure_list conformance/known_failures_nostd.txt
--maximum_edition 2024
conformance/target/${{ steps.host.outputs.triple }}/release/conformance
- name: Run conformance tests (via-view)
env:
BUFFA_VIA_VIEW: "1"
run: >-
conformance_test_runner
--failure_list conformance/known_failures_view.txt
--maximum_edition 2024
/tmp/buffa-conformance-std
- name: Run conformance tests (via-lazy)
env:
BUFFA_VIA_LAZY: "1"
run: >-
conformance_test_runner
--failure_list conformance/known_failures_lazy.txt
--maximum_edition 2024
/tmp/buffa-conformance-std
- name: Run conformance tests (view-json)
env:
BUFFA_VIEW_JSON: "1"
run: >-
conformance_test_runner
--failure_list conformance/known_failures_view_json.txt
--maximum_edition 2024
/tmp/buffa-conformance-std
- name: Run conformance tests (via-reflect)
env:
BUFFA_VIA_REFLECT: "1"
run: >-
conformance_test_runner
--failure_list conformance/known_failures_reflect.txt
--maximum_edition 2024
/tmp/buffa-conformance-std
- name: Run conformance tests (via-vtable)
env:
BUFFA_VIA_VTABLE: "1"
run: >-
conformance_test_runner
--failure_list conformance/known_failures_view_vtable.txt
--maximum_edition 2024
/tmp/buffa-conformance-std