Skip to content

Build against DuckDB v1.5.6 (#515) #1729

Build against DuckDB v1.5.6 (#515)

Build against DuckDB v1.5.6 (#515) #1729

#
# This workflow calls the main distribution pipeline from DuckDB to build, test and (optionally) release the extension
#
name: Main Extension Distribution Pipeline
on:
pull_request:
# `main` for release-bound PRs, plus `feat/**` and `fix/**` so stacked
# PRs (each targeting the previous branch rather than `main`) trigger
# CI automatically — instead of needing a manual `workflow_dispatch`
# call from a reviewer or from a maintainer's local machine.
branches:
- main
- 'feat/**'
- 'fix/**'
paths-ignore:
- '**/README.md'
- 'doc/**'
push:
branches:
- main
paths-ignore:
- '**/README.md'
- 'doc/**'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || '' }}-${{ github.base_ref || '' }}-${{ github.ref != 'refs/heads/main' || github.sha }}
cancel-in-progress: true
jobs:
# THE ONE PLACE A SUPPORTED DUCKDB VERSION IS WRITTEN.
# The version used to appear nine times in this file, and the comment on the
# artifact download asked the reader to "keep the version here in sync with the
# two jobs above" — a rule nothing enforces. Every consumer below reads this
# job's output, so adding or dropping a supported line is one edit to the list
# in the `pick` step, and a matrix over it builds each version named there.
#
# The two `uses:` refs stay literal because a reusable-workflow reference admits
# no expression ("You cannot use contexts or expressions in this keyword"). They
# name the CI-TOOLS workflow to run, which is a different fact from which DuckDB
# versions are supported: one such workflow serves several of them through the
# `duckdb_version` and `ci_tools_version` inputs.
versions:
name: Supported DuckDB versions
runs-on: ubuntu-24.04
outputs:
supported: ${{ steps.pick.outputs.supported }}
steps:
- id: pick
shell: bash
run: echo 'supported=["v1.5.6"]' >> "$GITHUB_OUTPUT"
duckdb-latest-build:
needs: versions
strategy:
fail-fast: false
matrix:
duckdb: ${{ fromJSON(needs.versions.outputs.supported) }}
# The upstream distribution workflow with one addition: every job keeps a vcpkg binary
# cache of its own, so the dependency set the manifest names — GDAL, GEOS,
# PROJ, OpenSSL, libmeos and the twenty ports beneath them — is restored
# rather than compiled. The public cache the workflow reads holds an archive
# only for the dependency sets DuckDB itself builds, and this extension
# carries a port of its own, so twenty-seven of its twenty-eight packages
# are compiled from source on every job: 12.7 minutes of a 27.7-minute
# Linux job, and the larger share of a macOS one. The addition is offered
# upstream; the fork is where it runs until it is taken. On the MinGW job the fork
# also takes the R the runner image carries: the build compiles with the Rtools
# toolchain and never runs R, so R devel is not downloaded.
uses: estebanzimanyi/extension-ci-tools/.github/workflows/_extension_distribution.yml@v1.5.6-node24
with:
duckdb_version: ${{ matrix.duckdb }}
extension_name: mobilityduck
ci_tools_version: ${{ matrix.duckdb }}
vcpkg_commit: c27eeddba73f608f10605d80bc0144c1166f8fb7
# A compiler cache only pays off if it is still there next time, and a
# repository gets 10 GB for all of them. The distribution workflow allows
# each job 5 GB, so a matrix of seven architectures overruns the budget and
# GitHub evicts least-recently-used to make room; the entries that lose that
# race are the small ones, which is why the Linux leg reads
# `cache hit rate 0.00 %` with all 724 objects recompiled, 46 minutes of a
# 51-minute job.
#
# `test_env_variables` is appended to the Docker environment file after the
# workflow writes its own defaults, and a later assignment wins there, so
# naming the ceiling here overrides the 5 GB for every architecture built in
# Docker. The ceiling bounds the cache directory, which the Docker legs keep
# uncompressed (CCACHE_NOCOMPRESS=1), while the entry saved from it is a
# zstd archive about six times smaller. One build writes about 300 MB of
# objects, and a job starts from the directory the last one saved, which
# after a DuckDB version change holds the other version's objects. 1.5G
# holds such a restored directory and a full rebuild without evicting the
# objects the next job reads, and saves an entry of at most about 250 MB.
# The macOS and Windows legs take their ceiling from the ccache action
# instead and are unaffected.
test_config: '{"test_env_variables": {"CCACHE_MAXSIZE": "1.5G"}}'
# Windows builds through MinGW (windows_amd64_mingw), the toolchain MEOS
# is built with on Windows. windows_amd64 is excluded: it compiles with
# MSVC, which MEOS has no build for.
#
# On pull_request events the reduced matrix leaves the native targets
# that run the suite. macOS is where a non-portable binary shows up first
# — the loader there rejects a missing @rpath dependency outright — so
# osx_arm64 is one of those a pull request keeps, and the
# verify-standalone job below inspects its artifact on the event that
# gates review.
exclude_archs: 'windows_amd64;linux_amd64_musl'
# Of the four native targets a pull request builds, two run the suite —
# linux_amd64 and osx_arm64 — and two only build: linux_arm64 and
# osx_amd64 report `Test extension … skipped`, the latter because it is
# cross-compiled on an arm64 host. Reduced mode keeps exactly the pair
# that tests, so a pull request pays for two builds rather than four and
# loses no assertion, only the compile of the two archs it drops. Every
# other event keeps the full matrix, so a push to main and a release
# still build every target.
reduced_ci_mode: ${{ github.event_name == 'pull_request' && 'enabled' || 'disabled' }}
# A hand-written binding declares its result type twice — in the registration
# DuckDB reads and in the executor template that writes the result vector —
# and nothing makes the two agree. The audit is source-only, so it answers in
# seconds and gates the pull request rather than waiting on a build.
source-audits:
name: Source audits
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Declared return types are the ones the executors write
run: python3 scripts/return-type-audit.py .
# The published artifact is a single file a user downloads and LOADs, so the
# guarantee it has to carry is that it names no shared library it does not
# ship, and that a stock DuckDB CLI can LOAD it and evaluate a MEOS-backed
# expression. It runs before deploy so a non-portable binary is never released.
verify-standalone:
name: Standalone load (${{ matrix.duckdb }}, ${{ matrix.duckdb_arch }})
needs: [versions, duckdb-latest-build]
# An entry consumes the artifact of the build job of its own architecture, so the set of
# entries is the set the build matrix produces: a pull request builds linux_amd64 and
# osx_arm64, and asking for either of the other two would fail on a download of an artifact
# nothing produced. The two lists are written out rather than filtered by a job-level `if`,
# which cannot read the `matrix` context.
#
# Reading an artifact's declared dependencies works from any host — `otool`/`readelf` parse
# headers and do not execute code — so that assertion covers every entry. Running the loader
# needs a host of the artifact's own architecture, so `native` marks the entries where the
# LOAD step can run.
#
# GitHub has no Intel macOS runner: a job asking for `macos-13` is never assigned one and is
# cancelled at the 24h queue limit having run no step, which reads as a red check that no code
# change can turn green. The build job does not hit this because it produces osx_amd64 by
# cross-compiling on an arm64 host. So inspect that artifact on arm64 and leave its LOAD to
# the deployed binary's osx_arm64 sibling, which exercises the same linkage.
strategy:
fail-fast: false
matrix:
duckdb: ${{ fromJSON(needs.versions.outputs.supported) }}
include: ${{ fromJSON(github.event_name == 'pull_request'
&& '[{"duckdb_arch":"linux_amd64","runner":"ubuntu-24.04","native":true},{"duckdb_arch":"osx_arm64","runner":"macos-14","native":true}]'
|| '[{"duckdb_arch":"linux_amd64","runner":"ubuntu-24.04","native":true},{"duckdb_arch":"linux_arm64","runner":"ubuntu-24.04-arm","native":true},{"duckdb_arch":"osx_amd64","runner":"macos-14","native":false},{"duckdb_arch":"osx_arm64","runner":"macos-14","native":true}]') }}
runs-on: ${{ matrix.runner }}
steps:
# Artifact name is built from duckdb_version + extension_name, both read
# from the matrix rather than written again.
- uses: actions/download-artifact@v8
with:
name: mobilityduck-${{ matrix.duckdb }}-extension-${{ matrix.duckdb_arch }}
path: artifact
- name: Assert the artifact names no non-system shared library
shell: bash
run: |
set -euo pipefail
EXT=artifact/mobilityduck.duckdb_extension
test -f "$EXT"
if [ "${{ runner.os }}" = "macOS" ]; then
# Drop line 1 (otool echoes the file path) and keep the load-command
# paths. The extension's own LC_ID_DYLIB appears here too and is not
# a dependency, hence the explicit allowance for it.
otool -L "$EXT" | tail -n +2 | awk 'NF {print $1}' > deps.txt
ALLOW='^(/usr/lib/|/System/Library/Frameworks/|@rpath/mobilityduck\.duckdb_extension$)'
else
readelf -d "$EXT" | sed -n 's/.*NEEDED.*\[\(.*\)\]/\1/p' > deps.txt
# glibc's own set plus the C++ runtime. Anything vcpkg built (MEOS,
# GEOS, PROJ, GDAL, GSL, json-c, h3, libxml2) must be absorbed into
# the binary, so none of them may appear here.
ALLOW='^(libc\.so|libm\.so|libdl\.so|libpthread\.so|librt\.so|libresolv\.so|libstdc\+\+\.so|libgcc_s\.so|libz\.so|ld-linux)'
fi
echo "--- shared libraries named by the artifact:"
cat deps.txt
if grep -Ev "$ALLOW" deps.txt > offenders.txt; then
echo "::error::The published artifact depends on libraries it does not ship. A downloader cannot dlopen it."
cat offenders.txt
exit 1
fi
echo "OK: only system libraries are referenced."
- name: LOAD the artifact into a stock DuckDB CLI
if: ${{ matrix.native }}
shell: bash
run: |
set -euo pipefail
case "${{ matrix.duckdb_arch }}" in
linux_amd64) CLI=duckdb_cli-linux-amd64.zip ;;
linux_arm64) CLI=duckdb_cli-linux-arm64.zip ;;
osx_*) CLI=duckdb_cli-osx-universal.zip ;;
esac
curl -fsSL -o duckdb.zip "https://github.com/duckdb/duckdb/releases/download/${{ matrix.duckdb }}/${CLI}"
unzip -q -o duckdb.zip
chmod +x duckdb
# tbool() round-trips through MEOS and depends on the Europe/Brussels
# session timezone the extension installs at load time, so a correct
# answer proves MEOS is both linked in and initialised — not merely
# that dlopen succeeded. Mirrors test/sql/tbool.test.
./duckdb -unsigned -noheader -list -c \
"LOAD 'artifact/mobilityduck.duckdb_extension'; SELECT tbool(true, timestamptz '2012-01-01 08:00:00');" \
> out.txt
echo "--- output:"; cat out.txt
grep -qx 't@2012-01-01 08:00:00+01' out.txt
duckdb-latest-deploy:
needs: [versions, duckdb-latest-build, verify-standalone]
strategy:
fail-fast: false
matrix:
duckdb: ${{ fromJSON(needs.versions.outputs.supported) }}
uses: estebanzimanyi/extension-ci-tools/.github/workflows/_extension_deploy.yml@v1.5.6-node24
secrets: inherit
with:
duckdb_version: ${{ matrix.duckdb }}
ci_tools_version: ${{ matrix.duckdb }}
extension_name: mobilityduck
deploy_latest: ${{ startsWith(github.ref, 'refs/heads/v') || github.ref == 'refs/heads/main' }}
# Gathers exactly the artifacts the build job produces, per event: the full
# matrix on push/dispatch, and on pull_request the linux_amd64 the reduced
# matrix leaves, since asking for an arch that matrix does not build fails
# on "Artifact not found for name: mobilityduck-…-linux_arm64" and cancels
# its siblings with it. The build workflow produces no Wasm artifact, so
# the Wasm targets of this workflow's own matrix are excluded on every
# event, as are windows_amd64 and musl (see the build job above).
exclude_archs: ${{ github.event_name == 'pull_request' && 'windows_amd64;windows_amd64_mingw;linux_amd64_musl;linux_arm64;osx_amd64;osx_arm64;wasm_mvp;wasm_eh;wasm_threads' || 'windows_amd64;linux_amd64_musl;wasm_mvp;wasm_eh;wasm_threads' }}