Skip to content

FEAT: Add conda packaging recipes and publish tooling for mssql-python - #720

Draft
Jahnvi Thakkar (jahnvi480) wants to merge 105 commits into
mainfrom
jahnvi/conda-onboarding
Draft

FEAT: Add conda packaging recipes and publish tooling for mssql-python#720
Jahnvi Thakkar (jahnvi480) wants to merge 105 commits into
mainfrom
jahnvi/conda-onboarding

Conversation

@jahnvi480

@jahnvi480 Jahnvi Thakkar (jahnvi480) commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

AB#47315


Summary

This pull request introduces a complete, standalone Conda build and release pipeline for the mssql-python package, decoupling Conda packaging from the wheel/PyPI release process. The new pipelines ensure that Conda packages are built, validated, and published independently, with robust audit and gating steps to prevent regressions and incomplete releases.

Key changes include:

1. New Conda Build and Release Pipelines

  • Added OneBranchPipelines/conda-build-pipeline.yml: Implements a full, per-platform Conda build pipeline, repackaging prebuilt wheels into Conda packages for all supported OS/architectures, with validation and artifact consolidation.
  • Added OneBranchPipelines/conda-release-pipeline.yml: Introduces a dedicated release pipeline to validate the consolidated Conda packages and optionally publish them to Anaconda.org, with strict gating to ensure completeness and version consistency.

2. Automated Auditing on Pull Requests

  • Added .github/workflows/conda-audit.yml: Sets up a GitHub Actions workflow that builds and audits a Linux Conda package on every relevant PR, automatically failing the PR if regressions are detected in binary dependencies or audit checks.

3. Improved Separation and Safety

  • The Conda pipelines are now fully decoupled from the wheel/PyPI release pipeline, ensuring that Conda artifacts are not mixed or confused with wheel artifacts and that each release path is independently validated and gated.

4. Platform Coverage and Validation

  • The build pipeline covers all major platforms (Windows, macOS Intel/ARM, Linux x86_64/aarch64), using native and cross-builds as appropriate, and includes runtime validation (e.g., import tests, TLS probes) to ensure package integrity.

5. Secure, Gated Publishing

  • Publishing to Anaconda.org is strictly gated behind successful validation and is only enabled when explicitly requested, with secrets handled securely and never exposed in logs.

These changes significantly improve the reliability, auditability, and maintainability of the Conda packaging and release process for mssql-python.

Adds conda recipes for mssql-python and its mssql-python-odbc companion, a local build/test-before-live harness, and a draft OneBranch conda publish stage. Azure SDK deps resolve from the lean 'microsoft' Anaconda channel under --strict-channel-priority so conda-forge's azure-core recipe (which over-declares flask/six -> celery/boto3/botocore) does not bloat the environment. See conda-forge/azure-core-feedstock#71.
Copilot AI lite review requested due to automatic review settings August 14, 2026 08:50
@github-actions github-actions Bot added the pr-size: medium Moderate update size label Aug 14, 2026
Comment thread conda/mssql-python-odbc/meta.yaml Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds initial conda packaging assets for mssql-python (and its mssql-python-odbc companion) so the project can be published to the Microsoft-owned microsoft Anaconda channel, alongside the existing PyPI wheel distribution.

Changes:

  • Adds conda recipes for mssql-python and mssql-python-odbc that repackage existing wheels (no compilation).
  • Adds a local PowerShell harness to build both recipes, create a local channel, and validate import / optional live-connect.
  • Adds a draft OneBranch stage to build/test/publish conda artifacts from signed release artifacts.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
conda/onebranch-publish-conda-stage.yml Draft OneBranch stage to build from signed artifacts, gate on smoke tests, and publish to the microsoft Anaconda channel.
conda/mssql-python/meta.yaml Conda recipe for repackaging the mssql-python wheel and depending on a version-locked mssql-python-odbc.
conda/mssql-python-odbc/meta.yaml Conda recipe for repackaging the proprietary driver wheel into a companion conda package.
conda/build_and_test_local.ps1 Local “test-before-live” harness to build, index, install, and smoke test the conda packages.
Suppressed comments (1)

conda/onebranch-publish-conda-stage.yml:133

  • Same string-vs-boolean condition issue here: quoting the template expression turns it into a string, which can cause the publish step to be skipped unexpectedly even when publishToConda is true.
          - task: PowerShell@2
            displayName: 'Publish to anaconda.org/microsoft'
            condition: and(succeeded(), eq('${{ parameters.publishToConda }}', true))

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread conda/mssql-python/meta.yaml Outdated
Comment thread conda/mssql-python-odbc/meta.yaml Outdated
Comment thread conda/build_and_test_local.ps1 Outdated
Comment thread conda/onebranch-publish-conda-stage.yml Outdated
Comment thread conda/mssql-python-odbc/meta.yaml Outdated
Comment thread conda/build_and_test_local.ps1 Outdated
Recipes now honor ARTIFACTS_PY/ARTIFACTS_ODBC to install signed wheels offline (--no-index --find-links) instead of always hitting PyPI. build_and_test_local.ps1 adds --override-channels for reproducible solves and its header no longer claims an offline/no-microsoft-channel run. Draft publish stage condition compares the boolean parameter directly (eq(param, true)) instead of a quoted string.
Comment thread conda/mssql-python-odbc/meta.yaml Fixed
- Single-source versions via MSSQL_PYTHON_VERSION/MSSQL_ODBC_VERSION env (wired from the publish-stage params) so the package version and the companion pin can't drift.

