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
106 changes: 95 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,19 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: zig build test-release-small -Doptimize=ReleaseSmall

# musl / fully-static Linux target. `-Dtarget` was never actually
# cross-compiled anywhere in CI before -- every lane built the native
# glibc triple. A `-linux-musl` target links a static musl libc, and
# such a binary runs natively on this glibc x86_64 runner, so this is
# real execution coverage (1076/1076 tests), not just a build check.
# Proves zzdds is musl-clean for Alpine / static-binary / container
# consumers. aarch64-linux-musl would need qemu to run and is deferred;
# a static-archive libzzdds bundle variant is a separate follow-up
# (see docs/roadmap.md "CI / Release Platform Coverage").
- name: Test (musl static, x86_64-linux)
if: matrix.os == 'ubuntu-latest'
run: zig build test -Dtarget=x86_64-linux-musl

# Binding smoke tests, mirroring ci.yml's test-other job: the release
# gate previously ran only `zig build test` (+ReleaseFast +TSan) and
# never built a single binding, so a PR breaking the C/C++/Java
Expand Down Expand Up @@ -263,10 +276,22 @@ jobs:
# pkgconfig + CMake package files) on each release platform and uploads it
# as a per-platform tarball; `publish` attaches them to the GitHub release.
#
# The *functional* smoke test of these libraries is the `test` job's
# `test-bindings` step, which compiles and runs real C/C++/Java programs
# against a freshly built libzzdds on every platform -- this job only has
# to confirm the install tree is complete before packaging it.
# Beyond confirming the tree is complete, this job (Linux + macOS) also
# extracts the finished tarball into an unrelated directory and drives a real
# downstream consume of it -- `find_package(ZZDDS)` + pkg-config, building
# examples/{c,cpp}/hello_world and a minimal consumer against the *relocated*
# prefix (scripts/verify_release_bundle.py). That catches broken CMake
# package files / pkg-config relocatability / rpath|install-name that the
# `test` job's in-tree `test-bindings` step (which points CMAKE_PREFIX_PATH
# straight at the live zig-out) cannot see.
#
# Windows gets the structural check only: the generated zzdds-config.cmake /
# zzdds.pc are POSIX-shaped today (search `lib/` for the shared lib, no
# IMPORTED_IMPLIB, `bin/zidl` not `bin/zidl.exe`), so `find_package(ZZDDS)`
# can't configure there yet. Making the generated package Windows-correct is
# tracked in docs/roadmap.md "CI / Release Platform Coverage"; until then the
# Windows tarball ships as-is (its libraries are still functionally covered by
# the `test` job's `test-bindings` step).
package-libs:
name: package-libs (${{ matrix.name }})
needs: [prepare, test]
Expand Down Expand Up @@ -330,6 +355,29 @@ jobs:
mv zig-out "$dir"
tar -czf "${dir}.tar.gz" "$dir"

- name: Install Python (for verify_release_bundle.py)
if: runner.os != 'Windows'
uses: actions/setup-python@v5
with:
python-version: "3.11"

# Extract the tarball we just made into an unrelated directory and prove a
# downstream project can consume it from there. Linux: full path --
# find_package + pkg-config, build examples/{c,cpp}/hello_world, run a
# pub/sub pair. macOS: same but --skip-example-run (the hello_world pair
# needs live UDP discovery, flaky on hosted macOS; cmake_consumer still
# links + runs against libzzdds.dylib, so install-name relocatability is
# still covered). Windows is skipped -- see the job-header comment.
- name: Verify prebuilt bundle is consumable
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
tarball="zzdds-${{ needs.prepare.outputs.full_version }}-${{ matrix.name }}.tar.gz"
args=""
[ "${{ runner.os }}" = "macOS" ] && args="--skip-example-run"
python3 scripts/verify_release_bundle.py --bundle "$tarball" $args

- name: Upload bundle
uses: actions/upload-artifact@v4
with:
Expand Down Expand Up @@ -380,21 +428,51 @@ jobs:
git tag "${TAG}"
git push origin "${TAG}"

# Release notes body: prefer the curated, date-headed CHANGELOG.md
# sections written since the previous release; fall back to raw commit
# subjects only if that yields nothing (e.g. CHANGELOG not updated).
# Either way, append a compare link to the full commit log.
- name: Generate changelog
id: changelog
env:
TAG: ${{ needs.prepare.outputs.tag }}
run: |
PREV_TAG=$(git describe --tags --abbrev=0 HEAD~1 2>/dev/null || echo "")

