Build and test mssql-odbc across many Linux distros via prebuilt CI artifacts - #127
Build and test mssql-odbc across many Linux distros via prebuilt CI artifacts#127saurabh500 wants to merge 19 commits into
Conversation
Add build_e2e.sh to build the driver and C++ gtest binaries and stage them into a drop directory, and add --skip-build / --driver=PATH modes to run_e2e.sh so prebuilt binaries can be registered and run without cargo or cmake. This decouples the ODBC e2e build from the run so binaries can be built once and reused across distro containers in CI. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
Build the ODBC driver and C++ gtest e2e binaries once in the Ubuntu 22.04
build container (Build_Linux / Build_Linux_ARM), stage them via build_e2e.sh,
and publish as Odbc_Linux_x64 / Odbc_Linux_arm64 pipeline artifacts. Add a
Test_odbc_amd64 stage that downloads the x64 drop, restores the build tree,
brings up SQL Server, and reruns the prebuilt binaries in Debian bookworm,
Ubuntu 22.04/24.04, and Azure Linux 3 containers via run_e2e.sh --skip-build.
The modern-track reuse matrix is limited to glibc >= 2.35 / OpenSSL 3 distros
since the binaries are built on Ubuntu 22.04; RHEL 8/9 are left to the
glibc-2.28 track. Adds containerized-odbc-build.sh, build-odbc-template.yml,
test-matrix-odbc-template.yml, and odbc-e2e-{deb,rhel,azlinux3}.sh entrypoints.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
Add test-matrix-odbc-template-arm64.yml and a Test_odbc_arm64 stage that downloads the Odbc_Linux_arm64 drop and reruns the prebuilt driver + ctest binaries against the cross-pool x64 SQL host (sql-host-template.yml), mirroring the nextest arm64 SQL-host orchestration (derive password, poll-for-endpoint, teardown sentinels). The e2e server env is set to DB_HOST,DB_PORT so the driver targets the resolved cross-pool endpoint. Matrix limited to glibc >= 2.35 / OpenSSL 3 distros. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
Build the mssql-odbc driver + ctest binaries once in the Alpine musl build container (dynamic, -C target-feature=-crt-static) and publish as Odbc_Musl_x64 / Odbc_Musl_arm64. Add Test_odbc_alpine (x64) and Test_odbc_alpine_arm64 (cross-pool x64 SQL host) that download those drops and rerun the prebuilt binaries across Alpine 3.18-3.21 via run_e2e.sh --skip-build. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
Build the mssql-odbc driver + ctest binaries on the manylinux_2_28 (AlmaLinux 8, glibc 2.28, OpenSSL 1.1) base and publish as Odbc_glibc228_x64, then download and rerun them in a RHEL 8 / UBI 8 container via run_e2e.sh --skip-build. Import redhat/ubi8 and mirror the manylinux_2_28 base to GHCR so both images are pullable in CI. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
Alpine base images ship no bash, so --entrypoint bash fails before the entry script can apk add it. Drop the explicit entrypoint and run the executable entry script directly (matching the nextest test templates); each script's shebang (sh for Alpine, bash elsewhere) resolves inside the container. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
Explain build_e2e.sh (build once, publish) + run_e2e.sh --skip-build (rerun prebuilt binaries on many distros) and the three libc/OpenSSL tracks (glibc modern, musl, glibc 2.28) in the e2e README. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
Azure DevOps job names allow only alphanumeric and underscore, so the odbc-shared instanceId produced an invalid Sql_Host_odbc-shared job name. Rename to odbc_shared and match the sql-ready sentinel artifact name. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
Fold the mssql-odbc Linux e2e matrices into the existing nextest test stages instead of allocating separate ODBC stages/agents: - Test_amd64: adds modern-glibc and glibc-2.28 (RHEL8) ODBC x64 matrices - Test_alpine: adds musl x64 ODBC matrix - Test_arm64 / Test_alpine_arm64: share one cross-pool x64 SQL host between the nextest and ODBC matrices (expectedSentinels unions both, ODBC sentinels prefixed 'odbc_' to avoid distro-name collisions) Add manageSqlHost to the nextest arm64 templates and manageSqlHost/sqlReadyInstanceId/sentinelPrefix to the ODBC arm64 template so a stage can own the shared host; add nameSuffix to the x64 ODBC template so two matrices can coexist in one stage. Delete the four standalone Test_odbc_* stages. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
📊 Code Coverage Report
Diff CoverageDiff: main...HEAD, staged and unstaged changesNo lines with coverage information in this diff. 🔗 Quick Links |
There was a problem hiding this comment.
Pull request overview
Extends the main-branch CI pipeline to build mssql-odbc + its CTest/gtest e2e binaries once per (arch, libc/OpenSSL track), publish them as pipeline artifacts, and then re-run those prebuilt binaries across a multi-distro container matrix (including arm64 cross-pool SQL host orchestration), mirroring the existing nextest archive pattern used elsewhere in the repo.
Changes:
- Add split e2e scripts (
build_e2e.sh+run_e2e.sh --skip-build) to enable artifact reuse across distro containers. - Introduce new Azure Pipelines templates to build/publish ODBC drops for glibc (modern), musl (Alpine), and glibc 2.28 (RHEL8) tracks, plus corresponding test matrices (amd64 + arm64).
- Add distro-specific container entry scripts and update image sync to ensure required base images are available (e.g., UBI8, manylinux_2_28 mirror).
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/dockerentry/odbc-e2e-rhel.sh | RHEL-family test container entrypoint (dnf) to install runtime deps and run prebuilt ODBC e2e. |
| scripts/dockerentry/odbc-e2e-deb.sh | Debian/Ubuntu test container entrypoint (apt) to install runtime deps and run prebuilt ODBC e2e. |
| scripts/dockerentry/odbc-e2e-azlinux3.sh | Azure Linux 3 test container entrypoint (tdnf) to install runtime deps and run prebuilt ODBC e2e. |
| scripts/dockerentry/odbc-e2e-alpine.sh | Alpine (musl) test container entrypoint (apk) to install runtime deps and run prebuilt ODBC e2e. |
| scripts/dockerentry/alpine-odbc-build.sh | Alpine (musl) build container entrypoint to produce and stage a reusable ODBC e2e drop. |
| mssql-odbc/tests/e2e/run_e2e.sh | Add --skip-build / --driver support and adjust help output for prebuilt artifact execution. |
| mssql-odbc/tests/e2e/README.md | Document the “build once, test everywhere” artifact flow and libc/OpenSSL track constraints. |
| mssql-odbc/tests/e2e/build_e2e.sh | New build-only script to produce the driver + CMake build tree and stage them into an artifact drop dir. |
| .pipeline/templates/validation-stages.yml | Wire new ODBC build steps and ODBC test matrices into the main-branch validation stages (non-PR). |
| .pipeline/templates/test-matrix-template-arm64.yml | Add manageSqlHost to allow sharing a single cross-pool SQL host with another matrix in the same stage. |
| .pipeline/templates/test-matrix-template-alpine_arm64.yml | Add manageSqlHost to support sharing the musl cross-pool SQL host with ODBC musl arm64 matrix. |
| .pipeline/templates/test-matrix-odbc-template.yml | New amd64 ODBC e2e test matrix template that downloads an ODBC drop and reruns via containers. |
| .pipeline/templates/test-matrix-odbc-template-arm64.yml | New arm64 ODBC e2e matrix template that uses cross-pool SQL host sentinels and reruns prebuilt binaries. |
| .pipeline/templates/build-odbc-template.yml | New template to build/publish a modern-glibc ODBC drop from Ubuntu 22.04 build container. |
| .pipeline/templates/build-odbc-glibc228-template.yml | New template to build/publish a glibc 2.28 / OpenSSL 1.1 ODBC drop for RHEL8 compatibility. |
| .pipeline/templates/build-odbc-alpine-template.yml | New template to build/publish a musl ODBC drop from Alpine build container. |
| .pipeline/sync-container-images.yml | Import/mirror additional base images required by the new ODBC distro matrices (e.g., UBI8, manylinux_2_28). |
| .pipeline/scripts/containerized-odbc-build.sh | New build-container helper script to install deps and run build_e2e.sh for Ubuntu-based track. |
| .pipeline/scripts/containerized-odbc-build-glibc228.sh | New build-container helper script to install deps + rustup and run build_e2e.sh for glibc 2.28 track. |
The glibc/musl/glibc-2.28 ODBC build tasks run sequentially in one job and share the drop dir and the in-place tests/e2e/build tree. Docker leaves these root-owned, so the next task's host-level 'rm -rf odbc-drop' failed with Permission denied and the reused CMake cache mixed toolchains. Clean both inside each container as root and drop the host-level rm. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
The musl (Alpine) build image ships neither python3 nor git. build_e2e.sh resolved the Cargo target dir via 'cargo metadata | python3', which failed on Alpine and fell back to the crate-local target, so the workspace-level libmsodbcsql18.so was reported missing. Parse target_directory with pure shell instead, with a workspace-aware fallback. Also install git in the Alpine build so the e2e CMake FetchContent(googletest) clone works. Verified end-to-end in the ghcr Alpine 3.18 build image via Docker. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
The redhat/ubi8 import and manylinux_2_28/ubi8 GHCR mirror in sync-container-images.yml are split out into #128 so this PR is scoped to the ODBC build/test pipeline only. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
Build the mssql-odbc glibc-2.28 driver + e2e binaries on arm64 as well as
x64, using the pre-baked manylinux_2_28_{x86_64,aarch64}_rust images (rust,
cmake, unixODBC-devel, OpenSSL 1.1 baked in) instead of installing the
toolchain at runtime. Adds an arm64 RHEL 8 / UBI 8 test track (shared and
per-job modes) reusing the cross-pool x64 SQL host, with an odbc_RHEL8
teardown sentinel and a nameSuffix on the arm64 ODBC matrix job to avoid
duplicate job names.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
The modern-glibc ODBC track builds first in the same job, shares the workspace, and uses the same x86_64-unknown-linux-gnu host triple, so cargo reused its host build scripts (e.g. openssl-sys build-script-main) for the glibc-2.28 build. Those are linked against the modern image's glibc 2.34+ and fail to run on the glibc-2.28 base (GLIBC_2.29..2.34 not found). Build the 2.28 track into a dedicated CARGO_TARGET_DIR so cargo recompiles the build scripts under glibc 2.28. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
…sentinel prefix Correct odbc-e2e-rhel.sh header to name the RHEL 8 / UBI 8 (glibc 2.28) track it is actually wired for, not RHEL/UBI 9. In the arm64 ODBC shared+managed-host branch, compute expectedSentinels with sentinelPrefix applied so it matches the prefixed teardown sentinelName the jobs publish. Prevents the SQL host from waiting forever if a caller ever manages the host while setting sentinelPrefix. Reduces to the plain join when prefix is ''. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
Pipeline artifact upload/download drops the Unix execute bit, so the
restored ctest binaries come back as 0644 and ctest fails to exec them
("permission denied"). chmod +x the *_test binaries in run_e2e.sh's
--skip-build branch, which every distro/arch entrypoint funnels through.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
The x64 ODBC test template inline-starts SQL Server with `docker run -d` then immediately runs the prebuilt e2e binaries. Unlike the nextest tracks (which spend minutes building cargo, absorbing SQL's first-boot window) the ODBC binaries are prebuilt, so the first live test races SQL init and fails with `Login failed for user 'sa' (native=18456)`. Add a `sqlcmd SELECT 1` readiness probe (mirroring sql-host/start.sh) that polls until the SA login is live before running the container, so live tests connect only once SQL is ready. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
SQL Server 2025 intermittently hits a SQLPAL startup assertion on the x64 agents and the container dies. Wrap the container start in a retry loop that restarts SQL (up to 3 attempts) when it exits before accepting the SA login, so the prebuilt ODBC e2e binaries only run against a live server. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
David-Engel
left a comment
There was a problem hiding this comment.
Summary
Extends the main-branch CI to build the mssql-odbc driver + C++ gtest e2e binaries once per (arch, libc) track and rerun the prebuilt binaries across many distro containers — mirroring the existing mssql-tds nextest-archive pattern. The design is sound, the split build/run flow is well-reasoned, and the cross-job SQL-host sentinel accounting checks out. Findings below are non-blocking; the real validation is a green live pipeline run (which requires #128 first).
Verified correct (the parts flagged as risky)
- arm64 shared-host sentinel accounting balances. The
Test_arm64shared host declares 12expectedSentinels, and the three consumer matrices publish exactly those 12: nextest (7) + ODBC modernodbc_{DebianBookworm,Ubuntu22,Ubuntu24,AzLinux3}(4) + ODBC glibc228odbc_RHEL8(1).Test_alpine_arm64similarly balances (4 nextest + 4odbc_= 8). No orphaned/missing sentinels that would hang or prematurely release the host. - The
format/replacesentinel-prefix expression intest-matrix-odbc-template-arm64.ymlcorrectly producesodbc_A,odbc_B(prefix applied to every element, not just after commas) and degrades to a plain join whensentinelPrefixis''. chmod +xrestore is complete. Thefind … -name '*_test'inrun_e2e.shmatches every compiled binary — CMakeLists names them all*_testviaadd_odbc_test(...). Good catch on the artifact-strips-exec-bit problem.- Absolute-path /
/workspacemount reasoning holds — both build and test jobs mount the repo at/workspace, so the paths baked intoCTestTestfile.cmakeresolve on rerun. The dedicatedCARGO_TARGET_DIR=/workspace/target-glibc228doesn't affect this since the driver is copied intobuild/. - amd64 SQL image hardcoding matches convention —
test-matrix-template.ymlalso hardcodesmssql/server:2025-latest, so the new ODBC amd64 template isn't a deviation.
Suggestions
- Heavy duplication in
validation-stages.yml. The Alpine and deb/rhel target lists are now repeated across shared-mode and per-job-mode for both nextest and ODBC (6+ copies of near-identicaltargets:blocks). Adding/removing a distro requires editing many places and invites drift. Consider hoisting each target list into a reusable variable / YAML anchor or a parameter default. Mirrors the existing pattern, so not merge-blocking. - ~130-line block duplicated between shared-mode and per-job-mode in
test-matrix-odbc-template-arm64.yml; the twosteps:sequences differ only in howcontainer/entry/sentinel names are sourced. Hard to factor further in ADO templates, but worth a note so future edits stay in sync. - Dead artifact output:
build_e2e.shwrites"$OUT_DIR/DRIVER_FILE", butrun_e2e.sh --skip-buildre-derives the lib name fromunameand never reads it. Either consume it inrun_e2e.sh(single source of truth) or drop the write.
Nits
- Inconsistent
architecturecasing: the glibc228 arm64 call passesarchitecture: arm64while sibling calls useARM64/x64. Display-only (step name), so harmless, but worth normalizing. - The RHEL/UBI8
dnf install unixODBC cmakeinodbc-e2e-rhel.shshould resolve from ubi-8-appstream, but confirm in the first live run (along with the already-listed musl runtime-dep andServer=host,portassumptions).
Merge ordering
The glibc-2.28 / RHEL8 tracks pull manylinux_2_28_{x86_64,aarch64}_rust and import/redhat/ubi8 from GHCR, which #128 provides. As the description states, #128 must merge and its sync pipeline must run before these tracks can pull the images — otherwise those matrix legs fail on image pull. Worth reconfirming ordering at merge time.
Overall: clean, well-documented, correctly wired. Main pre-merge action is a green live pipeline run; the duplication items are quality-of-life, not blockers.
- build_e2e.sh wrote OUT_DIR/DRIVER_FILE, but run_e2e.sh --skip-build re-derives the driver name from uname and never reads it. Remove the dead write. - Normalize the glibc-2.28 arm64 build call to architecture: ARM64 to match its sibling ODBC/nextest calls (display-only string). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 496ab173-ccc0-4a5e-8412-717097724a06
|
Thanks for the review, David. Addressed in 4a1c7af:
On the "verify in a live run" items — these all passed in the last full green run (build 161627): the RHEL8/ubi8 I filed the duplication cleanups (target lists in |
Summary
Extends the main-branch CI pipeline (not PR builds) to build the mssql-odbc driver + C++ gtest e2e binaries once per (arch, libc) and publish them as pipeline artifacts, then download and rerun those prebuilt binaries against a live SQL Server inside many distro containers — mirroring the existing
mssql-tdsnextest-archive → artifact → multi-distro pattern.This is a draft for iteration; the full e2e runs can only be validated by running the pipeline (no live SQL Server locally). All YAML and shell scripts pass
yaml.safe_loadandbash -n.Approach: build once, test everywhere
The e2e flow was split into two halves:
build_e2e.sh [--release] [--out=DIR]— builds the Rust driver + ctest binaries and stagesbuild/(withlibmsodbcsql18.socopied inside) into a drop dir that gets published as an artifact.run_e2e.sh --skip-build— skips all compilation, restores the prebuiltbuild/tree, auto-resolves the driver frombuild/libmsodbcsql18.so, and reruns the binaries via CTest.CTestTestfile.cmakebakes absolute paths, so build and test jobs both mount the repo at/workspaceto keep paths consistent.Build tracks (libc / OpenSSL specific)
Odbc_Linux_x64Odbc_Linux_arm64Odbc_Musl_x64/Odbc_Musl_arm64manylinux_2_28_x86_64_rust(AlmaLinux 8, OpenSSL 1.1)Odbc_glibc228_x64manylinux_2_28_aarch64_rust(AlmaLinux 8, OpenSSL 1.1)Odbc_glibc228_arm64A glibc-2.35 / OpenSSL-3 binary won't load on RHEL 8 (glibc 2.28,
libssl.so.1.1), so the glibc-2.28 track is built separately, on both x64 and arm64. RHEL 9 / Oracle 9 (glibc 2.34) are intentionally excluded from the modern track.The glibc-2.28 tracks build in the pre-baked
manylinux_2_28_{x86_64,aarch64}_rustimages (from #128), which ship rust + cmake + unixODBC-devel + OpenSSL 1.1 dev, so nothing is installed at build time.New CI stages
The ODBC e2e reuses the existing
Test_amd64/Test_arm64stages (no new stages / agent allocation):arm64 tests use the existing cross-pool x64 SQL host orchestration (
sql-host-template.yml: derive password, poll-for-endpoint, teardown sentinels), passingODBC_TEST_SERVER=host,port. The arm64 RHEL 8 job publishes anodbc_RHEL8teardown sentinel and uses anameSuffixon the ODBC matrix job to avoid a duplicate job name alongside the modern-glibc arm64 ODBC matrix.Files
New scripts
mssql-odbc/tests/e2e/build_e2e.sh— build half.pipeline/scripts/containerized-odbc-build.sh(Ubuntu),containerized-odbc-build-glibc228.sh(manylinux_2_28_rust)scripts/dockerentry/alpine-odbc-build.sh(musl build)scripts/dockerentry/odbc-e2e-{deb,rhel,azlinux3,alpine}.sh(test entrypoints)New templates
build-odbc-template.yml,build-odbc-alpine-template.yml,build-odbc-glibc228-template.ymltest-matrix-odbc-template.yml,test-matrix-odbc-template-arm64.ymlModified
mssql-odbc/tests/e2e/run_e2e.sh—--skip-build/--driverflags.pipeline/templates/validation-stages.yml— build calls (incl. glibc-2.28 arm64) + ODBC test matrices (incl. arm64 RHEL 8, shared + per-job).pipeline/templates/build-odbc-glibc228-template.yml—architectureparam; defaultbuildImage→ GHCRmanylinux_2_28_x86_64_rust.pipeline/scripts/containerized-odbc-build-glibc228.sh— dropped runtimednf install+ rustup (now baked into the_rustimage).pipeline/templates/test-matrix-odbc-template-arm64.yml—nameSuffixparam for the shared-mode job namemssql-odbc/tests/e2e/README.md— document the flowOpen items / assumptions to verify in-pipeline
.soruntime deps on Alpine (libssl3, unixodbc, libstdc++).Server=host,port;for the arm64 cross-pool endpoint (assumed fromBuildConnectionString).Work item
AB#46492
Verification done
yaml.safe_load.bash -n.--entrypoint bash(Alpine base images have no bash) in favor of executing entry scripts via their shebang, matching the nextest templates.