- Point mssql-python-odbc license_file at the actual ODBC Driver 18 EULA + VC++ license (was MIT-primary root LICENSE); remove the resolved TODO.

- Drop the no-op azure-identity >=1.12.0 floor (microsoft channel ships CalVer).

- Re-assert the wheel platform floor via __glibc/__osx virtual-package run constraints.

- Add conda/driver_load_probe.py + run it in the gate and local harness so we prove the native ODBC driver loads, not just the Python shim.

- Publish stage: require signed wheels (no PyPI fallback), publish companion-first with --skip-existing and a #706 pair guard, and document the required resources.pipelines declaration.
Comment thread conda/driver_load_probe.py Fixed
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

100%


🎯 Overall Coverage

82%


📈 Total Lines Covered: 7774 out of 9444
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

No lines with coverage information in this diff.


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.logger_bridge.cpp: 58.9%
mssql_python.pybind.ddbc_bindings.h: 61.5%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 75.6%
mssql_python.__init__.py: 77.6%
mssql_python.row.py: 77.6%
mssql_python.pybind.connection.connection_pool.cpp: 82.2%
mssql_python.pybind.connection.connection.cpp: 84.4%
mssql_python.logging.py: 85.5%
mssql_python.connection.py: 85.9%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

@jahnvi480
Jahnvi Thakkar (jahnvi480) marked this pull request as draft August 18, 2026 08:57
Port the productionized conda pipeline from the ADO conda-publish-pipeline branch onto GitHub conda-onboarding, replacing the earlier draft prototype:

- conda recipes: meta.yaml + build.sh + bld.bat for mssql-python and mssql-python-odbc, vendored ODBC/VC++ EULA text, and .gitattributes (LF for shell scripts).

- OneBranchPipelines conda glue: scripts/build-conda-packages.{sh,ps1}, steps/conda-build-validate-step{,-posix}.yml, steps/conda-publish-step.yml, steps/conda-release-step.yml, jobs/consolidate-conda-artifacts-job.yml.

- Wire conda legs into build/release pipelines (buildConda params on win-64/osx/linux legs + ConsolidateConda stage), preserving GitHub-only signWindowsBinaries content.

- Remove superseded draft prototype: conda/onebranch-publish-conda-stage.yml and conda/build_and_test_local.ps1.
@github-actions github-actions Bot added pr-size: large Substantial code update and removed pr-size: medium Moderate update size labels Aug 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 3 comments.

Suppressed comments (4)

OneBranchPipelines/scripts/build-conda-packages.sh:173

  • The verify env name only includes the Python version. On macOS this script is invoked twice on the same agent (native osx-arm64, then cross-target osx-64), so the second conda create -n verify_... can fail with an existing prefix or reuse the wrong-arch env. Include the effective subdir (native vs $CONDA_SUBDIR) in the env name to avoid collisions.
for py in $pyvers; do
  envName="verify_${py//./}"
  echo "=== [py $py] create verify env from local channel ==="

OneBranchPipelines/stages/build-linux-single-stage.yml:116

  • This apt install runs before the later apt-get update in the job, so it can fail on agents with stale apt indices (common on hosted Ubuntu). Add an apt-get update (and ideally noninteractive) before installing qemu-user-static/libc6-arm64-cross.
              # qemu-user-static: run aarch64 ELF binaries on the x86_64 host.
              # libc6-arm64-cross: the aarch64 glibc runtime (loader + libc/libm/...)
              # under /usr/aarch64-linux-gnu so qemu can resolve /lib/ld-linux-aarch64.so.1
              # for the emulated aarch64 conda build/verify (QEMU_LD_PREFIX points here).
              sudo apt-get install -y qemu-user-static libc6-arm64-cross

OneBranchPipelines/steps/conda-publish-step.yml:91

  • Installing anaconda-client from PyPI without pinning a version can make releases non-reproducible and may unexpectedly break publishing if a new release introduces behavioral changes. Consider pinning to a vetted version (or using a constraints file) so publish behavior is stable.
        $ErrorActionPreference = 'Stop'
        python -m pip install --upgrade pip
        python -m pip install anaconda-client
        # anaconda-client installs the `anaconda` console script onto PATH.
        anaconda --version

OneBranchPipelines/steps/conda-build-validate-step.yml:126

  • The error message references $links (the wheel find-links directory), but conda packages are searched under ${{ parameters.outputDir }}/bld. If this trips, the message will mislead troubleshooting.
      if (-not $built) { Write-Error "No conda packages were produced under $($links)"; exit 1 }

Comment thread conda/mssql-python/meta.yaml Outdated
Comment thread OneBranchPipelines/scripts/build-conda-packages.sh Outdated
Comment thread OneBranchPipelines/scripts/build-conda-packages.ps1 Outdated
…uildAll on Windows