BODY=""
SOURCE=""
# extract_changelog.py emits the CHANGELOG.md sections added since
# PREV_TAG (by heading-set diff against CHANGELOG.md as of that tag;
# falls back to the leading date-headed run if the tag predates the
# file). An empty PREV_TAG is passed through and lands on that same
# fallback.
if SLICE=$(python3 scripts/extract_changelog.py --changelog CHANGELOG.md --prev-ref "$PREV_TAG"); then
# Demote CHANGELOG's own "## <date>" headings so they nest under
# the "## Changelog" heading in the release-notes template.
BODY=$(printf '%s\n' "$SLICE" | sed 's/^## /### /')
SOURCE="CHANGELOG.md since ${PREV_TAG:-<repo start>}"
fi

if [ -z "$BODY" ]; then
if [ -n "$PREV_TAG" ]; then
BODY=$(git log "${PREV_TAG}..HEAD~1" --pretty=format:"- %s" --no-merges)
else
BODY=$(git log HEAD~1 --pretty=format:"- %s" --no-merges)
fi
SOURCE="git log (CHANGELOG.md had no dated sections since the last release)"
fi

if [ -n "$PREV_TAG" ]; then
LOG=$(git log "${PREV_TAG}..HEAD~1" --pretty=format:"- %s" --no-merges)
else
LOG=$(git log HEAD~1 --pretty=format:"- %s" --no-merges)
BODY="${BODY}

**Full commit log:** https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${TAG}"
fi

echo "Release-notes body sourced from: ${SOURCE}"
{
echo "log<<EOF"
echo "$LOG"
echo "EOF"
echo "log<<CHANGELOG_EOF"
echo "$BODY"
echo "CHANGELOG_EOF"
} >> $GITHUB_OUTPUT

- name: Create GitHub release
Expand All @@ -406,9 +484,15 @@ jobs:
run: |
gh release create "${TAG}" \
--title "${TAG}" \
--notes "## Changes
--notes "## Changelog
${CHANGELOG}

