Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dcafa6d
FEAT: Add conda packaging recipes and publish tooling for mssql-python
jahnvi480 Aug 14, 2026
a1ff766
FIX: Address PR review on conda recipes and harness
jahnvi480 Aug 14, 2026
0013f70
FIX: address round-2 conda recipe review (#563)
jahnvi480 Aug 14, 2026
ca0be45
Merge branch 'main' into jahnvi/conda-onboarding
jahnvi480 Aug 17, 2026
e10a4de
CHORE: sync conda build/validate/publish architecture from ADO
jahnvi480 Aug 18, 2026
8e7f217
CHORE: build mssql-python-odbc conda once (python-agnostic) in ODBC_B…
jahnvi480 Aug 18, 2026
43f23a8
Merge branch 'main' into jahnvi/conda-onboarding
jahnvi480 Aug 18, 2026
1f749d1
FIX: fail-closed conda driver-load probe + metadata-based release gate
jahnvi480 Aug 18, 2026
a8d42d6
FIX: genuinely cross-build osx-arm64 conda on the Intel macOS agent (…
jahnvi480 Aug 18, 2026
4695f6b
FIX: declare conda security deps for the ODBC driver companion (P1-4)
jahnvi480 Aug 18, 2026
599f03f
FIX: complete self-contained glibc ODBC payload (bundle libltdl.so.7)
jahnvi480 Aug 18, 2026
6d67a12
FIX: bundle musl libltdl too, add LGPL notice, add binary-audit gate
jahnvi480 Aug 18, 2026
d6e790c
FIX: drop unused libodbc.2.dylib (removes macOS Homebrew-libtool lan…
jahnvi480 Aug 18, 2026
81a34cf
FIX: keep shipped libodbc.2.dylib; make audit reachability-aware
jahnvi480 Aug 18, 2026
4722248
FIX: make conda-declared openssl/krb5 reachable from the ODBC driver …
jahnvi480 Aug 18, 2026
7d2fc1e
FEAT: add a live Encrypt=yes TLS gate that proves the OpenSSL backend…
jahnvi480 Aug 18, 2026
9a2f4c3
FIX: bundle Linux libltdl.so.7 + dual-RUNPATH driver binaries (glibc …
jahnvi480 Aug 18, 2026
e136c1e
FIX: conda openssl Linux-only + pinned <4; surface dlopen error text …
jahnvi480 Aug 19, 2026
50d02fa
FIX: splat DriverCondaDir as hashtable in conda-build step (fixes CON…
jahnvi480 Aug 19, 2026
79eec5c
FIX: skip conda-only unit tests when conda/ sources absent (installed…
jahnvi480 Aug 19, 2026
abd0f66
Declare libtool (libltdl) for conda instead of vendoring it on Linux
jahnvi480 Aug 19, 2026
a0923d1
Merge branch 'main' into jahnvi/conda-onboarding
jahnvi480 Aug 19, 2026
6435a8a
Revert "Declare libtool (libltdl) for conda instead of vendoring it o…
jahnvi480 Aug 19, 2026
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
50 changes: 50 additions & 0 deletions OneBranchPipelines/build-release-package-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,9 @@ extends:
odbcDependsOn:
- ConsolidateOdbc
installOdbcWheel: true
# Conda: build+validate conda packages on every x64 leg (win-64).
# ARM64 has no conda target (can't provision a win-arm64 host on x64).
buildConda: ${{ eq(config.arch, 'x64') }}

# =========================
# MACOS BUILD STAGES
Expand Down Expand Up @@ -440,6 +443,8 @@ extends:
odbcDependsOn:
- ConsolidateOdbc
installOdbcWheel: true
# Conda: build+validate the osx-arm64 conda package on every macOS leg.
buildConda: true

# =========================
# LINUX BUILD STAGES
Expand Down Expand Up @@ -474,6 +479,11 @@ extends:
odbcDependsOn:
- ConsolidateOdbc
installOdbcWheel: true
# Conda: build on BOTH manylinux_2_28 legs — x86_64 natively (linux-64)
# and aarch64 cross-targeted via QEMU binfmt (linux-aarch64), matching
# the two glibc Linux wheels on PyPI. musl has no conda target, so the
# musllinux legs still skip conda.
buildConda: ${{ eq(config.tag, 'manylinux_2_28') }}

# =========================
# CONSOLIDATE STAGE
Expand Down Expand Up @@ -566,3 +576,43 @@ extends:
# mssql-python build stages now install the external mssql-python-odbc wheel
# (from ConsolidateOdbc) and run the full pytest suite against it — so the
# external-package resolution is already validated end-to-end during the build.

# =========================================================================
# CONSOLIDATE CONDA STAGE
# =========================================================================
# Gathers the conda packages emitted by the conda-producing legs (win-64,
# osx-64, osx-arm64, linux-64, linux-aarch64) into a single conda/ tree and
# publishes them as `drop_ConsolidateConda_ConsolidateArtifacts`. BEST-EFFORT:
# it depends ONLY on the conda-producing legs (not the win-arm64 or musl legs,
# which have no conda target) and its job never hard-fails on a short count, so
# a conda hiccup can never block the wheel deliverable. The release pipeline
# enforces the hard conda gate (required subdirs + #706 pairing) before
# anything is published.
- stage: ConsolidateConda
displayName: 'Consolidate All Conda Packages'
dependsOn:
# win-64 (x64 only — win-arm64 has no conda host on an x64 agent).
# NOTE: these legs now emit ONLY the per-Python mssql-python (binding) conda;
# the win-64 companion (mssql-python-odbc) is built once in ODBC_BuildAll.
- Win_py310_x64
- Win_py311_x64
- Win_py312_x64
- Win_py313_x64
- Win_py314_x64
# osx-64 + osx-arm64 (every macOS universal2 leg builds BOTH: arm64 native,
# x64 under Rosetta 2)
- MacOS_py310
- MacOS_py311
- MacOS_py312
- MacOS_py313
- MacOS_py314
# linux-64 (glibc x86_64 host, native) + linux-aarch64 (x86_64 host + QEMU)
- Linux_manylinux_2_28_x86_64
- Linux_manylinux_2_28_aarch64
# win-64 companion (mssql-python-odbc) is built ONCE here (Python-agnostic).
- ODBC_BuildAll
jobs:
- template: /OneBranchPipelines/jobs/consolidate-conda-artifacts-job.yml@self
parameters:
# Match effectiveOneBranchType so scheduled builds run as 'Official'.
oneBranchType: '${{ variables.effectiveOneBranchType }}'
80 changes: 80 additions & 0 deletions OneBranchPipelines/dummy-release-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ parameters:
type: boolean
default: true # Safe to enable - uses Maven ContentType for testing

# [TEST] Conda release readiness. When true, a DECOUPLED stage downloads the
# consolidated conda packages (drop_ConsolidateConda_ConsolidateArtifacts) and
# enforces the hard completeness + #706 pairing gate. It publishes NOTHING; it is
# the safe place to prove the conda set is release-ready.
- name: releaseConda
displayName: '[TEST] Validate Conda Release Readiness (does not publish)'
type: boolean
default: false

# [TEST] Conda publish rehearsal. Wires the SAME anaconda-client publish path as
# the official pipeline for workflow testing. condaChannel is EMPTY by default and
# the publish step refuses to run without it, so this TEST pipeline can never push
# to the production 'microsoft' channel by accident. Point it at a personal/test
# Anaconda.org channel to rehearse the upload.
- name: publishToConda
displayName: '[TEST] Publish Conda Packages to Anaconda.org (rehearsal - set a TEST channel)'
type: boolean
default: false

- name: condaChannel
displayName: '[TEST] Anaconda.org channel/org (leave empty to block publishing; set a TEST channel to rehearse)'
type: string
default: ''

# Variables
variables:
# Common variables
Expand All @@ -42,6 +66,11 @@ variables:
- group: 'ESRP Federated Creds (AME)' # Contains ESRP signing credentials
- group: 'Symbols Publishing' # Contains SymbolServer, SymbolTokenUri variables

# ANACONDA_API_TOKEN for the conda publish rehearsal lives in this variable group.
# Included ONLY when publishing so the validate-only path doesn't require it.
- ${{ if eq(parameters.publishToConda, true) }}:
- group: 'Anaconda Publishing'

# Select which consolidated artifact to download based on the target package.
# Both are produced by the same build pipeline (definition 2199):
# mssql-python -> drop_Consolidate_ConsolidateArtifacts
Expand Down Expand Up @@ -422,3 +451,54 @@ extends:
Write-Host "3. Verify symbols in SqlClientDrivers org (if published)"
Write-Host "4. For PRODUCTION release, use official-release-pipeline.yml"
Write-Host "====================================="

# =====================================================================
# [TEST] CONDA RELEASE READINESS (decoupled; publishes nothing)
# =====================================================================
# Runs when releaseConda=true OR publishToConda=true. Downloads the consolidated
# conda packages from build definition 2199 and enforces the release-time hard
# gate (exact count + #706 binding/companion pairing). dependsOn: [] keeps it
# independent of the dummy release stage. When publishToConda=true, a second
# releaseJob rehearses the Anaconda.org upload (to whatever TEST channel is
# supplied), but ONLY after this gate succeeds.
- ${{ if or(eq(parameters.releaseConda, true), eq(parameters.publishToConda, true)) }}:
- stage: ValidateCondaRelease
displayName: '[TEST] Validate & Publish Conda Release'
dependsOn: []
jobs:
- job: ValidateConda
displayName: '[TEST] Validate consolidated conda packages'
pool:
type: windows
isCustom: true
name: Python-1ES-pool
demands:
- imageOverride -equals PYTHON-1ES-MMS2022
variables:
ob_outputDirectory: '$(Build.ArtifactStagingDirectory)'
steps:
- template: /OneBranchPipelines/steps/conda-release-step.yml@self
parameters:
labelPrefix: '[TEST] '

# [TEST] Conda publish rehearsal (releaseJob). Runs only when
# publishToConda=true and ONLY after the ValidateConda gate succeeds. The
# publish step refuses to run with an empty condaChannel, so this can
# never reach the production 'microsoft' channel by default.
- ${{ if eq(parameters.publishToConda, true) }}:
- job: PublishConda
displayName: '[TEST] Publish Conda Packages to Anaconda.org'
dependsOn: ValidateConda
templateContext:
type: releaseJob
pool:
type: windows
variables:
ob_outputDirectory: '$(Build.ArtifactStagingDirectory)'
WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest'
steps:
- template: /OneBranchPipelines/steps/conda-publish-step.yml@self
parameters:
condaChannel: ${{ parameters.condaChannel }}
condaLabel: 'main'
labelPrefix: '[TEST] '
125 changes: 125 additions & 0 deletions OneBranchPipelines/jobs/consolidate-conda-artifacts-job.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Consolidate Conda Artifacts Job Template
# Collects the per-platform conda packages (mssql-python binding + its companion
# mssql-python-odbc) that each build leg staged under conda/<subdir>/ and gathers
# them into a single conda/ tree for the release pipeline to publish.
#
# BEST-EFFORT (build pipeline): conda is a downstream repackage of the ESRP-signed
# wheels and must NEVER block the primary wheel deliverable, so a missing/short set
# only WARNS here. The HARD count gate lives in the release pipeline (which refuses
# to publish an incomplete conda set), symmetric with how the wheel/odbc drops are
# best-effort collected in the build and gated at release time.
#
# Expected packages (validated conda subdirs). The per-Python BINDING (mssql-python)
# is emitted by each build leg. The version-locked COMPANION (mssql-python-odbc) is
# still emitted per-Python by the macOS/Linux legs, but on Windows it is built ONCE
# (Python-agnostic -- Lib\site-packages is not version-pathed) in the ODBC_BuildAll
# stage and collected here from that stage's drop instead of once per Python leg:
# win-64 : 5 py x mssql-python + 1 companion (once, ODBC_BuildAll) = 6
# osx-64 : 5 py x 2 (Intel Mac, cross-built under Rosetta 2) = 10
# osx-arm64 : 5 py x 2 (Apple Silicon, native) = 10
# linux-64 : 5 py x 2 (glibc x86_64 host, native) = 10
# linux-aarch64 : 5 py x 2 (x86_64 host + QEMU, best-effort) = 10
# ------------------------------------------------------------------------------
# TOTAL (PyPI parity minus win-arm64 + musllinux) = 46
# win-arm64 (no import-validation host on x64) and musllinux (no conda musl subdir)
# are intentionally NOT conda-built. This job is BEST-EFFORT and never hard-fails on
# a short set; the release pipeline's conda-release-step enforces the hard gate
# (required subdirs present + #706 pairing) before anything is published.
parameters:
- name: oneBranchType
type: string
default: 'Official'

jobs:
- job: ConsolidateArtifacts
displayName: 'Consolidate All Conda Packages'
condition: succeeded()

pool:
type: linux
isCustom: true
name: Azure Pipelines
vmImage: 'ubuntu-latest'

variables:
# Consolidation only moves files; no binaries to scan.
- name: ob_sdl_binskim_enabled
value: false
- name: ob_outputDirectory
value: '$(Build.ArtifactStagingDirectory)'

steps:
- checkout: self
fetchDepth: 1

# The conda packages are staged INSIDE the mssql-python build-leg artifacts
# (drop_Win_*, drop_MacOS_*, drop_Linux_*) under conda/<subdir>/, EXCEPT the
# win-64 companion (mssql-python-odbc), which is built ONCE in ODBC_BuildAll and
# rides its drop under conda/win-64/. Scope the download to those stages so every
# leg's conda output plus the once-built Windows companion is gathered in one
# place. The wheels ride along in the download but are ignored below (we only
# pick *.conda / *.tar.bz2).
- task: DownloadPipelineArtifact@2
displayName: 'Download All Platform Artifacts'
inputs:
buildType: 'current'
itemPattern: |
drop_Win_*/**
drop_MacOS_*/**
drop_Linux_*/**
drop_ODBC_BuildAll_*/**
targetPath: '$(Pipeline.Workspace)/all-artifacts'

- bash: |
set -e
echo "Collecting conda packages (preserving <subdir>/ layout)..."
mkdir -p $(ob_outputDirectory)/conda

# Copy every mssql-python* conda package into conda/<subdir>/. The glob
# 'mssql-python*' matches BOTH the binding (mssql-python-*) and the
# companion (mssql-python-odbc-*); each build leg wrote them under a
# conda/<subdir>/ folder, so the parent dir name IS the target subdir.
found=0
while IFS= read -r p; do
subdir=$(basename "$(dirname "$p")")
mkdir -p "$(ob_outputDirectory)/conda/$subdir"
cp -v "$p" "$(ob_outputDirectory)/conda/$subdir/"
found=1
done < <(find $(Pipeline.Workspace)/all-artifacts -type f \( -name 'mssql-python*.conda' -o -name 'mssql-python*.tar.bz2' \))

echo ""
echo "Consolidated conda tree:"
find $(ob_outputDirectory)/conda -type f | sort

PKG_COUNT=$(find $(ob_outputDirectory)/conda -type f \( -name '*.conda' -o -name '*.tar.bz2' \) | wc -l)
echo ""
echo "Per-subdir conda package counts:"
for d in $(ob_outputDirectory)/conda/*/; do
[ -d "$d" ] || continue
sub=$(basename "$d")
n=$(find "$d" -type f \( -name '*.conda' -o -name '*.tar.bz2' \) | wc -l)
printf ' %-14s %s\n' "$sub" "$n"
done
echo "Total conda package count: $PKG_COUNT (full PyPI-parity set = 46)"

# BEST-EFFORT: warn only, never exit non-zero — a conda hiccup on any leg
# must not fail this build or block the wheel release. The release pipeline's
# conda-release-step enforces the hard gate (required subdirs + #706 pairing)
# before anything is published.
if [ "$found" != "1" ]; then
echo "##vso[task.logissue type=warning]No conda packages found in the build-leg artifacts."
else
echo "Collected $PKG_COUNT conda package(s) (best-effort; release-time gate enforces completeness)."
fi
displayName: 'Consolidate conda packages'

- task: PublishPipelineArtifact@1
displayName: 'Publish Consolidated Conda Artifacts'
inputs:
targetPath: '$(ob_outputDirectory)'
# Distinct name so it does not collide with the wheel consolidate artifact
# (drop_Consolidate_ConsolidateArtifacts) or the odbc one
# (drop_ConsolidateOdbc_ConsolidateArtifacts) in the same run. Matches the
# OneBranch auto-name for a stage named `ConsolidateConda`.
artifact: 'drop_ConsolidateConda_ConsolidateArtifacts'
publishLocation: 'pipeline'
80 changes: 80 additions & 0 deletions OneBranchPipelines/official-release-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,31 @@ parameters:
type: boolean
default: false # Safety: Default to false to prevent accidental releases

# Conda release readiness. When true, a DECOUPLED stage downloads the consolidated
# conda packages (drop_ConsolidateConda_ConsolidateArtifacts) and enforces the hard
# completeness + #706 pairing gate. It is independent of the wheel PyPI release, so
# a conda check can never block a wheel release.
- name: releaseConda
displayName: 'Validate Conda Release Readiness (does not publish)'
type: boolean
default: false

# Conda PRODUCTION publish to Anaconda.org. When true, AFTER the readiness gate
# (ValidateConda) passes, a releaseJob uploads the consolidated conda packages via
# anaconda-client (ESRP has no Conda ContentType). The version-locked companion
# (mssql-python-odbc) is uploaded BEFORE the binding (mssql-python) per #706.
# Decoupled from the wheel release; default false so a normal release never
# touches Anaconda.
- name: publishToConda
displayName: 'Publish Conda Packages to Anaconda.org (PRODUCTION)'
type: boolean
default: false

- name: condaChannel
displayName: 'Anaconda.org channel/org to publish conda packages to'
type: string
default: 'microsoft'

# Variables
variables:
# Common variables
Expand All @@ -42,6 +67,12 @@ variables:
- group: 'ESRP Federated Creds (AME)' # Contains ESRP signing credentials
- group: 'Symbols Publishing' # Contains SymbolServer, SymbolTokenUri variables

# ANACONDA_API_TOKEN for conda publishing lives in this org-scoped variable group.
# Included ONLY when actually publishing, so a normal wheel release doesn't require
# the group to exist.
- ${{ if eq(parameters.publishToConda, true) }}:
- group: 'Anaconda Publishing'

# Select which consolidated artifact to download/publish based on the target
# package. Both are produced by the same build pipeline (definition 2199):
# mssql-python -> drop_Consolidate_ConsolidateArtifacts
Expand Down Expand Up @@ -404,3 +435,52 @@ extends:
Write-Host "3. Verify package on PyPI: https://pypi.org/project/${{ parameters.releasePackage }}/"
Write-Host "4. Verify symbols in SqlClientDrivers org (if published)"
Write-Host "====================================="

# =====================================================================
# CONDA RELEASE READINESS (decoupled; does not gate the wheel release)
# =====================================================================
# Runs when releaseConda=true OR publishToConda=true. Downloads the consolidated
# conda packages from build definition 2199 and enforces the release-time hard
# gate (exact count + #706 binding/companion pairing). dependsOn: [] keeps it
# independent of ReleasePackages, so a conda problem can never block the wheel
# PyPI release. When publishToConda=true, a second releaseJob publishes to
# Anaconda.org, but ONLY after this gate succeeds.
- ${{ if or(eq(parameters.releaseConda, true), eq(parameters.publishToConda, true)) }}:
- stage: ValidateCondaRelease
displayName: 'Validate & Publish Conda Release'
dependsOn: []
jobs:
- job: ValidateConda
displayName: 'Validate consolidated conda packages'
pool:
type: windows
isCustom: true
name: Python-1ES-pool
demands:
- imageOverride -equals PYTHON-1ES-MMS2022
variables:
ob_outputDirectory: '$(Build.ArtifactStagingDirectory)'
steps:
- template: /OneBranchPipelines/steps/conda-release-step.yml@self

# Conda PRODUCTION publish (releaseJob). Runs only when publishToConda=true
# and ONLY after the ValidateConda gate succeeds, so we never upload an
# incomplete or mis-paired set. anaconda-client upload (ESRP has no Conda
# ContentType); companion-before-binding per #706.
- ${{ if eq(parameters.publishToConda, true) }}:
- job: PublishConda
displayName: 'Publish Conda Packages to Anaconda.org'
dependsOn: ValidateConda
templateContext:
type: releaseJob
isProduction: true
pool:
type: windows
variables:
ob_outputDirectory: '$(Build.ArtifactStagingDirectory)'
WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest'
steps:
- template: /OneBranchPipelines/steps/conda-publish-step.yml@self
parameters:
condaChannel: ${{ parameters.condaChannel }}
condaLabel: 'main'
1 change: 1 addition & 0 deletions OneBranchPipelines/scripts/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.sh text eol=lf
Loading
Loading