The Windows mssql-python-odbc companion conda is now built ONCE as a Python-agnostic package in the ODBC_BuildAll stage (no python in host, wheel extracted via tar in bld.bat), mirroring the single py3-none-win_* PyPI wheel, instead of once per Python on every binding leg. The per-Python mssql-python binding legs seed that prebuilt companion into their local channel (-Package binding -DriverCondaDir) so the version-locked dependency still resolves. ConsolidateConda now also pulls the ODBC_BuildAll companion, and the #706 release/publish gates use presence-pairing (keeping strict 1:1 only for per-Python companions, c>1). macOS/Linux stay per-Python (unchanged).
P0-1: invert conda/driver_load_probe.py from a fail-OPEN denylist to a
fail-CLOSED allowlist. A repackaged native ODBC driver that fails to load now
FAILS the DB-less pre-publish gate instead of passing on any unrecognized
exception. Only a clean connect or a connection-stage diagnostic the loaded
msodbcsql driver alone can emit (ODBC branding, network provider, TLS, auth)
counts as PASS. Defer 'import mssql_python' into main() so the classifier is
unit-testable without the compiled extension. Adds tests/test_026 (22 no-DB
tests).

P0-3: gate the conda release on package METADATA, not folder names/counts.
New conda/validate_conda_release.py reads each package's authoritative
info/index.json (zstd) and validates real subdir == folder, allowed subdirs,
the full (subdir x Python) binding matrix, exact/consistent versions, and #706
binding<->companion pairing. Catches a mislabeled subdir and the 8e7f217
dropped-win-64-variant regression the count gate missed. Rewires
OneBranchPipelines/steps/conda-release-step.yml to call it. Adds tests/test_027
(11 tests incl. a real .conda round-trip).
Comment thread conda/driver_load_probe.py Fixed
…P0-2)

macos-latest is an Intel Mac and the arm64 Python cannot execute there (no
reverse Rosetta), so the previous osx-arm64 leg silently built NATIVE osx-64
packages and staged them under osx-arm64 (mislabeled). Cross-build for real:

- conda/*/build.sh: when the host-env Python is not executable (non-emulated
  cross-build), extract the universal2 wheel into \ with unzip instead of
  pip -- mirrors the Windows bld.bat tar path; the arm64 slice comes from the
  universal2 wheel. Native + QEMU-emulated legs keep the pip install.
- conda/*/meta.yaml: skip_compile_pyc on macOS so conda-build does not run the
  non-runnable arm64 Python for .pyc byte-compilation (Python regenerates it).
- build-conda-packages.sh: section-7 verify auto-skips the runtime import when
  the target Python can't run on the host (osx-arm64 on Intel); the static
  arm64-slice audit is the stand-in. Native/QEMU legs still import for real.
- build-macos-single-stage.yml: add condaTargetSubdir: osx-arm64 +
  continueOnError to the arm64 leg; mark osx-64 as the native/blocking leg; add
  a BLOCKING static arm64 slice audit (lipo/otool/file) asserting the shipped
  ddbc_bindings ext and macos/arm64 dylibs really contain arm64 Mach-O. Fix the
  misleading 'Apple Silicon, native' comments.
The vendored ODBC Driver 18 links crypto/auth libs that must receive conda's
security updates instead of being frozen into the payload -- mirroring
conda-forge libpq (declares openssl + krb5, vendors neither):

- openssl # [not win]: the driver dlopen's libssl/libcrypto for TLS; it is not
  an ELF NEEDED entry so overlinking can't see it. Windows uses SChannel.
- krb5 # [linux]: libmsodbcsql NEEDs libkrb5.so.3 + libgssapi_krb5.so.2 (not
  bundled). macOS uses Kerberos.framework, Windows uses SSPI.
- vc14_runtime # [win]: msodbcsql18.dll imports VCRUNTIME140.dll but the
  vendored vcredist ships only msvcp140.dll; declare the serviced conda runtime.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.

Suppressed comments (2)

OneBranchPipelines/scripts/build-conda-packages.sh:184

  • The verify env name is only based on the Python version (e.g. "verify_310"). On macOS this script is invoked twice on the same agent (osx-64 and osx-arm64) sharing the same Miniforge install/outputDir, so the second invocation will fail when it tries to create an env that already exists. Include the target subdir in the env name (and/or delete any existing env before creating it) so the two runs don’t collide.
for py in $pyvers; do
  envName="verify_${py//./}"
  echo "=== [py $py] create verify env from local channel ==="
  # -c microsoft (ahead of conda-forge) so azure-core/azure-identity/msal resolve from the
  # lean `microsoft` channel, NOT conda-forge whose azure-core recipe over-declares flask/six
  # -> celery/boto3/botocore (~9 MB); see conda-forge/azure-core-feedstock#71.
  # --strict-channel-priority keeps the freshly built local companion + binding authoritative.
  "$conda" create -y -n "$envName" -c "$bld" -c microsoft -c conda-forge --strict-channel-priority --override-channels "python=$py" mssql-python

conda/validate_conda_release.py:67

  • read_index_json() uses next(...) to locate the info-*.tar.zst member inside a .conda. If the archive is malformed (missing that member), this will raise StopIteration and produce a stack trace rather than a clear validation failure message. Handle the empty case and raise a ValueError with a helpful message instead.
        with zipfile.ZipFile(path) as zf:
            info_name = next(
                n for n in zf.namelist() if n.startswith("info-") and n.endswith(".tar.zst")
            )
            info_blob = zf.read(info_name)

libodbcinst.so.2 has NEEDED libltdl.so.7 but no RUNPATH, so a minimal glibc

Linux base throws 'OSError: libltdl.so.7: cannot open shared object file' on

import. macOS already vendors libltdl.7.dylib; Linux was the inconsistent