## Stability
Pre-1.0: **any release may break source and ABI compatibility** — the Zig API, the
C ABI, the QoS/config schema, and the bundle layout are all still in flux (see
\`docs/decisions.md\` → Versioning / Releases). Pin this exact tag / bundle; do not
track a branch or a version range.

## Zig compatibility
Built with and requires Zig \`${ZIG_VERSION}\`.

Expand Down
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,54 @@ Dated entries (no release tags past `v0.2.1-zig.0.16.0`; `build.zig.zon` is
`TypeSupport.compute_key_hash` signature change (`is_key_only: bool`) rippling to the
C ABI mirror and a further zidl release, so it is a follow-up beyond the v0.3.12 bump.
Tracked in `docs/roadmap.md` "Selective CDR parse — deferred follow-ups".
- **Release prep — prebuilt-bundle consume check.** `release.yml`'s `package-libs` job used
to verify only the *contents* of the install tree it built, in place. It now also extracts
the finished per-platform tarball into an unrelated directory and drives a real downstream
consume of it (`scripts/verify_release_bundle.py` + committed fixtures under
`test/release-bundle/`): structural completeness, pkg-config / CMake-package
relocatability (no baked-in absolute build path), the bundled `bin/zidl` runs, and
`find_package(ZZDDS)` + `pkg-config` build `examples/{c,cpp}/hello_world` and a minimal
consumer against the *relocated* prefix — on Linux the hello_world pair also exchanges its
10 samples. This is the consumption path `rmw_zzdds` (and any C/C++ CMake consumer) takes;
the in-tree `test-bindings` step never exercised a moved prefix. Linux: full; macOS:
`--skip-example-run` (skips only the live-UDP pair run — `cmake_consumer` still links and
runs against `libzzdds.dylib`). Windows keeps the structural check only: the generated
`zzdds-config.cmake` / `zzdds.pc` are POSIX-shaped (search `lib/` for the shared lib, no
`IMPORTED_IMPLIB`, `bin/zidl` not `bin/zidl.exe`), so `find_package(ZZDDS)` can't configure
a bundle there yet — tracked in `docs/roadmap.md` "CI / Release Platform Coverage".
- **Release prep — musl / static Linux target lane.** `-Dtarget` was never actually
cross-compiled anywhere in CI. New `zig build test -Dtarget=x86_64-linux-musl` step in
`run_deterministic_matrix.py` (so `ci.yml`'s `test-linux` covers it) and `release.yml`'s
`test` job (Linux x86_64 only). A `-linux-musl` binary is statically linked and runs
natively on the glibc runner, so this executes the full suite (1076/1076), proving zzdds
is musl-clean for Alpine / static-binary / container consumers. In
`run_deterministic_matrix.py` the step is gated to x86_64-Linux hosts (elsewhere the
cross-built binaries can't run, and Zig would silently skip them); CI's `ubuntu-latest`
runs it unconditionally. `aarch64-linux-musl` (needs qemu) and a static-archive `libzzdds`
bundle variant remain deferred — `docs/roadmap.md` "CI / Release Platform Coverage".
- **Release prep — GitHub-release notes now come from `CHANGELOG.md`.** `release.yml`'s
`publish` job built its release body from raw `git log --pretty=%s` subjects. It now
quotes the `CHANGELOG.md` sections added since the previous release tag —
`scripts/extract_changelog.py` emits the leading run of sections whose heading is not
present in `CHANGELOG.md` as of that tag (whole-heading, not date, comparison), falling
back to the leading date-headed run when the tag predates the file, and to raw commit
subjects only if that yields nothing. Always appends a `compare` link. Two releases on
the *same calendar day* under one `## <date>` heading aren't distinguished — the second
gets the git-log fallback (fine for a hotfix; the notes are hand-editable).
- **Decision recorded — pre-1.0 has no stability guarantee.** `docs/decisions.md` gains a
"Versioning / Releases" section: any release may break the Zig API, the C ABI, the
QoS/config schema, or the bundle layout, with no deprecation cycle; the C ABI stays in
flux until zzdds and Zig mature toward a distant 1.0; `--runtime-version <N>` stays
unimplemented until there is a tier worth pinning. Consumers pin an exact
`vX.Y.Z-zig.A.B.C` tag / bundle; downstream middleware (e.g. `rmw_zzdds`) owns its own
version mapping and absorbs zzdds churn behind its own boundary. `release.yml`'s release
notes now carry a matching "Stability" section.
- **Fix — the installed `zzdds.pc` / `zzdds-config.cmake` version now tracks `build.zig.zon`.**
`build.zig` carried a second, hand-maintained `zzdds_version` string (stuck at
`0.1.1-zig.0.16.0-dev`) that stamped the `Version:` field of the generated pkg-config and
CMake package files — so a consumer's `pkg-config --modversion zzdds` reported a version
two minors behind the actual package. It now reads `@import("build.zig.zon").version`, the
same field `release.yml` bumps at tag time.

## 2026-08-30

Expand Down
8 changes: 6 additions & 2 deletions build.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
const std = @import("std");
const builtin = @import("builtin");

/// Single source of truth for the package version. Everything that stamps a
/// version (the installed `zzdds.pc` and `zzdds-config.cmake` below) reads it
/// from here so it can never drift from the published package. `release.yml`
/// bumps `.version` in `build.zig.zon` at tag time.
const zzdds_version = @import("build.zig.zon").version;

/// Run a test binary, giving it a unique DDS domain via `ZZDDS_TEST_DOMAIN_BASE`.
///
/// `zig build test` runs test binaries as parallel build-graph steps; the
Expand Down Expand Up @@ -49,8 +55,6 @@ pub fn build(b: *std.Build) void {
const sanitize_thread = b.option(bool, "sanitize-thread", "Enable ThreadSanitizer") orelse false;
const debug_allocator = b.option(bool, "debug-allocator", "Route the default (allocator=NULL) factory allocation path through std.heap.DebugAllocator instead of std.heap.c_allocator, for fast attributable double-free/UAF diagnostics") orelse false;

const zzdds_version = "0.1.1-zig.0.16.0-dev";

// ── Dependencies ──────────────────────────────────────────────────────────

const zidl_dep = b.dependency("zidl", .{
Expand Down
22 changes: 22 additions & 0 deletions docs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,25 @@ Wireshark correlation and deterministic tests. Both paths embed `ZZDDS_VENDOR_ID
`-Dipv4`, `-Dipv6`, `-Dinterface-monitor`, `-Dwire-trace`, `-Dguid-filter`,
`-Dxtypes`, `-Dcontent-subscription-profile`. Dead-code elimination removes unused
paths at compile time — no runtime overhead, no `#ifdef`-style branching at call sites.

---

## Versioning / Releases

**Pre-1.0: no source- or ABI-compatibility guarantee across releases.**
Any release may change the Zig API, the C ABI (`zzdds_c.h` + the zidl-generated C/C++
surface), the QoS/config schema, or the prebuilt-bundle layout — without a deprecation
cycle. The C ABI in particular is expected to stay in flux until both zzdds and Zig itself
mature toward a 1.0, which is a long way off. `--runtime-version <N>` (see
`language-bindings.md`) is deliberately unimplemented until there is a stable tier to pin;
there isn't one yet, and declaring one is not a near-term goal.

**Consumers pin an exact release.** A tag is `vX.Y.Z-zig.A.B.C` (package version + the
exact Zig toolchain it was built with — enforced in `release.yml`). Pin the exact tag for
`zig fetch`, or the exact per-platform bundle tarball for C/C++ / CMake consumers. Do not
track a branch or a version range.

**Downstream middleware owns its own compatibility mapping.** A consumer that re-exports
zzdds through its own stable-ish surface (e.g. an `rmw_zzdds`) is responsible for pinning a
specific zzdds release, carrying its own version/build metadata, and absorbing zzdds ABI
churn behind its own boundary — not for expecting zzdds to hold an interface for it.
Loading
Loading