outlier. Two parts:

1) eng/scripts/patch-linux-odbc-libs.sh (new): maintainer/CI tool, run in a

   manylinux_2_28 container, that sources the glibc libltdl.so.7, copies it

   next to libodbcinst.so.2, and patchelf --set-rpath '\' so the driver

   resolves it from its own dir. Skips Alpine/musl by design.

2) build-odbc-all-stage.yml: the wheel content verifier now REQUIRES

   libltdl.so.7 in both manylinux_2_28 payloads (fail-closed). This gate stays

   red until a maintainer runs the patch script and commits the produced

   libltdl.so.7 + rpath-patched libodbcinst.so.2 under

   mssql_python_odbc/libs/linux/debian_ubuntu/<arch>/lib/ (cannot be produced

   on the Windows-only odbc build host).
Decision 2(b): Alpine is a supported, PR-tested platform, so its wheels must be

fixed too (not dropped, not documented-as-limitation). Extends the libltdl

self-contained-payload work to musl:

- eng/scripts/patch-linux-odbc-libs.sh now auto-detects libc + arch and patches

  the matching distro subtrees: a glibc-built libltdl (manylinux, dnf) for

  debian_ubuntu/rhel/suse, a musl-built libltdl (Alpine, apk add libtool) for

  alpine. One build per (libc, arch) serves all that arch's distro subtrees. It

  also drops a per-dir LIBLTDL_LGPL_LICENSE.txt notice.

- mssql_python_odbc/libs/LICENSING: document libltdl (GNU Libtool, LGPL-2.1-or-

  later, dynamically linked) covering the existing macOS libltdl.7.dylib and the

  new Linux libltdl.so.7 (compliance; precedent = macOS already vendors it).

- build-odbc-all-stage.yml: musllinux_1_2_{x86_64,aarch64} wheels now also REQUIRE

  libltdl.so.7 (fail-closed), since the Alpine test leg's system libltdl masks

  the OSError today.

- eng/scripts/audit_bundled_binaries.py (new): allowlist-driven ELF/Mach-O/PE

  dependency audit (gate step 3). Every dep must be BUNDLED, BASE (OS/libc), or

  DECLARED (openssl/krb5 [linux], vc14_runtime [win]); anything else fails. ELF

  binaries that need a bundled sibling must carry an \ RUNPATH; macOS

  absolute non-system install names (e.g. /opt/homebrew) fail regardless of

  basename; --require-arch asserts Mach-O slices (the Intel-agent substitute for

  the arm64 runtime import). Validated locally: Windows PASS, Linux FAIL on the

  missing libltdl.so.7, macOS FAIL on libodbc.2.dylib's /opt/homebrew libltdl.
…e bulk copy)

Restore path for PR #737: the win-arm64 wheel now vendors the matching arm64
mssql_py_core, so stop unconditionally stripping it. bld.bat keeps the core when
the extracted wheel has a matching-arch native ext (mssql_py_core.cp<ver>-<arch>.pyd)
-- shipping bulk copy on win-arm64 -- and strips only when a legacy x64 core is
present (a pre-#737 wheel), so the package never carries a core that can't load on
the target. Self-healing: bulk copy turns on automatically once the arm64-core wheel
is the conda-build input, with no build failure during the rollout gap.
Gaurav Sharma (bewithgaurav) pushed a commit that referenced this pull request Sep 2, 2026
…signed wheel) (#734)

### Work Item / Issue Reference  
<!-- 
IMPORTANT: Please follow the PR template guidelines below.
For mssql-python maintainers: Insert your ADO Work Item ID below 
For external contributors: Insert Github Issue number below
Only one reference is required - either GitHub issue OR ADO Work Item.
-->

<!-- mssql-python maintainers: ADO Work Item -->
>
[AB#47315](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/47315)

-------------------------------------------------------------------

### Summary
First of a series splitting the large conda onboarding branch (#720)
into smaller, reviewable PRs.

Adds the conda-build **recipe** that repackages the prebuilt,
ESRP-signed `mssql-python` wheel into a **self-contained** conda package
(it vendors the ODBC Driver 18 payload — there is no separate companion
package). The recipe compiles nothing; it repackages the already-signed
wheel, fully offline.

**Files**
- `conda/mssql-python/meta.yaml` — recipe manifest (version from
`MSSQL_PYTHON_VERSION`; repackage-safe build flags so conda-build never
rewrites/scans the signed binaries).
- `conda/mssql-python/build.sh` — Linux/macOS: install/extract the code
+ odbc wheels into site-packages, then stamp the relative `$ORIGIN`
RUNPATH climb (#563) onto the vendored Linux driver so it resolves the
env's own krb5/openssl.
- `conda/mssql-python/bld.bat` — Windows: extract the code + odbc
wheels; on **win-arm64** it strips the x64 `mssql_py_core` (no arm64
build exists yet, so bulk copy is a documented limitation on that
platform).
- `conda/.gitattributes`

**Safety:** this recipe is **dormant on `main`** — nothing runs it until
the standalone conda build pipeline (a follow-up PR) invokes it, so
merging is safe and changes no existing behavior.

Follow-up PRs in this split: (2) conda build pipeline + the gates/probes
it runs, (3) conda release pipeline, (4) the GitHub conda-audit
workflow.
… messages name the real param (buildDefinitionId) - Copilot review
…unt, harden TLS probe

- Rename test_026_driver_load_probe.py -> test_033 (main already has
  test_026_windows_dll_search.py from #735; restores the unique-NNN convention;
  027/032 stay reserved for the release slice).
- consolidate-conda-artifacts-job.yml: fix stale accounting — the pipeline now
  cross-builds win-arm64 (py3.12-3.14), so TOTAL is 28, not 25 (only musllinux is
  excluded now); a release gate keyed to "25 / no win-arm64" would be wrong.
- build-conda-packages.sh: make the conda_builder create idempotent (env remove
  first) so a reused agent/workdir doesn't fail under set -e.
- tls_connect_probe.py: neutral "TLS handshake did not complete" wording (an
  ssl-routines / cert error is not necessarily an unreachable backend); handle
  MS-ODBCSTR }} escaped braces in _split_top_level.
… idempotent conda_builder, neutral TLS wording + }} brace escaping
…y audit (twin of assert_pe_machine.py); +3 tests
…it (lipo/otool)' claims; osx-arm64 arch is trusted from the universal2 wheel tag (no Mach-O check), real guards are the PE + ELF audits
…ad targetArch param/condition (win-arm64 is distinguished by condaTargetSubdir); 74 probe tests pass
…r + two odbc no-op branches from build-conda-packages.ps1 (dead in the self-contained model; the .sh twin never had them); -27 lines, PS AST clean
…on", }} test, distro-scope doc

- assert_pe_machine.py: assert the native binding (mssql_python/ddbc_bindings*.pyd)
  AND the vendored ODBC driver DLLs are BOTH present, independently (was: >=1 native
  file) -- win-arm64 skips the runtime import, so this is its presence gate. +2 tests.
- driver_load_probe.py: drop "missing companion" from the failure label -- the package
  is self-contained (vendors the ODBC payload), there is no separate companion.
- tests/test_028: add a }} escaped-brace case for _split_top_level (Medium: '}}' is a
  literal '}' in MS-ODBCSTR, so a value with '}}' + internal ';' must not be mis-split).
- audit_bundled_binaries.py: document that the Linux audit validates every DISCOVERED
  distro/arch dir but does not assert a required distro SET (the wheel is the source of
  truth), so a wholesale-missing distro is a wheel-build concern, not caught here.

96 probe/audit unit tests pass; black clean.
…CRET variable, not a plaintext queue-time parameter (ADO leaves params unmasked); define condaTlsProbeConn in a variable group/Key Vault, unset = probe skips (Sumit, Medium)
Jahnvi Thakkar (jahnvi480) added a commit that referenced this pull request Sep 3, 2026
Second slice of the conda onboarding split (#720), after the recipe (#734, merged).
Adds the OneBranch conda-build pipeline that runs conda-build against the recipe already
on main and produces a consolidated conda/ artifact tree, plus the pure, no-DB validation
tooling the pipeline gates on.

Pipeline (OneBranchPipelines/):
- conda-build-pipeline.yml + the per-platform build/validate step templates
  (conda-build-validate-step{,-posix}.yml) and the consolidate-artifacts job (win-arm64
  py3.12-3.14 included in the accounting).
- build-conda-packages.{ps1,sh}: drive conda-build per leg; idempotent builder env
  (set -e safe). The TLS-probe connection string is sourced from a SECRET variable
  (variable group / Key Vault) mapped into the step env, never a plaintext queue-time
  parameter that ADO would leave unmasked in logs.

Validation tooling (conda/, eng/scripts/) + pure no-DB tests (tests/test_028-033):
- tls_connect_probe.py / driver_load_probe.py: import-time TLS-handshake and driver-load
  probes. The connection-string splitter honors MS-ODBCSTR }} brace escaping so a braced
  password is never mis-split at an internal ';'; neutral failure labels for triage.
- assert_pe_machine.py: Windows PE machine assert -- verifies the native binding
  (ddbc_bindings*.pyd) AND the vendored ODBC driver DLLs are both present and match the
  package arch (win-arm64 skips the runtime import, so this is its presence gate).
- audit_bundled_binaries.py: Linux RUNPATH self-containment audit + ELF e_machine arch
  gate (linux-64==x86_64, linux-aarch64==aarch64).

Scope: BUILD pipeline only. The release/publish steps, release-metadata validator, and
re-solve drift gate move to a follow-up PR. The osx-arm64 slice is trusted from the
universal2 wheel tag (no Mach-O audit is claimed); the enforced arch guards are the PE
and ELF checks. The GitHub conda-audit workflow and the product-code changes are
separate slices.

All probe/audit unit tests pass; black clean. Validated on a NonOfficial ADO build run.
…rse, shared conda reader, ldd/TLS hardening

Should-fix (before the release pipeline publishes):
- assert_pe_machine.py: the Windows presence gate now requires the CORE driver
  (msodbcsql18*.dll) specifically, not just any vendored .dll -- a support-DLL-only
  package (e.g. only mssql-auth) with the core driver missing would otherwise pass, and
  on win-arm64 (runtime import skipped) this is the sole check. +1 test.
- tls_connect_probe.py: drop the false-positive-prone 18456+'login' arm (a pre-TLS
  'Login timeout ... 18456' carries both '18456' and 'login' and would false-pass this
  fail-closed gate); keep only the locale-independent SQLSTATE 28000. +2 tests.
- conda-build-pipeline.yml: wire CONDA_TLS_PROBE_REQUIRED via a new enableMandatoryTlsGate
  parameter (default off) so the mandatory-TLS mode is turnable-on at release together
  with the secret conn -- it was previously set nowhere (the fail-closed mode shipped inert).
- eng/scripts/_conda_pkg.py: extract the shared .conda/zstd/tar + info/index.json reader
  used by BOTH audit scripts (was duplicated; pylint R0801) into one sibling module.

Real gaps:
- audit_bundled_binaries.py: parse the openssl range pin properly (operator+version per
  clause) so '>=3,<40' no longer false-passes ('<40' merely CONTAINS '<4'), while conda's
  canonical '<4.0a0' still passes and a bare '>=3' (no upper) correctly fails. +2 tests.
- build-conda-packages.sh: the ldd reachability gate now clears LD_LIBRARY_PATH (so the
  RUNPATH $ORIGIN climb ALONE must reach the prefix -- an ambient LD_LIBRARY_PATH could
  otherwise mask a broken RUNPATH) and requires resolution under $PREFIX/lib, not anywhere
  under $PREFIX.

Ponytail:
- audit_bundled_binaries.py: drop the RUNPATH canonical-ORDER check (the loader searches
  all entries regardless of order; the set-membership check already pins {$ORIGIN, climb}).
- consolidate-conda-artifacts-job.yml: correct the total-count log (25 -> 28).

black + flake8 + bash -n clean; mypy clean on the scripts; 112 probe/audit unit tests pass.
Jahnvi Thakkar (jahnvi480) added a commit that referenced this pull request Sep 3, 2026
Second slice of the conda onboarding split (#720), after the recipe (#734, merged).
Adds the OneBranch conda-build pipeline that runs conda-build against the recipe already
on main and produces a consolidated conda/ artifact tree, plus the pure, no-DB validation
tooling the pipeline gates on.

Pipeline (OneBranchPipelines/):
- conda-build-pipeline.yml + the per-platform build/validate step templates
  (conda-build-validate-step{,-posix}.yml) and the consolidate-artifacts job (win-arm64
  py3.12-3.14 included in the accounting).
- build-conda-packages.{ps1,sh}: drive conda-build per leg; idempotent builder env
  (set -e safe). The TLS-probe connection string is sourced from a SECRET variable
  (variable group / Key Vault) mapped into the step env, never a plaintext queue-time
  parameter that ADO would leave unmasked in logs.

Validation tooling (conda/, eng/scripts/) + pure no-DB tests (tests/test_028-033):
- tls_connect_probe.py / driver_load_probe.py: import-time TLS-handshake and driver-load
  probes. The connection-string splitter honors MS-ODBCSTR }} brace escaping so a braced
  password is never mis-split at an internal ';'; neutral failure labels for triage.
- assert_pe_machine.py: Windows PE machine assert -- verifies the native binding
  (ddbc_bindings*.pyd) AND the vendored ODBC driver DLLs are both present and match the
  package arch (win-arm64 skips the runtime import, so this is its presence gate).
- audit_bundled_binaries.py: Linux RUNPATH self-containment audit + ELF e_machine arch
  gate (linux-64==x86_64, linux-aarch64==aarch64).

Scope: BUILD pipeline only. The release/publish steps, release-metadata validator, and
re-solve drift gate move to a follow-up PR. The osx-arm64 slice is trusted from the
universal2 wheel tag (no Mach-O audit is claimed); the enforced arch guards are the PE
and ELF checks. The GitHub conda-audit workflow and the product-code changes are
separate slices.

All probe/audit unit tests pass; black clean. Validated on a NonOfficial ADO build run.
…v, not base (parity with .sh)

The .ps1 installed conda-build<26 into `base` (and pip-installed zstandard there), while the
.sh port uses a DEDICATED `conda_builder` env precisely because a pre-installed conda whose
base is pinned to a python no conda-build<26 supports (e.g. 3.14) makes a base install
UNSOLVABLE. The .ps1 was doing exactly what the .sh warns against -- latent on a reused /
self-hosted agent or any host with a system conda on PATH (Gaurav review).

- create `conda_builder` (conda-forge, --override-channels) with conda-build<26 + zstandard,
  with a best-effort pre-remove (ErrorActionPreference flipped to Continue for that one step
  -- the PS equivalent of the bash port's `|| true` -- then restored to Stop).
- run conda-build, the RUNPATH audit, and the win-arm64 PE-machine assert via
  `conda run -n conda_builder` (drops the separate `pip install zstandard` into base).

Windows-only change; needs an ADO run to confirm the win-64 + win-arm64 legs stay green.
…rom the conda BUILD pipeline

The linux-64 leg failed at the Encrypt=yes TLS gate: with the `condaTlsProbeConn` secret
undefined, ADO passed CONDA_TLS_PROBE_CONN through as the LITERAL string
`$(condaTlsProbeConn)` (non-empty, no `=`), and with enableMandatoryTlsGate on that tripped
`TLS_PROBE_MISCONFIGURED` (exit 1).

A validation gate should not be a queue-time human toggle, and the live TLS gate needs a
reachable SQL Server + a secret connection string -- release/integration infra the BUILD
pipeline does not have. So it is removed from the build pipeline entirely (the
enableMandatoryTlsGate parameter + CONDA_TLS_PROBE_CONN + CONDA_TLS_PROBE_REQUIRED). The
masking-immune static RUNPATH audit (audit_bundled_binaries.py) already guards the OpenSSL
layout at build time; the live Encrypt=yes gate moves to the release pipeline, where the
secret is always present so it runs UNCONDITIONALLY (no toggle).

The minimal-base ldd reachability gate stays opt-in: it is only VALID on a curated minimal
base (it fails closed on a full agent's system krb5/libltdl), a real environmental
constraint rather than a gratuitous switch.
- tls_connect_probe.py: _split_top_level now models a SINGLE-LEVEL braced value matching the
  production parser (connection_string_parser.py::_parse_braced_value) -- an inner `{` is a
  literal (not a nested open) and a `{` is only a brace-open at a value's START, so
  `Pwd={a{b};Encrypt=no` splits correctly and force_tls no longer emits a duplicate
  `encrypt` that mssql_python.connect would reject. (The production parser cannot be imported
  here: it pulls in mssql_python -> the native ddbc_bindings extension, which this standalone
  probe must stay importable / unit-testable without.)
- audit_bundled_binaries.py::_openssl_range_ok: the upper bound is valid only as an EXCLUSIVE
  `<` at numeric release 4.0(.0...) -- `<4`, `<4.0`, `<4.0.0`, `<4.0a0` pass; `<=4`, `<4.1`,
  `<4.0.1` (each admitting some openssl 4.x) now correctly FAIL.
- audit_bundled_binaries.py: an unknown `linux-*` subdir with no _SUBDIR_MACHINE mapping now
  FAILS CLOSED instead of proceeding with expected_machine=None and silently skipping the ELF
  architecture gate.
- _conda_pkg.py::iter_payload_members: a `.conda` missing its pkg-*.tar.zst now RAISES (was a
  bare `return` -> silent empty iteration); both audit scripts convert that to a violation.

The Medium (the job-level TLS secret never reaching the posix `bash:` step) is already
resolved: the live Encrypt=yes gate was removed from the build pipeline entirely in the
previous commit (it moves to the release pipeline, where the secret is always present so the
gate is unconditional).

+7 unit tests (119 pass). black + flake8 clean; mypy clean on the scripts.
…ases duplicated in test_028 (no coverage loss; removes a now-stale 18456-arm test name)
…ormat validation)

pythonVersions/pythonVersion (and its sibling condaTargetSubdir) were interpolated as raw
${{ parameters.* }} directly into the bash / PowerShell script text. Match the repo's
secret-handling convention (as already done for condaTlsProbeConn): surface both via a
step-level env: block and VALIDATE them in-script before use, so a value can neither inject
into the script nor pass through as a malformed version list.

- posix: PYTHON_VERSIONS must be empty (auto-detect) or comma-separated X.Y; CONDA_TARGET_SUBDIR
  must be empty (native) or a known conda subdir -- also fixed the staging TARGET_SUBDIR= that
  still interpolated the parameter (whole-class, not just the one call site).
- windows: PYTHON_VERSION must be empty or a single X.Y; same subdir allowlist.

Adversarially tested: '3.10;rm -rf /', bare '3', a trailing comma, a comma-list on the
single-version Windows leg, and an unknown subdir are all REJECTED; valid X.Y lists, empty
(auto-detect), and win-arm64 are ACCEPTED. bash -n clean.
Same silent-pass class the reviewer filed, on lines the earlier passes didn't reach:

- conda-build-pipeline.yml: drop the unpinned `--privileged multiarch/qemu-user-static`
  public image (SDL); qemu-user-static + binfmt-support already register the aarch64 handler
  on apt install -- verify /proc/sys/fs/binfmt_misc/qemu-aarch64 and fail loudly instead.
- audit_bundled_binaries.py::_openssl_range_ok: replace the prefix-regex parse with an
  ALLOWLIST of canonical bound spellings -- '>=3|>=1' (conda OR), a garbage clause,
  '<4garbage', '<=4', '<4.1' now all FAIL CLOSED.
- audit_bundled_binaries.py: an EMPTY RUNPATH entry (trailing/double ':' = current-directory
  search) now fails -- _entries() dropped it, letting '$ORIGIN:' pass the exact-{$ORIGIN,
  climb} check.
- audit_bundled_binaries.py: the DT_NEEDED check now anchors on the '.so' soname, so
  libkrb5support.so no longer satisfies a required libkrb5.so.
- tls_connect_probe.py::tls_completed: match SQLSTATE 28000 ONLY in SQLSTATE context
  (bracketed/quoted or after the label), not as a bare substring (a ':28000' port or a
  'sql28000' host would false-pass -- same class as the dropped bare-'18456' arm).
- tls_connect_probe.py::_redact: mask a no-'=' segment to '<<NO-VALUE>>' instead of echoing
  the raw token (a mis-split braced password could land there).
- build-conda-packages.ps1: document that the win-arm64 best-effort real-create masking is
  BOUNDED by the blocking build + dry-run solve + PE/static arch gates (no logic change).

+5 adversarial unit tests (120 pass). black + flake8 + bash -n + PS AST clean.
…macosx* like the odbc glob, so a stray Linux cpXY wheel can't be picked up. NOTE: recipe file from #734 (on main); deliver via its own PR to main, not #744.
… + print, not 2>$null) so a real (non-arch) create/run failure is diagnosable, not swallowed (devil's-eye)
…rchestrator

Replace build-conda-packages.ps1 + build-conda-packages.sh with one cross-platform Python orchestrator (OneBranchPipelines/scripts/build_conda_packages.py) and collapse both conda-build-validate step templates to invoke it; eliminates the duplicated PowerShell/bash logic and the cmd /c exit-0 hack. Also folds in three-lens review findings: #1 assert_pe_machine.py requires a vendored mssql-auth DLL on win-arm64 (loader throws at connect if absent); #3 audit_bundled_binaries.py scans the whole payload for vendored crypto .so, not just /libs/linux/; #4 build_conda_packages.py clears bld/ and LINKS and pins mssql-python==version in the verify env; #5 conda-build-pipeline.yml documents that the win-arm64 python set (3.12-3.14) is a deliberately fixed matrix. Tests: add win-arm64 missing-auth-DLL (test_030) and vendored-crypto-outside-libs-linux (test_029) failure cases.
…ator

The orchestrator refactor deleted build-conda-packages.sh, but .github/workflows/conda-audit.yml still invoked it (and path-triggered on it), so the PR conda-audit gate would fail. Repoint the workflow to build_conda_packages.py (which derives both wheel versions from the filenames) and fix the standalone audit --root to the new per-subdir output layout (outputDir/<subdir>/bld). Also repoint two stale comments (tls_connect_probe.py, conda_resolve_check.py) that named the deleted scripts.
The Encrypt=yes TLS gate (_tls_gate + conda/tls_connect_probe.py) is opt-in via CONDA_TLS_PROBE_CONN/REQUIRED, which NO build-pipeline leg sets -- it always skips. It belongs to the release pipeline (live gate runs unconditionally there), so remove it from the build slice: strip _tls_gate from build_conda_packages.py, delete conda/tls_connect_probe.py + tests/test_028_tls_connect_probe.py + tests/test_031_tls_probe_required.py, and drop the test_031 reference from the conda-audit workflow. Recoverable from history (861a1a1) for the release-pipeline PR.
…e; aarch64 continueOnError

Finding #1 (blocking): verify() ran `python -c import mssql_python` from the agent cwd (the checkout root, which holds the un-built mssql_python/ + mssql_python_odbc/ source). For python -c, sys.path[0] is '' (cwd), so the SOURCE shadowed the conda-installed package -> ImportError: No ddbc_bindings module found; the verify gate validated source (or failed) instead of the built package. Fix: a thin verify() wrapper os.chdir's to the neutral per-leg build dir before the phase (the Python equivalent of the deleted scripts' cd), so every verify + reachability subprocess inherits it; renamed the body to _verify_impl. New tests/test_034_conda_verify_cwd.py asserts the import probes run from the workdir (proven to FAIL without the wrapper) and is wired into the conda-audit gate. Finding #2 (medium): linux-aarch64 lacked continueOnError:true while sharing the BuildConda job with native linux-64 -- a QEMU flake failed the job and skipped the condition:succeeded() publish, discarding validated linux-64 packages. Added continueOnError:true (blast-radius isolation, matching win-arm64/osx-arm64; the release gate still requires the subdir) and reconciled the contradictory MANDATORY-blocking comment.
…x (+recipe_root abspath, failure-path test)

Review consensus should-fixes hardening the finding #1 cwd fix (all optional, none blocked merge): (1) _import_probe() -- the verify imports now assert mssql_python(_odbc).__file__ resolves under sys.prefix (the conda env), so a stray PYTHONPATH/.pth that os.chdir cannot catch still FAILS the leg (proven: a PYTHONPATH shadow raises AssertionError; the installed pkg passes). One shared helper for both imports. (2) main() normalizes --recipe-root to an absolute path so verify()'s os.chdir can never misresolve the driver_load_probe from a relative CLI arg (CI already passes absolute). (3) test_034 gains a failure-path case asserting the wrapper's finally restores cwd when the phase raises; the import matcher was updated for the new -c body.
…n list, condaSubdir cross-target)

Review low findings on conda-build-validate-step.yml: the pythonVersion comment said a single X.Y but the param takes a comma-separated list (win-64 follows pythonVersions; win-arm64 uses a fixed 3.12-3.14 set); and condaSubdir said 'Must match the platform of THIS agent', which is wrong for the win-arm64 cross leg (built on an x64 agent via condaTargetSubdir). Reworded both to describe the leg's target/staging subdir and the comma-separated versions. Comment-only; the posix twin already had accurate wording.
… --flag=value (PS 5.1 empty-arg drop)

The ADO win-64 conda leg failed with 'argument --conda-target-subdir: expected one argument'. Windows PowerShell 5.1 DROPS an empty '' argument to a native command, so on a native leg (win-64 -> empty condaTargetSubdir) the space form --conda-target-subdir '' vanished and argparse saw the next flag. Reproduced under PS 5.1 (5.1.26100.8875, the agent's shell): the space form exits 2 (the exact ADO error), the =value form parses. Switched both optionally-empty args (--conda-target-subdir, --python-versions) to the --flag=value form, which stays one token even when empty. POSIX/bash is unaffected (bash preserves empty args), so only the Windows template changes. Verified win-64 (empty target-subdir) and win-arm64 (non-empty) both parse under PS 5.1.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: large Substantial code update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants