From 17df51013fa097f71b9bfc9a80490ff8613b42f4 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:17:02 -0700 Subject: [PATCH 1/9] Rename mssql-mock-tds-py Python package to mssql-mock-tds Change the PyPI distribution name and Python import module from mssql-mock-tds-py / mssql_mock_tds_py to mssql-mock-tds / mssql_mock_tds. The folder and Cargo package name stay mssql-mock-tds-py to avoid colliding with the existing mssql-mock-tds crate it depends on. Use a #[pyo3(name)] override so the pymodule fn keeps a distinct identifier from the dependency crate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql-mock-tds-py/pyproject.toml | 3 ++- mssql-mock-tds-py/src/lib.rs | 3 ++- .../rs-only-tests/test_mock_server_fedauth.py | 18 +++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/mssql-mock-tds-py/pyproject.toml b/mssql-mock-tds-py/pyproject.toml index 47728355..ec8957fd 100644 --- a/mssql-mock-tds-py/pyproject.toml +++ b/mssql-mock-tds-py/pyproject.toml @@ -3,7 +3,7 @@ requires = ["maturin>=1.4,<2.0"] build-backend = "maturin" [project] -name = "mssql-mock-tds-py" +name = "mssql-mock-tds" version = "0.1.0" description = "Python bindings for the mock TDS server for testing SQL Server clients" requires-python = ">=3.9" @@ -18,4 +18,5 @@ classifiers = [ ] [tool.maturin] +module-name = "mssql_mock_tds" features = ["pyo3/extension-module"] diff --git a/mssql-mock-tds-py/src/lib.rs b/mssql-mock-tds-py/src/lib.rs index b29e02d8..44f2918d 100644 --- a/mssql-mock-tds-py/src/lib.rs +++ b/mssql-mock-tds-py/src/lib.rs @@ -323,7 +323,8 @@ impl PyMockTdsServer { /// Python module for mock TDS server bindings #[pymodule] -fn mssql_mock_tds_py(m: &Bound<'_, PyModule>) -> PyResult<()> { +#[pyo3(name = "mssql_mock_tds")] +fn mssql_mock_tds_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; Ok(()) diff --git a/mssql-py-core/tests/rs-only-tests/test_mock_server_fedauth.py b/mssql-py-core/tests/rs-only-tests/test_mock_server_fedauth.py index 21743b0b..9560bb2b 100644 --- a/mssql-py-core/tests/rs-only-tests/test_mock_server_fedauth.py +++ b/mssql-py-core/tests/rs-only-tests/test_mock_server_fedauth.py @@ -20,7 +20,7 @@ # Try to import the mock TDS server Python bindings try: - import mssql_mock_tds_py + import mssql_mock_tds MOCK_TDS_PY_AVAILABLE = True except ImportError: MOCK_TDS_PY_AVAILABLE = False @@ -52,7 +52,7 @@ def mock_server_port(): @pytest.mark.skipif( not MOCK_TDS_PY_AVAILABLE, - reason="mssql_mock_tds_py not available. Build it with: cd mssql-mock-tds-py && maturin develop", + reason="mssql_mock_tds not available. Build it with: cd mssql-mock-tds-py && maturin develop", ) class TestMockServerFedAuth: """Test FedAuth (access token) authentication with mock TDS server.""" @@ -75,7 +75,7 @@ def test_connect_with_access_token(self, mock_server_port): # Create and start the mock server using Python bindings # Use tls=True to enable TLS encryption for secure token transmission - server = mssql_mock_tds_py.PyMockTdsServer(port=mock_server_port, tls=True) + server = mssql_mock_tds.PyMockTdsServer(port=mock_server_port, tls=True) with server: # Build client context for the mock server with access token auth @@ -126,7 +126,7 @@ def test_connect_with_unique_access_token(self, mock_server_port): # Generate a unique token for this test unique_token = f"unique_token_{secrets.token_hex(16)}" - server = mssql_mock_tds_py.PyMockTdsServer(port=mock_server_port, tls=True) + server = mssql_mock_tds.PyMockTdsServer(port=mock_server_port, tls=True) with server: client_context = { @@ -156,7 +156,7 @@ def test_connect_with_unique_access_token(self, mock_server_port): def test_mock_server_starts_successfully(self, mock_server_port): """Test that the mock TDS server starts and listens on the expected port.""" - server = mssql_mock_tds_py.PyMockTdsServer(port=mock_server_port) + server = mssql_mock_tds.PyMockTdsServer(port=mock_server_port) with server: # Verify we can connect at the TCP level @@ -178,7 +178,7 @@ def test_execute_query_with_access_token(self, mock_server_port): mock_token = "mock_token_for_query_execution" - server = mssql_mock_tds_py.PyMockTdsServer(port=mock_server_port, tls=True) + server = mssql_mock_tds.PyMockTdsServer(port=mock_server_port, tls=True) with server: client_context = { @@ -224,7 +224,7 @@ def test_get_all_connections(self, mock_server_port): token = "test_token_for_connection_list" - server = mssql_mock_tds_py.PyMockTdsServer(port=mock_server_port, tls=True) + server = mssql_mock_tds.PyMockTdsServer(port=mock_server_port, tls=True) with server: client_context = { @@ -254,7 +254,7 @@ def test_clear_connections(self, mock_server_port): import mssql_py_core import time - server = mssql_mock_tds_py.PyMockTdsServer(port=mock_server_port, tls=True) + server = mssql_mock_tds.PyMockTdsServer(port=mock_server_port, tls=True) with server: client_context = { @@ -284,7 +284,7 @@ def test_user_agent_format(self, mock_server_port): import mssql_py_core import time - server = mssql_mock_tds_py.PyMockTdsServer(port=mock_server_port, tls=True) + server = mssql_mock_tds.PyMockTdsServer(port=mock_server_port, tls=True) with server: client_context = { From e4fc1da9ce221dc86075f17660cde0608403f3f1 Mon Sep 17 00:00:00 2001 From: Saurabh Singh Date: Mon, 29 Jun 2026 20:55:32 -0700 Subject: [PATCH 2/9] Add sandbox pipeline to publish mock TDS artifacts (test-only) (#81) * Add sandbox OneBranch pipeline to publish mock TDS artifacts Adds .pipeline/OneBranch/PublishFeeds-Sandbox.yml, a manual-only, test-only pipeline that builds and (opt-in) publishes the mock TDS Python wheels and crate to the mssql-rs_Public Azure Artifacts feed. Publishing is gated behind publishPython/publishCrate parameters, both default false (build + dry run only). Adds a single-abi3-wheel build script for the Linux container build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Extend sandbox pipeline with ARM64 and macOS wheel jobs Adds Linux ARM64, Windows ARM64, and macOS universal2 mock wheel build jobs to the sandbox PublishMockPython stage and wires them into the UploadPython dependsOn. macOS builds with a new vendored-openssl passthrough feature on mssql-mock-tds-py so the universal2 wheel statically links OpenSSL instead of Homebrew's libssl. Linux jobs keep system libssl (auditwheel skip); Windows uses SChannel. Still sandbox/test-only and opt-in. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix OneBranch template resolution in sandbox pipeline The sandbox pipeline inlines its stages under extends.parameters.stages while extending the OneBranch governed template, so unqualified template: includes resolved against the GovernedTemplates repo and failed (e.g. cargo-authenticate-template.yml not found). Suffix every internal /.pipeline/... template include with @self so they resolve against this repo. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Conform sandbox artifact names to OneBranch convention The governed OneBranch job template requires PublishPipelineArtifact names to be drop___. Rename the five mock wheel artifacts accordingly so stage validation passes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Stamp sandbox wheel version as SemVer prerelease cargo metadata rejects the PEP 440 dotted '.dev' suffix in Cargo.toml ('unexpected character . after patch version number'), failing maturin before the build. Use the SemVer-valid '-dev' form for both pyproject.toml and Cargo.toml; PEP 440 normalizes it back to '.dev' for the wheel, and the matching versions satisfy maturin's pyproject/Cargo consistency check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix sandbox macOS PEP 668 install and inline crate version stamping macOS wheel build failed at 'pip install --user pipx' with externally-managed-environment (PEP 668) on the Homebrew Python. Bootstrap pipx with --break-system-packages only when missing, and install the wheel package the same way for the re-tag step. cargo publish --dry-run failed because the shared stamp-crate-versions.ps1 uses a global ^version regex that also rewrote the [dependencies.uuid] version field to the prerelease version, corrupting the uuid requirement. Stamp inline instead, replacing only the first ^version line (the [package] version) in mssql-tds and mssql-mock-tds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix macOS sandbox wheel version stamp for BSD sed The macOS Stamp step used the GNU-only empty-regex reuse form s//.../ which BSD sed rejects with "first RE may not be empty". Replace it with an explicit substitution pattern. Each mock manifest has exactly one line-starting version = (the package version), so a plain substitution stamps only that line. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Invoke wheel as a module in macOS re-tag step The --user/Homebrew wheel install does not place the wheel console script on PATH, so wheel tags failed with exit 127 (command not found). Call it as python3 -m wheel tags instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use UsePythonVersion for macOS mock wheel job Pin Python 3.12 via UsePythonVersion@0 instead of relying on the agent's externally-managed Homebrew python3. The selected interpreter is managed, writable, and on PATH, so the PEP 668 --break-system-packages workarounds and the off-PATH wheel/maturin console-script issues go away. Install maturin and wheel with python -m pip against that interpreter and re-tag with python -m wheel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Run mock wheel upload job on the custom Windows pool The UploadPython job used the default governed Windows pool, whose build container has an empty Python tools cache and no outbound network, so UsePythonVersion@0 failed trying to download Python 3.12 (WinError 10013). Use the same custom pool/image as the Windows build jobs (RUST-1ES-POOL-WUS3 / RUST-Win22-Sql25-1P), which ships Python 3.12 and can pip install twine. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Drop unsupported --skip-existing from twine upload Azure Artifacts' PyPI endpoint rejects twine's --skip-existing flag (UnsupportedConfiguration). Each run stamps a unique .dev wheel version, so there is no existing version to skip. Remove the flag. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .pipeline/OneBranch/PublishFeeds-Sandbox.yml | 652 ++++++++++++++++++ mssql-mock-tds-py/Cargo.toml | 7 + .../build-mock-python-wheel-in-container.sh | 66 ++ 3 files changed, 725 insertions(+) create mode 100644 .pipeline/OneBranch/PublishFeeds-Sandbox.yml create mode 100644 scripts/build-mock-python-wheel-in-container.sh diff --git a/.pipeline/OneBranch/PublishFeeds-Sandbox.yml b/.pipeline/OneBranch/PublishFeeds-Sandbox.yml new file mode 100644 index 00000000..b59fee68 --- /dev/null +++ b/.pipeline/OneBranch/PublishFeeds-Sandbox.yml @@ -0,0 +1,652 @@ +################################################################################# +# # +# ███ SANDBOX / TEST-ONLY PIPELINE — NOT FOR PRODUCTION ███ # +# # +# This pipeline publishes the **mock TDS** artifacts (a TEST helper used to # +# stand up a fake SQL Server for client tests) to the mssql-rs_Public Azure # +# Artifacts feed. It exists for fast, manual experimentation only. # +# # +# NOTHING produced by this pipeline is a production artifact: # +# * Python wheels are stamped with PEP 440 *.devN prerelease versions. # +# * Crates are stamped with -dev.. prerelease versions. # +# * Publishing is OPT-IN. With the default parameters this pipeline only # +# BUILDS and runs a DRY RUN — it does NOT upload anything. # +# # +# Do not point real consumers at versions produced here. Treat the feed # +# entries it creates as disposable test data. # +# # +# Documentation: https://aka.ms/obpipelines # +# Yaml Schema: https://aka.ms/obpipelines/yaml/schema # +################################################################################# + +name: 'Publish Feeds (Sandbox)' + +# Manual only. No CI trigger, no PR validation — this is a hand-queued sandbox. +trigger: none +pr: none + +parameters: +# Publishing is opt-in for safety. When false (the default) the corresponding +# stage still BUILDS everything but logs a clear DRY RUN message instead of +# uploading to the feed. +- name: publishPython + displayName: 'Upload mock Python wheels to feed (false = build + dry run only)' + type: boolean + default: false + +- name: publishCrate + displayName: 'Publish mssql-mock-tds crate to feed (false = cargo publish --dry-run)' + type: boolean + default: false + +variables: + CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # For onebranch.pipeline.version task + LinuxContainerImage: 'mcr.microsoft.com/onebranch/azurelinux/build:3.0' + WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' + DEBIAN_FRONTEND: noninteractive + toolchainFeed: 'https://sqlclientdrivers.pkgs.visualstudio.com/mssql-rs/_packaging/mssql-rs/nuget/v3/index.json' + # Twine repository name (matches the .pypirc entry created by TwineAuthenticate@1). + pythonFeedName: 'mssql-rs_Public' + # Project-scoped Azure Artifacts feed: /. + pythonArtifactFeed: 'public/mssql-rs_Public' + # Cargo registry name defined in .cargo/config.ci.toml. + cargoRegistry: 'mssql-rs_Public' + +resources: + repositories: + - repository: templates + type: git + name: OneBranch.Pipelines/GovernedTemplates + ref: refs/heads/main + +extends: + template: v2/OneBranch.NonOfficial.CrossPlat.yml@templates + parameters: + featureFlags: + EnableCDPxPAT: false + WindowsHostVersion: 1ESWindows2022 + globalSdl: + binskim: + scanOutputDirectoryOnly: true + clippy: + enabled: true + stages: + ########################################################################### + # Stage 1: PublishMockPython + # Build the abi3 wheel(s) for mssql-mock-tds-py (Linux x64 + Windows x64) + # and (opt-in) twine upload to the feed's PyPI index. + # + # abi3-py39 => ONE wheel per (OS, arch) covering Python >= 3.9. No loop over + # Python versions (unlike mssql-py-core). + # + # SANDBOX matrix: Linux x64/ARM64 (manylinux_2_34), Windows x64/ARM64, and + # macOS universal2. musllinux can be added later by copying a Linux job and + # swapping the container image. + ########################################################################### + - stage: PublishMockPython + displayName: 'Publish Mock Python (sandbox)' + jobs: + ##################################################################### + # Linux x64 — manylinux_2_34 container build + ##################################################################### + - job: BuildLinux_x64 + displayName: 'Build mock wheel (Linux x64)' + pool: + type: linux + isCustom: true + name: RUST-1ES-POOL-WUS3 + demands: + - imageOverride -equals RUST-1ES-UBUSLIM + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + ob_artifactSuffix: '_mock_linux_x64' + CARGO_TARGET_DIR: $(Build.SourcesDirectory) + steps: + - template: /.pipeline/templates/cargo-authenticate-template.yml@self + parameters: + osType: Linux + - script: .pipeline/scripts/install-rustup.sh + displayName: 'Install Rustup' + - task: DockerInstaller@0 + inputs: + dockerVersion: '29.0.0' + displayName: 'Install Docker' + - template: /.pipeline/templates/install-ubuntu-dependency.yaml@self + parameters: + installDocker: true + # Stamp a PEP 440 prerelease version into BOTH the pyproject.toml and the + # Cargo.toml so maturin bakes the right version into the wheel. The mounted + # workspace is what the container builds, so we patch on the host first. + - script: | + set -euo pipefail + BASE=$(grep -m1 -E '^version\s*=' mssql-mock-tds-py/pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') + DEV="$(date -u +%Y%m%d)${BUILD_BUILDID}" + VER="${BASE}-dev${DEV}" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + echo "Sandbox wheel version: ${VER}" + # pyproject.toml [project].version + sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/pyproject.toml + # Cargo.toml [package].version (first version line) + sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/Cargo.toml + echo "##vso[task.setvariable variable=mockWheelVersion]${VER}" + displayName: 'Stamp sandbox wheel version' + env: + BUILD_BUILDID: $(Build.BuildId) + - script: | + set -euo pipefail + CONTAINER_IMAGE="ghcr.io/microsoft/mssql-rs/python-build/manylinux_2_34_x86_64_rust:latest" + echo "Building mock wheel in container: $CONTAINER_IMAGE" + mkdir -p "$(ob_outputDirectory)/wheels" + bash .pipeline/scripts/docker-cargo-run.sh --rm \ + -v "$(Build.SourcesDirectory):/workspace" \ + -v "$(ob_outputDirectory)/wheels:/workspace/target/wheels" \ + -e "WORKSPACE_DIR=/workspace" \ + -e "OUTPUT_DIR=/workspace/target/wheels" \ + "$CONTAINER_IMAGE" \ + bash /workspace/scripts/build-mock-python-wheel-in-container.sh + sudo chown -R $(whoami):$(whoami) "$(Build.SourcesDirectory)/target" || true + WHEEL_COUNT=$(find "$(ob_outputDirectory)/wheels" -type f -name '*.whl' | wc -l) + if [ "$WHEEL_COUNT" -eq 0 ]; then + echo "ERROR: No mock wheels were produced!" + exit 1 + fi + echo "$WHEEL_COUNT wheel(s) built." + ls -lh "$(ob_outputDirectory)/wheels" + displayName: 'Build mock abi3 wheel (manylinux_2_34)' + env: + CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN) + CARGO_REGISTRIES_MSSQL_RS_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_TOKEN) + - task: PublishPipelineArtifact@1 + displayName: 'Publish Linux x64 mock wheel' + inputs: + targetPath: '$(ob_outputDirectory)/wheels' + artifact: 'drop_PublishMockPython_BuildLinux_x64_mock_linux_x64' + publishLocation: 'pipeline' + + ##################################################################### + # Linux ARM64 — manylinux_2_34 aarch64 container build + ##################################################################### + - job: BuildLinux_arm64 + displayName: 'Build mock wheel (Linux ARM64)' + pool: + type: linux + isCustom: true + name: RUST-1ES-POOL-ARM-WUS3 + demands: + - imageOverride -equals RUST-UBUNTU-ARM64 + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + ob_artifactSuffix: '_mock_linux_arm64' + CARGO_TARGET_DIR: $(Build.SourcesDirectory) + steps: + - template: /.pipeline/templates/cargo-authenticate-template.yml@self + parameters: + osType: Linux + - script: .pipeline/scripts/install-rustup.sh + displayName: 'Install Rustup' + - task: DockerInstaller@0 + inputs: + dockerVersion: '29.0.0' + displayName: 'Install Docker' + - template: /.pipeline/templates/install-ubuntu-dependency.yaml@self + parameters: + installDocker: true + # Stamp a PEP 440 prerelease version into BOTH the pyproject.toml and the + # Cargo.toml so maturin bakes the right version into the wheel. The mounted + # workspace is what the container builds, so we patch on the host first. + - script: | + set -euo pipefail + BASE=$(grep -m1 -E '^version\s*=' mssql-mock-tds-py/pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') + DEV="$(date -u +%Y%m%d)${BUILD_BUILDID}" + VER="${BASE}-dev${DEV}" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + echo "Sandbox wheel version: ${VER}" + sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/pyproject.toml + sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/Cargo.toml + echo "##vso[task.setvariable variable=mockWheelVersion]${VER}" + displayName: 'Stamp sandbox wheel version' + env: + BUILD_BUILDID: $(Build.BuildId) + - script: | + set -euo pipefail + CONTAINER_IMAGE="ghcr.io/microsoft/mssql-rs/python-build/manylinux_2_34_aarch64_rust:latest" + echo "Building mock wheel in container: $CONTAINER_IMAGE" + mkdir -p "$(ob_outputDirectory)/wheels" + bash .pipeline/scripts/docker-cargo-run.sh --rm \ + -v "$(Build.SourcesDirectory):/workspace" \ + -v "$(ob_outputDirectory)/wheels:/workspace/target/wheels" \ + -e "WORKSPACE_DIR=/workspace" \ + -e "OUTPUT_DIR=/workspace/target/wheels" \ + "$CONTAINER_IMAGE" \ + bash /workspace/scripts/build-mock-python-wheel-in-container.sh + sudo chown -R $(whoami):$(whoami) "$(Build.SourcesDirectory)/target" || true + WHEEL_COUNT=$(find "$(ob_outputDirectory)/wheels" -type f -name '*.whl' | wc -l) + if [ "$WHEEL_COUNT" -eq 0 ]; then + echo "ERROR: No mock wheels were produced!" + exit 1 + fi + echo "$WHEEL_COUNT wheel(s) built." + ls -lh "$(ob_outputDirectory)/wheels" + displayName: 'Build mock abi3 wheel (manylinux_2_34 aarch64)' + env: + CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN) + CARGO_REGISTRIES_MSSQL_RS_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_TOKEN) + - task: PublishPipelineArtifact@1 + displayName: 'Publish Linux ARM64 mock wheel' + inputs: + targetPath: '$(ob_outputDirectory)/wheels' + artifact: 'drop_PublishMockPython_BuildLinux_arm64_mock_linux_arm64' + publishLocation: 'pipeline' + + ##################################################################### + # Windows x64 — native maturin build + ##################################################################### + - job: BuildWindows_x64 + displayName: 'Build mock wheel (Windows x64)' + pool: + type: windows + isCustom: true + name: RUST-1ES-POOL-WUS3 + demands: + - imageOverride -equals RUST-Win22-Sql25-1P + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + ob_artifactSuffix: '_mock_windows_x64' + CARGO_TARGET_DIR: C:\cargo_target_dir + steps: + - template: /.pipeline/templates/cargo-authenticate-template.yml@self + parameters: + osType: Windows + - template: /.pipeline/templates/install-dependencies.yml@self + parameters: + osType: Windows + skipPythonSetup: true + skipRustupInstall: false + enableJsDeps: false + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: '3.12' + architecture: 'x64' + addToPath: true + - pwsh: | + $ErrorActionPreference = 'Stop' + $py = Get-Content 'mssql-mock-tds-py/pyproject.toml' -Raw + if ($py -match '(?m)^version\s*=\s*"([^"]+)"') { $base = $Matches[1] } + else { Write-Error 'Could not read version from pyproject.toml'; exit 1 } + $dev = "$(Get-Date -Format 'yyyyMMdd')$(Build.BuildId)" + $ver = "$base-dev$dev" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + Write-Host "Sandbox wheel version: $ver" + (Get-Content 'mssql-mock-tds-py/pyproject.toml' -Raw) ` + -replace '(?m)^(version\s*=\s*)"[^"]+"', "`$1`"$ver`"" | + Set-Content 'mssql-mock-tds-py/pyproject.toml' -NoNewline + (Get-Content 'mssql-mock-tds-py/Cargo.toml' -Raw) ` + -replace '(?m)^(version\s*=\s*)"[^"]+"', "`$1`"$ver`"" | + Set-Content 'mssql-mock-tds-py/Cargo.toml' -NoNewline + Write-Host "##vso[task.setvariable variable=mockWheelVersion]$ver" + displayName: 'Stamp sandbox wheel version' + - pwsh: | + $ErrorActionPreference = 'Stop' + python --version + pip install maturin + New-Item -ItemType Directory -Force -Path "$(ob_outputDirectory)\wheels" | Out-Null + # abi3 build: one wheel, no per-version loop. --manifest-path because the + # Cargo package is still mssql-mock-tds-py while the dist is mssql-mock-tds. + maturin build --release --auditwheel skip ` + --interpreter python ` + --manifest-path mssql-mock-tds-py\Cargo.toml ` + --out "$(ob_outputDirectory)\wheels" + Get-ChildItem "$(ob_outputDirectory)\wheels" | Format-Table Name, Length + displayName: 'Build mock abi3 wheel (Windows x64)' + env: + CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN) + CARGO_REGISTRIES_MSSQL_RS_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_TOKEN) + - task: PublishPipelineArtifact@1 + displayName: 'Publish Windows x64 mock wheel' + inputs: + targetPath: '$(ob_outputDirectory)/wheels' + artifact: 'drop_PublishMockPython_BuildWindows_x64_mock_windows_x64' + publishLocation: 'pipeline' + + ##################################################################### + # Windows ARM64 — native maturin build + ##################################################################### + - job: BuildWindows_arm64 + displayName: 'Build mock wheel (Windows ARM64)' + pool: + type: windows + isCustom: true + name: RUST-1ES-POOL-ARM-WUS3 + demands: + - imageOverride -equals RUST-WINSRV-ARM + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + ob_artifactSuffix: '_mock_windows_arm64' + CARGO_TARGET_DIR: C:\cargo_target_dir + steps: + # Node.js workaround for ARM64 ADO tasks (UsePythonVersion etc. need Node). + - pwsh: ./scripts/setup-nodejs-path.ps1 + displayName: 'Add Node.js to PATH for ADO tasks' + - pwsh: ./scripts/install-nodejs-to-agent.ps1 + displayName: 'Install Node.js to agent externals folder' + - template: /.pipeline/templates/cargo-authenticate-template.yml@self + parameters: + osType: Windows + - template: /.pipeline/templates/install-dependencies.yml@self + parameters: + osType: Windows + skipPythonSetup: true + skipRustupInstall: false + enableJsDeps: false + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: '3.12' + architecture: 'arm64' + addToPath: true + - pwsh: | + $ErrorActionPreference = 'Stop' + $py = Get-Content 'mssql-mock-tds-py/pyproject.toml' -Raw + if ($py -match '(?m)^version\s*=\s*"([^"]+)"') { $base = $Matches[1] } + else { Write-Error 'Could not read version from pyproject.toml'; exit 1 } + $dev = "$(Get-Date -Format 'yyyyMMdd')$(Build.BuildId)" + $ver = "$base-dev$dev" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + Write-Host "Sandbox wheel version: $ver" + (Get-Content 'mssql-mock-tds-py/pyproject.toml' -Raw) ` + -replace '(?m)^(version\s*=\s*)"[^"]+"', "`$1`"$ver`"" | + Set-Content 'mssql-mock-tds-py/pyproject.toml' -NoNewline + (Get-Content 'mssql-mock-tds-py/Cargo.toml' -Raw) ` + -replace '(?m)^(version\s*=\s*)"[^"]+"', "`$1`"$ver`"" | + Set-Content 'mssql-mock-tds-py/Cargo.toml' -NoNewline + Write-Host "##vso[task.setvariable variable=mockWheelVersion]$ver" + displayName: 'Stamp sandbox wheel version' + - pwsh: | + $ErrorActionPreference = 'Stop' + python --version + pip install maturin + New-Item -ItemType Directory -Force -Path "$(ob_outputDirectory)\wheels" | Out-Null + maturin build --release --auditwheel skip ` + --interpreter python ` + --manifest-path mssql-mock-tds-py\Cargo.toml ` + --out "$(ob_outputDirectory)\wheels" + Get-ChildItem "$(ob_outputDirectory)\wheels" | Format-Table Name, Length + displayName: 'Build mock abi3 wheel (Windows ARM64)' + env: + CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN) + CARGO_REGISTRIES_MSSQL_RS_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_TOKEN) + - task: PublishPipelineArtifact@1 + displayName: 'Publish Windows ARM64 mock wheel' + inputs: + targetPath: '$(ob_outputDirectory)/wheels' + artifact: 'drop_PublishMockPython_BuildWindows_arm64_mock_windows_arm64' + publishLocation: 'pipeline' + + ##################################################################### + # macOS universal2 (x86_64 + ARM64) — native maturin build + # Governed templates don't support a macOS pool type natively, so we use + # type: linux + isCustom with the Azure Pipelines macOS image (same trick + # as stages.yml's MacOS_universal2 job). + # + # OpenSSL: mssql-mock-tds pulls the openssl crate on cfg(not(windows)) to + # build the PKCS#12 identity. To keep the wheel portable we build with + # --features vendored-openssl so OpenSSL is statically linked instead of + # dynamically linking Homebrew's libssl. + ##################################################################### + - job: BuildMacOS_universal2 + displayName: 'Build mock wheel (macOS universal2)' + pool: + type: linux + isCustom: true + name: 'Azure Pipelines' + vmImage: macOS-14 + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + ob_artifactSuffix: '_mock_macos_universal2' + CARGO_TARGET_DIR: $(Build.SourcesDirectory) + steps: + - template: /.pipeline/templates/cargo-authenticate-template.yml@self + parameters: + osType: MacOS + - template: /.pipeline/templates/install-dependencies.yml@self + parameters: + osType: MacOS + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: '3.12' + addToPath: true + - script: | + set -euo pipefail + # Target the UsePythonVersion-selected interpreter explicitly. It is a + # managed, writable, on-PATH Python, so there is no PEP 668 + # externally-managed-environment problem (no --break-system-packages) and + # the maturin/wheel console scripts land on PATH. abi3 needs only one + # interpreter regardless of the wheel's >=3.9 compatibility. + python -m pip install --upgrade pip + python -m pip install maturin wheel + echo "Installing Rust targets for universal2 build..." + rustup target add x86_64-apple-darwin + rustup target add aarch64-apple-darwin + displayName: 'Install maturin and Rust targets' + - script: | + set -euo pipefail + BASE=$(grep -m1 -E '^version\s*=' mssql-mock-tds-py/pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') + DEV="$(date -u +%Y%m%d)${BUILD_BUILDID}" + VER="${BASE}-dev${DEV}" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + echo "Sandbox wheel version: ${VER}" + # BSD sed (macOS) rejects the GNU empty-regex reuse `s//.../`, so use an + # explicit pattern. Each manifest has exactly one line-starting + # `version =` (the package version), so a plain substitution is safe. + sed -i '' -E "s/^version[[:space:]]*=.*/version = \"${VER}\"/" mssql-mock-tds-py/pyproject.toml + sed -i '' -E "s/^version[[:space:]]*=.*/version = \"${VER}\"/" mssql-mock-tds-py/Cargo.toml + echo "##vso[task.setvariable variable=mockWheelVersion]${VER}" + displayName: 'Stamp sandbox wheel version' + env: + BUILD_BUILDID: $(Build.BuildId) + - script: | + set -euo pipefail + export MACOSX_DEPLOYMENT_TARGET=15.0 + mkdir -p "$(ob_outputDirectory)/wheels" + # ONE universal2 abi3 wheel. vendored-openssl statically links OpenSSL + # so the wheel doesn't depend on Homebrew's libssl. --manifest-path + # because the Cargo package is still mssql-mock-tds-py. + maturin build --release \ + --target universal2-apple-darwin \ + --auditwheel skip \ + --features vendored-openssl \ + --manifest-path mssql-mock-tds-py/Cargo.toml \ + --out "$(ob_outputDirectory)/wheels" + ls -lh "$(ob_outputDirectory)/wheels" + displayName: 'Build mock universal2 wheel (macOS)' + env: + CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN) + CARGO_REGISTRIES_MSSQL_RS_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_TOKEN) + - script: | + set -euo pipefail + # wheel was installed against the selected interpreter above; -m avoids + # any reliance on the console script's location on PATH. + for whl in "$(ob_outputDirectory)"/wheels/*.whl; do + echo "Re-tagging: $(basename "$whl")" + python -m wheel tags --platform-tag macosx_15_0_universal2 --remove "$whl" + done + echo "Re-tagged wheels:" + ls -lh "$(ob_outputDirectory)/wheels" + displayName: 'Re-tag macOS wheel to uniform universal2 platform tag' + - task: PublishPipelineArtifact@1 + displayName: 'Publish macOS universal2 mock wheel' + inputs: + targetPath: '$(ob_outputDirectory)/wheels' + artifact: 'drop_PublishMockPython_BuildMacOS_universal2_mock_macos_universal2' + publishLocation: 'pipeline' + + ##################################################################### + # Upload — collect wheels and (opt-in) twine upload to the feed. + ##################################################################### + - job: UploadPython + displayName: 'Upload mock wheels to feed' + dependsOn: + - BuildLinux_x64 + - BuildLinux_arm64 + - BuildWindows_x64 + - BuildWindows_arm64 + - BuildMacOS_universal2 + # Never upload from PR runs (defence in depth; this pipeline has pr: none). + condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + pool: + type: windows + isCustom: true + name: RUST-1ES-POOL-WUS3 + demands: + - imageOverride -equals RUST-Win22-Sql25-1P + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + steps: + - download: current + displayName: 'Download mock wheel artifacts' + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: '3.12' + architecture: 'x64' + addToPath: true + - pwsh: | + $ErrorActionPreference = 'Stop' + python -m pip install --upgrade twine + $dist = "$(Build.StagingDirectory)\dist" + New-Item -ItemType Directory -Force -Path $dist | Out-Null + Get-ChildItem "$(Pipeline.Workspace)" -Recurse -Filter *.whl | + Copy-Item -Destination $dist + Write-Host "=== Collected wheels ===" + Get-ChildItem $dist -Filter *.whl | ForEach-Object { + Write-Host " $($_.Name) ($([math]::Round($_.Length/1KB,1)) KB)" + } + Write-Host "##vso[task.setvariable variable=distDir]$dist" + displayName: 'Collect wheels' + - ${{ if eq(parameters.publishPython, true) }}: + - task: TwineAuthenticate@1 + displayName: 'Authenticate twine to feed' + inputs: + artifactFeed: '$(pythonArtifactFeed)' + - pwsh: | + $ErrorActionPreference = 'Stop' + Write-Host "Uploading SANDBOX wheels to $(pythonFeedName) ..." + # Azure Artifacts' PyPI endpoint rejects --skip-existing + # (UnsupportedConfiguration). Each run stamps a unique + # .dev version, so there is no collision to skip. + twine upload -r $(pythonFeedName) --config-file "$(PYPIRC_PATH)" ` + "$(distDir)\*.whl" + displayName: 'twine upload (sandbox)' + - ${{ if ne(parameters.publishPython, true) }}: + - pwsh: | + Write-Host "================ DRY RUN ================" + Write-Host "publishPython=false -> NOT uploading." + Write-Host "Re-run with publishPython=true to push these" + Write-Host "SANDBOX wheels to the $(pythonFeedName) feed." + Write-Host "========================================" + displayName: 'DRY RUN — skip twine upload' + + ########################################################################### + # Stage 2: PublishMockCrate + # cargo publish the mssql-mock-tds crate to the feed's Cargo index. + # + # mssql-mock-tds path-depends on mssql-tds. cargo publish rewrites that path + # dep to a registry version dependency and verifies by BUILDING the crate, so + # mssql-tds@ must already exist in the feed. We therefore publish in + # dependency order: mssql-tds FIRST, then mssql-mock-tds. + # + # Runs on Windows so the verify build uses SChannel and never needs OpenSSL + # (mssql-mock-tds only pulls openssl on cfg(not(windows))). + # + # publishCrate=false (default) => cargo publish --dry-run only. No feed writes. + ########################################################################### + - stage: PublishMockCrate + displayName: 'Publish Mock Crate (sandbox)' + jobs: + - job: PublishCrate + displayName: 'cargo publish mssql-mock-tds (sandbox)' + pool: + type: windows + isCustom: true + name: RUST-1ES-POOL-WUS3 + demands: + - imageOverride -equals RUST-Win22-Sql25-1P + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + CARGO_TARGET_DIR: C:\cargo_target_dir + steps: + - template: /.pipeline/templates/cargo-authenticate-template.yml@self + parameters: + osType: Windows + - template: /.pipeline/templates/install-dependencies.yml@self + parameters: + osType: Windows + skipPythonSetup: true + skipRustupInstall: false + enableJsDeps: false + # Stamp a PEP 440 / SemVer prerelease into ONLY the [package].version of + # mssql-tds and mssql-mock-tds. We do this inline instead of the shared + # .pipeline/scripts/stamp-crate-versions.ps1 because that script's global + # `^version = "..."` regex also rewrites table-style dependency versions + # such as [dependencies.uuid] version = "1.19.0", which corrupts the + # dependency requirement and makes `cargo publish` fail. Replacing only + # the first ^version line per file targets the package version. + - pwsh: | + $ErrorActionPreference = 'Stop' + $date = Get-Date -Format 'yyyyMMdd' + $rx = [regex]'(?m)^(version\s*=\s*)"[^"]+"' + $baseVer = ([regex]'(?m)^version\s*=\s*"([^"]+)"').Match((Get-Content 'mssql-tds/Cargo.toml' -Raw)).Groups[1].Value + if ([string]::IsNullOrWhiteSpace($baseVer)) { Write-Error 'Could not read base version from mssql-tds/Cargo.toml'; exit 1 } + $ver = "$baseVer-dev.$date.$(Build.BuildId)" + Write-Host "Sandbox crate version: $ver" + foreach ($f in 'mssql-tds/Cargo.toml','mssql-mock-tds/Cargo.toml') { + $c = Get-Content $f -Raw + $c = $rx.Replace($c, ('${1}"' + $ver + '"'), 1) + Set-Content $f $c -NoNewline + Write-Host "Stamped $f -> version = `"$ver`"" + } + Write-Host "##vso[task.setvariable variable=crateVersion]$ver" + displayName: 'Stamp crate versions (sandbox, package-only)' + # The mssql-tds path dependency in mssql-mock-tds/Cargo.toml has no version, + # which cargo publish rejects ("all dependencies must have a version"). Add + # the stamped version while keeping the path (cargo uses the version on + # publish and the path for local verify builds). + - pwsh: | + $ErrorActionPreference = 'Stop' + $ver = "$(crateVersion)" + if ([string]::IsNullOrWhiteSpace($ver)) { Write-Error 'crateVersion not set'; exit 1 } + $path = 'mssql-mock-tds/Cargo.toml' + $c = Get-Content $path -Raw + $c = $c -replace 'mssql-tds\s*=\s*\{\s*path\s*=\s*"\.\./mssql-tds"', + "mssql-tds = { path = `"../mssql-tds`", version = `"$ver`"" + Set-Content $path $c -NoNewline + Write-Host "Pinned mssql-tds dependency to version $ver" + Select-String -Path $path -Pattern 'mssql-tds\s*=' | ForEach-Object { Write-Host $_.Line } + displayName: 'Pin mssql-tds dependency version' + - ${{ if eq(parameters.publishCrate, true) }}: + - pwsh: | + $ErrorActionPreference = 'Stop' + Write-Host "Publishing SANDBOX crates to $(cargoRegistry) (mssql-tds first)..." + cargo publish -p mssql-tds --registry $(cargoRegistry) --allow-dirty + # mssql-tds must be queryable in the feed index before mssql-mock-tds + # is verified/published. Sparse index propagation is usually quick, but + # allow a short settle window for the sandbox. + Start-Sleep -Seconds 30 + cargo publish -p mssql-mock-tds --registry $(cargoRegistry) --allow-dirty + displayName: 'cargo publish (sandbox)' + env: + CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN) + CARGO_REGISTRIES_MSSQL_RS_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_TOKEN) + - ${{ if ne(parameters.publishCrate, true) }}: + - pwsh: | + $ErrorActionPreference = 'Stop' + Write-Host "================ DRY RUN ================" + Write-Host "publishCrate=false -> cargo publish --dry-run only." + Write-Host "========================================" + cargo publish -p mssql-tds --registry $(cargoRegistry) --dry-run --allow-dirty + # A dry run of mssql-mock-tds would need mssql-tds@$(crateVersion) to + # already exist in the feed (cargo verifies by building against the + # registry version), so it is intentionally NOT run here. Set + # publishCrate=true to publish mssql-tds then mssql-mock-tds in order. + Write-Host "Skipping mssql-mock-tds dry run (depends on a published mssql-tds)." + displayName: 'cargo publish --dry-run (sandbox)' + env: + CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN) + CARGO_REGISTRIES_MSSQL_RS_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_TOKEN) diff --git a/mssql-mock-tds-py/Cargo.toml b/mssql-mock-tds-py/Cargo.toml index f8e262fc..4cb7750f 100644 --- a/mssql-mock-tds-py/Cargo.toml +++ b/mssql-mock-tds-py/Cargo.toml @@ -9,6 +9,13 @@ publish = false name = "mssql_mock_tds_py" crate-type = ["cdylib"] +[features] +default = [] +# Statically link OpenSSL into the extension. Used for portable macOS wheels so +# the build doesn't dynamically link Homebrew's libssl. Passes through to the +# mssql-mock-tds crate's own vendored-openssl feature (openssl/vendored). +vendored-openssl = ["mssql-mock-tds/vendored-openssl"] + [dependencies] pyo3 = { version = "0.29.0", features = ["extension-module", "abi3-py39"] } mssql-mock-tds = { path = "../mssql-mock-tds" } diff --git a/scripts/build-mock-python-wheel-in-container.sh b/scripts/build-mock-python-wheel-in-container.sh new file mode 100644 index 00000000..9ec2f39b --- /dev/null +++ b/scripts/build-mock-python-wheel-in-container.sh @@ -0,0 +1,66 @@ +#!/bin/bash +set -e + +# SANDBOX / TEST-ONLY build script. +# Builds a single abi3 (Python >= 3.9) wheel for the mssql-mock-tds-py crate +# inside a manylinux container. Unlike scripts/build-python-wheels-in-container.sh +# (mssql-py-core), there is NO per-Python-version loop: pyo3's abi3-py39 feature +# produces one forward-compatible wheel per (OS, arch). +# +# NOTE: the produced distribution is named "mssql-mock-tds" (pyproject [project].name +# + maturin module-name = mssql_mock_tds), but the Cargo package / folder is still +# mssql-mock-tds-py, so we always pass --manifest-path. + +WORKSPACE_DIR="${WORKSPACE_DIR:-/workspace}" +OUTPUT_DIR="${OUTPUT_DIR:-$WORKSPACE_DIR/target/wheels}" + +echo "==> Building mock TDS abi3 wheel in container" +echo "Workspace: $WORKSPACE_DIR" +echo "Output directory: $OUTPUT_DIR" + +mkdir -p "$OUTPUT_DIR" + +# Find a Python binary to drive maturin (abi3 only needs one interpreter). +FIRST_PYTHON="" +for py_path in /opt/python/cp312-cp312/bin/python /opt/python/cp3*/bin/python /usr/local/bin/python3 /usr/bin/python3; do + if [ -x "$py_path" ]; then + FIRST_PYTHON="$py_path" + break + fi +done + +if [ -z "$FIRST_PYTHON" ]; then + echo "Error: No Python installation found in container!" + exit 1 +fi + +echo "Using Python: $FIRST_PYTHON" +$FIRST_PYTHON --version + +# Rust + maturin are pre-installed in the *_rust container images. +export PATH="$HOME/.cargo/bin:$PATH" +if ! command -v cargo &> /dev/null; then + echo "Error: Rust not found! Ensure you're using a *_rust container image." + exit 1 +fi +rustc --version +cargo --version + +if ! $FIRST_PYTHON -m pip show maturin &> /dev/null; then + echo "Error: maturin not found! Ensure you're using a *_rust container image." + exit 1 +fi + +# --auditwheel skip mirrors the mssql-py-core OpenSSL convention: do NOT vendor +# libssl/libcrypto into the wheel. The native extension links against the OS +# libssl.so.3 / libcrypto.so.3 at runtime (built on manylinux_2_34 / glibc 2.34). +# Passed on the CLI so we do not have to edit the (Stage 0) pyproject.toml. +$FIRST_PYTHON -m maturin build --release \ + --auditwheel skip \ + --interpreter "$FIRST_PYTHON" \ + --manifest-path "$WORKSPACE_DIR/mssql-mock-tds-py/Cargo.toml" \ + --out "$OUTPUT_DIR" + +echo "" +echo "==> Wheel(s) built:" +ls -lh "$OUTPUT_DIR" From c086649be414f0c5dc42b2882f54499fcfec42cf Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Mon, 29 Jun 2026 21:57:49 -0700 Subject: [PATCH 3/9] Add musllinux x64/arm64 mock wheel builds to sandbox pipeline Build self-contained musllinux_1_2 wheels (Alpine) for both architectures using the repo's musllinux_*_rust container images. musl wheels statically vendor OpenSSL (MATURIN_FEATURES=vendored-openssl) so they don't depend on Alpine's libssl at the consumer side; the container build script now honors an optional MATURIN_FEATURES env. Wire both jobs into UploadPython. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .pipeline/OneBranch/PublishFeeds-Sandbox.yml | 154 +++++++++++++++++- .../build-mock-python-wheel-in-container.sh | 19 ++- 2 files changed, 166 insertions(+), 7 deletions(-) diff --git a/.pipeline/OneBranch/PublishFeeds-Sandbox.yml b/.pipeline/OneBranch/PublishFeeds-Sandbox.yml index b59fee68..101d5c11 100644 --- a/.pipeline/OneBranch/PublishFeeds-Sandbox.yml +++ b/.pipeline/OneBranch/PublishFeeds-Sandbox.yml @@ -79,9 +79,8 @@ extends: # abi3-py39 => ONE wheel per (OS, arch) covering Python >= 3.9. No loop over # Python versions (unlike mssql-py-core). # - # SANDBOX matrix: Linux x64/ARM64 (manylinux_2_34), Windows x64/ARM64, and - # macOS universal2. musllinux can be added later by copying a Linux job and - # swapping the container image. + # SANDBOX matrix: Linux x64/ARM64 (manylinux_2_34), musllinux x64/ARM64 + # (Alpine, vendored OpenSSL), Windows x64/ARM64, and macOS universal2. ########################################################################### - stage: PublishMockPython displayName: 'Publish Mock Python (sandbox)' @@ -236,6 +235,153 @@ extends: artifact: 'drop_PublishMockPython_BuildLinux_arm64_mock_linux_arm64' publishLocation: 'pipeline' + ##################################################################### + # musllinux x64 — musllinux_1_2 (Alpine) container build. + # Built with vendored (static) OpenSSL so the wheel is self-contained + # and does not depend on Alpine's libssl at the consumer side. + ##################################################################### + - job: BuildLinuxMusl_x64 + displayName: 'Build mock wheel (musllinux x64)' + pool: + type: linux + isCustom: true + name: RUST-1ES-POOL-WUS3 + demands: + - imageOverride -equals RUST-1ES-UBUSLIM + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + ob_artifactSuffix: '_mock_linux_musl_x64' + CARGO_TARGET_DIR: $(Build.SourcesDirectory) + steps: + - template: /.pipeline/templates/cargo-authenticate-template.yml@self + parameters: + osType: Linux + - script: .pipeline/scripts/install-rustup.sh + displayName: 'Install Rustup' + - task: DockerInstaller@0 + inputs: + dockerVersion: '29.0.0' + displayName: 'Install Docker' + - template: /.pipeline/templates/install-ubuntu-dependency.yaml@self + parameters: + installDocker: true + - script: | + set -euo pipefail + BASE=$(grep -m1 -E '^version\s*=' mssql-mock-tds-py/pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') + DEV="$(date -u +%Y%m%d)${BUILD_BUILDID}" + VER="${BASE}-dev${DEV}" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + echo "Sandbox wheel version: ${VER}" + sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/pyproject.toml + sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/Cargo.toml + echo "##vso[task.setvariable variable=mockWheelVersion]${VER}" + displayName: 'Stamp sandbox wheel version' + env: + BUILD_BUILDID: $(Build.BuildId) + - script: | + set -euo pipefail + CONTAINER_IMAGE="ghcr.io/microsoft/mssql-rs/python-build/musllinux_1_2_x86_64_rust:latest" + echo "Building mock wheel in container: $CONTAINER_IMAGE" + mkdir -p "$(ob_outputDirectory)/wheels" + bash .pipeline/scripts/docker-cargo-run.sh --rm \ + -v "$(Build.SourcesDirectory):/workspace" \ + -v "$(ob_outputDirectory)/wheels:/workspace/target/wheels" \ + -e "WORKSPACE_DIR=/workspace" \ + -e "OUTPUT_DIR=/workspace/target/wheels" \ + -e "MATURIN_FEATURES=vendored-openssl" \ + "$CONTAINER_IMAGE" \ + bash /workspace/scripts/build-mock-python-wheel-in-container.sh + sudo chown -R $(whoami):$(whoami) "$(Build.SourcesDirectory)/target" || true + WHEEL_COUNT=$(find "$(ob_outputDirectory)/wheels" -type f -name '*.whl' | wc -l) + if [ "$WHEEL_COUNT" -eq 0 ]; then + echo "ERROR: No mock wheels were produced!" + exit 1 + fi + echo "$WHEEL_COUNT wheel(s) built." + ls -lh "$(ob_outputDirectory)/wheels" + displayName: 'Build mock abi3 wheel (musllinux_1_2 x86_64)' + env: + CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN) + CARGO_REGISTRIES_MSSQL_RS_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_TOKEN) + - task: PublishPipelineArtifact@1 + displayName: 'Publish musllinux x64 mock wheel' + inputs: + targetPath: '$(ob_outputDirectory)/wheels' + artifact: 'drop_PublishMockPython_BuildLinuxMusl_x64_mock_linux_musl_x64' + publishLocation: 'pipeline' + + ##################################################################### + # musllinux ARM64 — musllinux_1_2 aarch64 (Alpine) container build. + # Vendored (static) OpenSSL, same rationale as musllinux x64. + ##################################################################### + - job: BuildLinuxMusl_arm64 + displayName: 'Build mock wheel (musllinux ARM64)' + pool: + type: linux + isCustom: true + name: RUST-1ES-POOL-ARM-WUS3 + demands: + - imageOverride -equals RUST-UBUNTU-ARM64 + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + ob_artifactSuffix: '_mock_linux_musl_arm64' + CARGO_TARGET_DIR: $(Build.SourcesDirectory) + steps: + - template: /.pipeline/templates/cargo-authenticate-template.yml@self + parameters: + osType: Linux + - script: .pipeline/scripts/install-rustup.sh + displayName: 'Install Rustup' + - task: DockerInstaller@0 + inputs: + dockerVersion: '29.0.0' + displayName: 'Install Docker' + - template: /.pipeline/templates/install-ubuntu-dependency.yaml@self + parameters: + installDocker: true + - script: | + set -euo pipefail + BASE=$(grep -m1 -E '^version\s*=' mssql-mock-tds-py/pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') + DEV="$(date -u +%Y%m%d)${BUILD_BUILDID}" + VER="${BASE}-dev${DEV}" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + echo "Sandbox wheel version: ${VER}" + sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/pyproject.toml + sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/Cargo.toml + echo "##vso[task.setvariable variable=mockWheelVersion]${VER}" + displayName: 'Stamp sandbox wheel version' + env: + BUILD_BUILDID: $(Build.BuildId) + - script: | + set -euo pipefail + CONTAINER_IMAGE="ghcr.io/microsoft/mssql-rs/python-build/musllinux_1_2_aarch64_rust:latest" + echo "Building mock wheel in container: $CONTAINER_IMAGE" + mkdir -p "$(ob_outputDirectory)/wheels" + bash .pipeline/scripts/docker-cargo-run.sh --rm \ + -v "$(Build.SourcesDirectory):/workspace" \ + -v "$(ob_outputDirectory)/wheels:/workspace/target/wheels" \ + -e "WORKSPACE_DIR=/workspace" \ + -e "OUTPUT_DIR=/workspace/target/wheels" \ + -e "MATURIN_FEATURES=vendored-openssl" \ + "$CONTAINER_IMAGE" \ + bash /workspace/scripts/build-mock-python-wheel-in-container.sh + sudo chown -R $(whoami):$(whoami) "$(Build.SourcesDirectory)/target" || true + WHEEL_COUNT=$(find "$(ob_outputDirectory)/wheels" -type f -name '*.whl' | wc -l) + if [ "$WHEEL_COUNT" -eq 0 ]; then + echo "ERROR: No mock wheels were produced!" + exit 1 + fi + echo "$WHEEL_COUNT wheel(s) built." + ls -lh "$(ob_outputDirectory)/wheels" + displayName: 'Build mock abi3 wheel (musllinux_1_2 aarch64)' + env: + CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_PUBLIC_TOKEN) + CARGO_REGISTRIES_MSSQL_RS_TOKEN: $(CARGO_REGISTRIES_MSSQL_RS_TOKEN) + - task: PublishPipelineArtifact@1 + displayName: 'Publish musllinux ARM64 mock wheel' + inputs: + targetPath: '$(ob_outputDirectory)/wheels' + artifact: 'drop_PublishMockPython_BuildLinuxMusl_arm64_mock_linux_musl_arm64' + publishLocation: 'pipeline' + ##################################################################### # Windows x64 — native maturin build ##################################################################### @@ -485,6 +631,8 @@ extends: dependsOn: - BuildLinux_x64 - BuildLinux_arm64 + - BuildLinuxMusl_x64 + - BuildLinuxMusl_arm64 - BuildWindows_x64 - BuildWindows_arm64 - BuildMacOS_universal2 diff --git a/scripts/build-mock-python-wheel-in-container.sh b/scripts/build-mock-python-wheel-in-container.sh index 9ec2f39b..36143cd6 100644 --- a/scripts/build-mock-python-wheel-in-container.sh +++ b/scripts/build-mock-python-wheel-in-container.sh @@ -51,13 +51,24 @@ if ! $FIRST_PYTHON -m pip show maturin &> /dev/null; then exit 1 fi -# --auditwheel skip mirrors the mssql-py-core OpenSSL convention: do NOT vendor -# libssl/libcrypto into the wheel. The native extension links against the OS -# libssl.so.3 / libcrypto.so.3 at runtime (built on manylinux_2_34 / glibc 2.34). -# Passed on the CLI so we do not have to edit the (Stage 0) pyproject.toml. +# Optional cargo features (e.g. vendored-openssl for musllinux, where Alpine's +# dynamic libssl is not a portable runtime dependency). manylinux builds leave +# MATURIN_FEATURES unset and link the OS libssl.so.3 at runtime instead. +FEATURE_ARGS=() +if [ -n "${MATURIN_FEATURES:-}" ]; then + echo "Building with cargo features: $MATURIN_FEATURES" + FEATURE_ARGS=(--features "$MATURIN_FEATURES") +fi + +# --auditwheel skip mirrors the mssql-py-core OpenSSL convention: do NOT let +# maturin repair the wheel. On manylinux the native extension links against the +# OS libssl.so.3 / libcrypto.so.3 at runtime (manylinux_2_34 / glibc 2.34); on +# musllinux we instead statically vendor OpenSSL via MATURIN_FEATURES so the +# wheel is self-contained. Passed on the CLI so we do not edit pyproject.toml. $FIRST_PYTHON -m maturin build --release \ --auditwheel skip \ --interpreter "$FIRST_PYTHON" \ + "${FEATURE_ARGS[@]}" \ --manifest-path "$WORKSPACE_DIR/mssql-mock-tds-py/Cargo.toml" \ --out "$OUTPUT_DIR" From c4f19468d4c7dc600804b86b0438ebb0a8bd78cb Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:05:37 -0700 Subject: [PATCH 4/9] Add releaseVersion option to publish Python wheels without .dev suffix When the releaseVersion parameter is set, the seven Python wheel build jobs stamp the base version from pyproject.toml as-is (e.g. 1.0.0) instead of appending the .dev prerelease suffix. Default remains the .dev prerelease so existing behavior is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .pipeline/OneBranch/PublishFeeds-Sandbox.yml | 59 +++++++++++++++----- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/.pipeline/OneBranch/PublishFeeds-Sandbox.yml b/.pipeline/OneBranch/PublishFeeds-Sandbox.yml index 101d5c11..df7944f9 100644 --- a/.pipeline/OneBranch/PublishFeeds-Sandbox.yml +++ b/.pipeline/OneBranch/PublishFeeds-Sandbox.yml @@ -7,7 +7,8 @@ # Artifacts feed. It exists for fast, manual experimentation only. # # # # NOTHING produced by this pipeline is a production artifact: # -# * Python wheels are stamped with PEP 440 *.devN prerelease versions. # +# * Python wheels are stamped with PEP 440 *.devN prerelease versions # +# UNLESS the releaseVersion parameter is set (then BASE is published). # # * Crates are stamped with -dev.. prerelease versions. # # * Publishing is OPT-IN. With the default parameters this pipeline only # # BUILDS and runs a DRY RUN — it does NOT upload anything. # @@ -39,6 +40,11 @@ parameters: type: boolean default: false +- name: releaseVersion + displayName: 'Publish Python wheels as a RELEASE version (no .dev suffix, e.g. 1.0.0). Unchecked = .dev prerelease.' + type: boolean + default: false + variables: CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # For onebranch.pipeline.version task LinuxContainerImage: 'mcr.microsoft.com/onebranch/azurelinux/build:3.0' @@ -119,8 +125,10 @@ extends: - script: | set -euo pipefail BASE=$(grep -m1 -E '^version\s*=' mssql-mock-tds-py/pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') - DEV="$(date -u +%Y%m%d)${BUILD_BUILDID}" - VER="${BASE}-dev${DEV}" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + case "${RELEASE_VERSION}" in + True|true|TRUE) VER="${BASE}" ;; # release: publish BASE as-is (e.g. 1.0.0) + *) VER="${BASE}-dev$(date -u +%Y%m%d)${BUILD_BUILDID}" ;; # PEP 440 normalizes -dev -> .dev + esac echo "Sandbox wheel version: ${VER}" # pyproject.toml [project].version sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/pyproject.toml @@ -130,6 +138,7 @@ extends: displayName: 'Stamp sandbox wheel version' env: BUILD_BUILDID: $(Build.BuildId) + RELEASE_VERSION: ${{ parameters.releaseVersion }} - script: | set -euo pipefail CONTAINER_IMAGE="ghcr.io/microsoft/mssql-rs/python-build/manylinux_2_34_x86_64_rust:latest" @@ -195,8 +204,10 @@ extends: - script: | set -euo pipefail BASE=$(grep -m1 -E '^version\s*=' mssql-mock-tds-py/pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') - DEV="$(date -u +%Y%m%d)${BUILD_BUILDID}" - VER="${BASE}-dev${DEV}" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + case "${RELEASE_VERSION}" in + True|true|TRUE) VER="${BASE}" ;; # release: publish BASE as-is (e.g. 1.0.0) + *) VER="${BASE}-dev$(date -u +%Y%m%d)${BUILD_BUILDID}" ;; # PEP 440 normalizes -dev -> .dev + esac echo "Sandbox wheel version: ${VER}" sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/pyproject.toml sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/Cargo.toml @@ -204,6 +215,7 @@ extends: displayName: 'Stamp sandbox wheel version' env: BUILD_BUILDID: $(Build.BuildId) + RELEASE_VERSION: ${{ parameters.releaseVersion }} - script: | set -euo pipefail CONTAINER_IMAGE="ghcr.io/microsoft/mssql-rs/python-build/manylinux_2_34_aarch64_rust:latest" @@ -268,8 +280,10 @@ extends: - script: | set -euo pipefail BASE=$(grep -m1 -E '^version\s*=' mssql-mock-tds-py/pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') - DEV="$(date -u +%Y%m%d)${BUILD_BUILDID}" - VER="${BASE}-dev${DEV}" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + case "${RELEASE_VERSION}" in + True|true|TRUE) VER="${BASE}" ;; # release: publish BASE as-is (e.g. 1.0.0) + *) VER="${BASE}-dev$(date -u +%Y%m%d)${BUILD_BUILDID}" ;; # PEP 440 normalizes -dev -> .dev + esac echo "Sandbox wheel version: ${VER}" sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/pyproject.toml sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/Cargo.toml @@ -277,6 +291,7 @@ extends: displayName: 'Stamp sandbox wheel version' env: BUILD_BUILDID: $(Build.BuildId) + RELEASE_VERSION: ${{ parameters.releaseVersion }} - script: | set -euo pipefail CONTAINER_IMAGE="ghcr.io/microsoft/mssql-rs/python-build/musllinux_1_2_x86_64_rust:latest" @@ -341,8 +356,10 @@ extends: - script: | set -euo pipefail BASE=$(grep -m1 -E '^version\s*=' mssql-mock-tds-py/pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') - DEV="$(date -u +%Y%m%d)${BUILD_BUILDID}" - VER="${BASE}-dev${DEV}" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + case "${RELEASE_VERSION}" in + True|true|TRUE) VER="${BASE}" ;; # release: publish BASE as-is (e.g. 1.0.0) + *) VER="${BASE}-dev$(date -u +%Y%m%d)${BUILD_BUILDID}" ;; # PEP 440 normalizes -dev -> .dev + esac echo "Sandbox wheel version: ${VER}" sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/pyproject.toml sed -i -E "0,/^version\s*=.*/s//version = \"${VER}\"/" mssql-mock-tds-py/Cargo.toml @@ -350,6 +367,7 @@ extends: displayName: 'Stamp sandbox wheel version' env: BUILD_BUILDID: $(Build.BuildId) + RELEASE_VERSION: ${{ parameters.releaseVersion }} - script: | set -euo pipefail CONTAINER_IMAGE="ghcr.io/microsoft/mssql-rs/python-build/musllinux_1_2_aarch64_rust:latest" @@ -418,8 +436,12 @@ extends: $py = Get-Content 'mssql-mock-tds-py/pyproject.toml' -Raw if ($py -match '(?m)^version\s*=\s*"([^"]+)"') { $base = $Matches[1] } else { Write-Error 'Could not read version from pyproject.toml'; exit 1 } - $dev = "$(Get-Date -Format 'yyyyMMdd')$(Build.BuildId)" - $ver = "$base-dev$dev" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + if ('${{ parameters.releaseVersion }}' -eq 'True') { + $ver = $base # release: publish base version as-is (e.g. 1.0.0) + } else { + $dev = "$(Get-Date -Format 'yyyyMMdd')$(Build.BuildId)" + $ver = "$base-dev$dev" # PEP 440 normalizes -dev -> .dev for the wheel + } Write-Host "Sandbox wheel version: $ver" (Get-Content 'mssql-mock-tds-py/pyproject.toml' -Raw) ` -replace '(?m)^(version\s*=\s*)"[^"]+"', "`$1`"$ver`"" | @@ -493,8 +515,12 @@ extends: $py = Get-Content 'mssql-mock-tds-py/pyproject.toml' -Raw if ($py -match '(?m)^version\s*=\s*"([^"]+)"') { $base = $Matches[1] } else { Write-Error 'Could not read version from pyproject.toml'; exit 1 } - $dev = "$(Get-Date -Format 'yyyyMMdd')$(Build.BuildId)" - $ver = "$base-dev$dev" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + if ('${{ parameters.releaseVersion }}' -eq 'True') { + $ver = $base # release: publish base version as-is (e.g. 1.0.0) + } else { + $dev = "$(Get-Date -Format 'yyyyMMdd')$(Build.BuildId)" + $ver = "$base-dev$dev" # PEP 440 normalizes -dev -> .dev for the wheel + } Write-Host "Sandbox wheel version: $ver" (Get-Content 'mssql-mock-tds-py/pyproject.toml' -Raw) ` -replace '(?m)^(version\s*=\s*)"[^"]+"', "`$1`"$ver`"" | @@ -575,8 +601,10 @@ extends: - script: | set -euo pipefail BASE=$(grep -m1 -E '^version\s*=' mssql-mock-tds-py/pyproject.toml | sed -E 's/.*"([^"]+)".*/\1/') - DEV="$(date -u +%Y%m%d)${BUILD_BUILDID}" - VER="${BASE}-dev${DEV}" # SemVer prerelease; PEP 440 normalizes -dev -> .dev for the wheel + case "${RELEASE_VERSION}" in + True|true|TRUE) VER="${BASE}" ;; # release: publish BASE as-is (e.g. 1.0.0) + *) VER="${BASE}-dev$(date -u +%Y%m%d)${BUILD_BUILDID}" ;; # PEP 440 normalizes -dev -> .dev + esac echo "Sandbox wheel version: ${VER}" # BSD sed (macOS) rejects the GNU empty-regex reuse `s//.../`, so use an # explicit pattern. Each manifest has exactly one line-starting @@ -587,6 +615,7 @@ extends: displayName: 'Stamp sandbox wheel version' env: BUILD_BUILDID: $(Build.BuildId) + RELEASE_VERSION: ${{ parameters.releaseVersion }} - script: | set -euo pipefail export MACOSX_DEPLOYMENT_TARGET=15.0 From fd56a540db5e55582993d728fc50364bdbb81d8e Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:34:59 -0700 Subject: [PATCH 5/9] Fail fast when a release version is already published Add a PreflightVersionCheck job that gates all seven wheel builds. When publishing a release (releaseVersion + publishPython both true), it authenticates pip to the feed and queries the PEP 503 simple index for mssql-mock-tds; if the base version from pyproject.toml is already published, the pipeline fails in seconds instead of building seven wheels and only hitting the duplicate-version rejection at twine upload. The check is best-effort on lookup failure (warns, continues) since the upload-time rejection remains the authoritative guard. For dev/dry-run runs the job is a no-op. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .pipeline/OneBranch/PublishFeeds-Sandbox.yml | 46 ++++++ .../scripts/check-version-not-published.py | 153 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 .pipeline/scripts/check-version-not-published.py diff --git a/.pipeline/OneBranch/PublishFeeds-Sandbox.yml b/.pipeline/OneBranch/PublishFeeds-Sandbox.yml index df7944f9..4310fbfb 100644 --- a/.pipeline/OneBranch/PublishFeeds-Sandbox.yml +++ b/.pipeline/OneBranch/PublishFeeds-Sandbox.yml @@ -91,10 +91,50 @@ extends: - stage: PublishMockPython displayName: 'Publish Mock Python (sandbox)' jobs: + ##################################################################### + # Preflight — when publishing a RELEASE version (releaseVersion=true, + # publishPython=true) fail in seconds if that exact version is already + # on the feed, instead of building seven wheels first and only hitting + # the duplicate-version rejection at twine upload. All build jobs + # dependsOn this gate. For dev/dry-run combinations the job is a no-op. + ##################################################################### + - job: PreflightVersionCheck + displayName: 'Preflight release version' + pool: + type: windows + isCustom: true + name: RUST-1ES-POOL-WUS3 + demands: + - imageOverride -equals RUST-Win22-Sql25-1P + variables: + ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + steps: + - ${{ if and(eq(parameters.releaseVersion, true), eq(parameters.publishPython, true)) }}: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.12' + inputs: + versionSpec: '3.12' + architecture: 'x64' + addToPath: true + - task: PipAuthenticate@1 + displayName: 'Authenticate pip to feed (read)' + inputs: + artifactFeed: '$(pythonArtifactFeed)' + - pwsh: | + $ErrorActionPreference = 'Stop' + python .pipeline/scripts/check-version-not-published.py ` + mssql-mock-tds-py/pyproject.toml mssql-mock-tds + displayName: 'Fail if release version already on feed' + - ${{ if not(and(eq(parameters.releaseVersion, true), eq(parameters.publishPython, true))) }}: + - pwsh: | + Write-Host 'Preflight skipped: not a publishing release run (releaseVersion + publishPython both required).' + displayName: 'Preflight skipped' + ##################################################################### # Linux x64 — manylinux_2_34 container build ##################################################################### - job: BuildLinux_x64 + dependsOn: PreflightVersionCheck displayName: 'Build mock wheel (Linux x64)' pool: type: linux @@ -174,6 +214,7 @@ extends: # Linux ARM64 — manylinux_2_34 aarch64 container build ##################################################################### - job: BuildLinux_arm64 + dependsOn: PreflightVersionCheck displayName: 'Build mock wheel (Linux ARM64)' pool: type: linux @@ -253,6 +294,7 @@ extends: # and does not depend on Alpine's libssl at the consumer side. ##################################################################### - job: BuildLinuxMusl_x64 + dependsOn: PreflightVersionCheck displayName: 'Build mock wheel (musllinux x64)' pool: type: linux @@ -329,6 +371,7 @@ extends: # Vendored (static) OpenSSL, same rationale as musllinux x64. ##################################################################### - job: BuildLinuxMusl_arm64 + dependsOn: PreflightVersionCheck displayName: 'Build mock wheel (musllinux ARM64)' pool: type: linux @@ -404,6 +447,7 @@ extends: # Windows x64 — native maturin build ##################################################################### - job: BuildWindows_x64 + dependsOn: PreflightVersionCheck displayName: 'Build mock wheel (Windows x64)' pool: type: windows @@ -478,6 +522,7 @@ extends: # Windows ARM64 — native maturin build ##################################################################### - job: BuildWindows_arm64 + dependsOn: PreflightVersionCheck displayName: 'Build mock wheel (Windows ARM64)' pool: type: windows @@ -563,6 +608,7 @@ extends: # dynamically linking Homebrew's libssl. ##################################################################### - job: BuildMacOS_universal2 + dependsOn: PreflightVersionCheck displayName: 'Build mock wheel (macOS universal2)' pool: type: linux diff --git a/.pipeline/scripts/check-version-not-published.py b/.pipeline/scripts/check-version-not-published.py new file mode 100644 index 00000000..e7a3fa3b --- /dev/null +++ b/.pipeline/scripts/check-version-not-published.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Preflight guard for the sandbox release flow. + +Reads the base ``version`` from a pyproject.toml and fails (exit 1) when that +exact version is already published to the Azure Artifacts PyPI feed. This lets +the pipeline fail in seconds instead of building seven wheels and only hitting +the duplicate-version rejection at ``twine upload``. + +Authentication: PipAuthenticate@1 exports ``PIP_INDEX_URL`` with the feed's +credentials embedded (``https://user:token@.../pypi/simple/``). We reuse that +to query the feed's PEP 503 simple index for the package. + +Best-effort: if the feed cannot be reached or the index URL is missing, we WARN +and exit 0. The duplicate-version rejection at upload time remains the +authoritative guard, so we never block a release on a transient lookup failure. +""" + +import base64 +import re +import sys +import urllib.error +import urllib.request +from html.parser import HTMLParser +from urllib.parse import urlsplit, urlunsplit + + +def read_base_version(pyproject_path: str) -> str: + with open(pyproject_path, encoding="utf-8") as f: + text = f.read() + m = re.search(r'(?m)^version\s*=\s*"([^"]+)"', text) + if not m: + sys.exit(f"ERROR: could not read version from {pyproject_path}") + return m.group(1).strip() + + +def normalize_name(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower() + + +def normalize_version(version: str) -> str: + # Light PEP 440 normalization: lowercase, drop a leading 'v', collapse the + # SemVer '-dev' / '-rc' style separators maturin emits to PEP 440 form so a + # filename version compares equal to the manifest version. + v = version.strip().lower() + if v.startswith("v"): + v = v[1:] + v = v.replace("-dev", ".dev").replace("-rc", "rc").replace("-alpha", "a").replace("-beta", "b") + return v + + +class _AnchorParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.filenames: list[str] = [] + + def handle_data(self, data: str) -> None: + data = data.strip() + if data.endswith((".whl", ".tar.gz", ".zip")): + self.filenames.append(data) + + +def version_from_filename(filename: str) -> str | None: + if filename.endswith(".whl"): + parts = filename[:-4].split("-") + return parts[1] if len(parts) >= 2 else None + for ext in (".tar.gz", ".zip"): + if filename.endswith(ext): + stem = filename[: -len(ext)] + bits = stem.rsplit("-", 1) + return bits[1] if len(bits) == 2 else None + return None + + +def fetch_simple_page(index_url: str, package: str) -> str | None: + parts = urlsplit(index_url) + auth_header = None + netloc = parts.netloc + if "@" in netloc: + userinfo, host = netloc.rsplit("@", 1) + netloc = host + token = base64.b64encode(userinfo.encode("utf-8")).decode("ascii") + auth_header = f"Basic {token}" + + base = urlunsplit((parts.scheme, netloc, parts.path, "", "")) + if not base.endswith("/"): + base += "/" + url = f"{base}{normalize_name(package)}/" + + req = urllib.request.Request(url) + if auth_header: + req.add_header("Authorization", auth_header) + req.add_header("Accept", "text/html") + try: + with urllib.request.urlopen(req, timeout=60) as resp: + return resp.read().decode("utf-8", errors="replace") + except urllib.error.HTTPError as e: + if e.code == 404: + return "" # package not present at all -> no published versions + print(f"WARNING: feed lookup failed (HTTP {e.code}); skipping preflight.") + return None + except (urllib.error.URLError, TimeoutError) as e: + print(f"WARNING: feed unreachable ({e}); skipping preflight.") + return None + + +def main(argv: list[str]) -> int: + if len(argv) != 3: + sys.exit("usage: check-version-not-published.py ") + pyproject_path, package = argv[1], argv[2] + + base = read_base_version(pyproject_path) + target = normalize_version(base) + print(f"Release preflight: checking feed for {package}=={base} (normalized {target})") + + import os + + index_url = os.environ.get("PIP_INDEX_URL", "").strip() + if not index_url: + print("WARNING: PIP_INDEX_URL not set; skipping preflight (upload still guards).") + return 0 + + page = fetch_simple_page(index_url, package) + if page is None: + return 0 # best-effort: could not determine + if page == "": + print(f"OK: {package} has no published versions yet.") + return 0 + + parser = _AnchorParser() + parser.feed(page) + published = set() + for fn in parser.filenames: + v = version_from_filename(fn) + if v: + published.add(normalize_version(v)) + + if target in published: + print( + f"ERROR: {package}=={base} is already published to the feed.\n" + f" Bump 'version' in {pyproject_path} before running a release.\n" + f" Azure Artifacts rejects re-uploading an existing version." + ) + return 1 + + print(f"OK: {package}=={base} is not on the feed; safe to release.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) From 7e90eb15e3c58ce18d267b70a7e3c117a73940f6 Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:02:28 -0700 Subject: [PATCH 6/9] Make Python preflight anonymous and add crate release check The mssql-rs_Public feed allows anonymous reads, so drop PipAuthenticate from the Python preflight and pass the public simple-index URL directly. Extend releaseVersion to the crate stage: when set, mssql-tds and mssql-mock-tds publish their base version (e.g. 1.0.0) instead of the -dev.. prerelease. Add a PreflightVersionCheck-style guard as the first step of PublishCrate that queries the public Cargo sparse index and fails fast if either crate version is already published, before cargo-authenticate and the rust install. Best-effort on lookup failure; cargo's duplicate-version rejection remains the authoritative guard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .pipeline/OneBranch/PublishFeeds-Sandbox.yml | 27 ++++-- ...heck-version-not-published.cpython-312.pyc | Bin 0 -> 8382 bytes .../check-crate-version-not-published.ps1 | 86 ++++++++++++++++++ .../scripts/check-version-not-published.py | 17 ++-- 4 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 .pipeline/scripts/__pycache__/check-version-not-published.cpython-312.pyc create mode 100644 .pipeline/scripts/check-crate-version-not-published.ps1 diff --git a/.pipeline/OneBranch/PublishFeeds-Sandbox.yml b/.pipeline/OneBranch/PublishFeeds-Sandbox.yml index 4310fbfb..919d39a9 100644 --- a/.pipeline/OneBranch/PublishFeeds-Sandbox.yml +++ b/.pipeline/OneBranch/PublishFeeds-Sandbox.yml @@ -9,7 +9,8 @@ # NOTHING produced by this pipeline is a production artifact: # # * Python wheels are stamped with PEP 440 *.devN prerelease versions # # UNLESS the releaseVersion parameter is set (then BASE is published). # -# * Crates are stamped with -dev.. prerelease versions. # +# * Crates are stamped with -dev.. prerelease versions # +# UNLESS the releaseVersion parameter is set (then BASE is published). # # * Publishing is OPT-IN. With the default parameters this pipeline only # # BUILDS and runs a DRY RUN — it does NOT upload anything. # # # @@ -57,6 +58,9 @@ variables: pythonArtifactFeed: 'public/mssql-rs_Public' # Cargo registry name defined in .cargo/config.ci.toml. cargoRegistry: 'mssql-rs_Public' + # Public (anonymous-read) index URLs for the release preflight checks. + pythonSimpleIndex: 'https://pkgs.dev.azure.com/sqlclientdrivers/public/_packaging/mssql-rs_Public/pypi/simple/' + cargoSparseIndex: 'https://pkgs.dev.azure.com/sqlclientdrivers/public/_packaging/mssql-rs_Public/Cargo/index/' resources: repositories: @@ -116,14 +120,10 @@ extends: versionSpec: '3.12' architecture: 'x64' addToPath: true - - task: PipAuthenticate@1 - displayName: 'Authenticate pip to feed (read)' - inputs: - artifactFeed: '$(pythonArtifactFeed)' - pwsh: | $ErrorActionPreference = 'Stop' python .pipeline/scripts/check-version-not-published.py ` - mssql-mock-tds-py/pyproject.toml mssql-mock-tds + mssql-mock-tds-py/pyproject.toml mssql-mock-tds '$(pythonSimpleIndex)' displayName: 'Fail if release version already on feed' - ${{ if not(and(eq(parameters.releaseVersion, true), eq(parameters.publishPython, true))) }}: - pwsh: | @@ -795,6 +795,15 @@ extends: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' CARGO_TARGET_DIR: C:\cargo_target_dir steps: + - ${{ if and(eq(parameters.releaseVersion, true), eq(parameters.publishCrate, true)) }}: + - pwsh: | + $ErrorActionPreference = 'Stop' + $baseVer = ([regex]'(?m)^version\s*=\s*"([^"]+)"').Match((Get-Content 'mssql-tds/Cargo.toml' -Raw)).Groups[1].Value + if ([string]::IsNullOrWhiteSpace($baseVer)) { Write-Error 'Could not read base version from mssql-tds/Cargo.toml'; exit 1 } + & .pipeline/scripts/check-crate-version-not-published.ps1 ` + -IndexBaseUrl '$(cargoSparseIndex)' -Version $baseVer ` + -Crates mssql-tds,mssql-mock-tds + displayName: 'Preflight: release crate versions not already published' - template: /.pipeline/templates/cargo-authenticate-template.yml@self parameters: osType: Windows @@ -817,7 +826,11 @@ extends: $rx = [regex]'(?m)^(version\s*=\s*)"[^"]+"' $baseVer = ([regex]'(?m)^version\s*=\s*"([^"]+)"').Match((Get-Content 'mssql-tds/Cargo.toml' -Raw)).Groups[1].Value if ([string]::IsNullOrWhiteSpace($baseVer)) { Write-Error 'Could not read base version from mssql-tds/Cargo.toml'; exit 1 } - $ver = "$baseVer-dev.$date.$(Build.BuildId)" + if ('${{ parameters.releaseVersion }}' -eq 'True') { + $ver = $baseVer # release: publish base version as-is (e.g. 1.0.0) + } else { + $ver = "$baseVer-dev.$date.$(Build.BuildId)" + } Write-Host "Sandbox crate version: $ver" foreach ($f in 'mssql-tds/Cargo.toml','mssql-mock-tds/Cargo.toml') { $c = Get-Content $f -Raw diff --git a/.pipeline/scripts/__pycache__/check-version-not-published.cpython-312.pyc b/.pipeline/scripts/__pycache__/check-version-not-published.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8f2dd6d5eab62fd7f61fe4a2bffb150bd48459c2 GIT binary patch literal 8382 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 literal 0 HcmV?d00001 diff --git a/.pipeline/scripts/check-crate-version-not-published.ps1 b/.pipeline/scripts/check-crate-version-not-published.ps1 new file mode 100644 index 00000000..1353f83e --- /dev/null +++ b/.pipeline/scripts/check-crate-version-not-published.ps1 @@ -0,0 +1,86 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Preflight guard for the sandbox crate release flow. + +.DESCRIPTION + Fails (exit 1) when a crate version about to be published is already present + in the Azure Artifacts Cargo registry, so the pipeline fails in seconds + instead of building and only hitting cargo's duplicate-version rejection at + `cargo publish`. + + The mssql-rs_Public feed allows anonymous reads, so the PEP-equivalent Cargo + sparse index is queried without credentials. Each crate's index file is + newline-delimited JSON, one object per published version. + + Best-effort: if the registry cannot be reached (auth/network), the script + WARNs and exits 0. cargo's duplicate-version rejection at publish time remains + the authoritative guard, so a transient lookup failure never blocks a release. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)][string]$IndexBaseUrl, + [Parameter(Mandatory = $true)][string]$Version, + [Parameter(Mandatory = $true)][string[]]$Crates +) + +$ErrorActionPreference = 'Stop' + +function Get-CargoIndexPath([string]$name) { + $n = $name.ToLower() + switch ($n.Length) { + 1 { return "1/$n" } + 2 { return "2/$n" } + 3 { return "3/$($n[0])/$n" } + default { return "$($n.Substring(0,2))/$($n.Substring(2,2))/$n" } + } +} + +$base = $IndexBaseUrl.TrimEnd('/') +$conflict = $false + +foreach ($crate in $Crates) { + $url = "$base/$(Get-CargoIndexPath $crate)" + Write-Host "Release preflight: checking registry for $crate@$Version" + Write-Host " GET $url" + + $content = $null + try { + $resp = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 60 -ErrorAction Stop + $content = $resp.Content + } + catch { + $code = $null + if ($_.Exception.Response) { $code = [int]$_.Exception.Response.StatusCode } + if ($code -eq 404) { + Write-Host " OK: $crate has no published versions yet." + continue + } + Write-Host "##vso[task.logissue type=warning]Registry lookup for $crate failed (HTTP $code); skipping preflight for this crate." + continue + } + + $versions = @() + foreach ($line in ($content -split "`n")) { + $line = $line.Trim() + if (-not $line) { continue } + try { $versions += ($line | ConvertFrom-Json).vers } catch { } + } + + if ($versions -contains $Version) { + Write-Host "##vso[task.logissue type=error]$crate@$Version is already published to the registry." + $conflict = $true + } + else { + Write-Host " OK: $crate@$Version is not on the registry." + } +} + +if ($conflict) { + Write-Error "One or more crate versions are already published. Bump the [package].version in the crate Cargo.toml before running a release. Azure Artifacts rejects re-publishing an existing version." + exit 1 +} + +Write-Host "All crate versions are clear to publish." diff --git a/.pipeline/scripts/check-version-not-published.py b/.pipeline/scripts/check-version-not-published.py index e7a3fa3b..2725fdfa 100644 --- a/.pipeline/scripts/check-version-not-published.py +++ b/.pipeline/scripts/check-version-not-published.py @@ -9,9 +9,10 @@ the pipeline fail in seconds instead of building seven wheels and only hitting the duplicate-version rejection at ``twine upload``. -Authentication: PipAuthenticate@1 exports ``PIP_INDEX_URL`` with the feed's -credentials embedded (``https://user:token@.../pypi/simple/``). We reuse that -to query the feed's PEP 503 simple index for the package. +The ``mssql-rs_Public`` feed allows anonymous reads, so the simple index URL is +passed in directly (no credentials). For convenience an embedded-credential URL +(``https://user:token@.../pypi/simple/``) is still accepted, as is a fallback to +the ``PIP_INDEX_URL`` environment variable. Best-effort: if the feed cannot be reached or the index URL is missing, we WARN and exit 0. The duplicate-version rejection at upload time remains the @@ -107,8 +108,10 @@ def fetch_simple_page(index_url: str, package: str) -> str | None: def main(argv: list[str]) -> int: - if len(argv) != 3: - sys.exit("usage: check-version-not-published.py ") + if len(argv) not in (3, 4): + sys.exit( + "usage: check-version-not-published.py [simple-index-url]" + ) pyproject_path, package = argv[1], argv[2] base = read_base_version(pyproject_path) @@ -117,9 +120,9 @@ def main(argv: list[str]) -> int: import os - index_url = os.environ.get("PIP_INDEX_URL", "").strip() + index_url = (argv[3] if len(argv) == 4 else os.environ.get("PIP_INDEX_URL", "")).strip() if not index_url: - print("WARNING: PIP_INDEX_URL not set; skipping preflight (upload still guards).") + print("WARNING: no simple index URL provided; skipping preflight (upload still guards).") return 0 page = fetch_simple_page(index_url, package) From 4975e0f99f22eaa4f734a30640c1b030cb4aa3d1 Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Tue, 30 Jun 2026 12:02:39 -0700 Subject: [PATCH 7/9] Remove accidentally committed __pycache__ artifact Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../check-version-not-published.cpython-312.pyc | Bin 8382 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .pipeline/scripts/__pycache__/check-version-not-published.cpython-312.pyc diff --git a/.pipeline/scripts/__pycache__/check-version-not-published.cpython-312.pyc b/.pipeline/scripts/__pycache__/check-version-not-published.cpython-312.pyc deleted file mode 100644 index 8f2dd6d5eab62fd7f61fe4a2bffb150bd48459c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8382 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 From c100a2eb2eebf9367c0e3fd19a6841415e78590b Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:42:46 -0700 Subject: [PATCH 8/9] Record FedAuth token deterministically in mock server The mock server previously recorded connection tokens into the shared ConnectionStore only at connection teardown, and keyed the store by the client socket address. Both flaws made downstream fedauth regression tests flaky on Linux: tokens were not visible when connect() returned (no barrier, papered over with sleeps), and sequential connects reusing the same ephemeral port overwrote each other under the same key. Record the token eagerly during login, before the LoginAck is sent, by giving ConnectionProcessor a handle to the shared store. Since the client blocks on LoginAck, the token is guaranteed visible once connect() returns. Re-key ConnectionStore by a unique per-connection id from an AtomicU64 counter using a BTreeMap so iteration is ordered and the most recent connection is deterministic. Drop the time.sleep workarounds in the Python fedauth tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql-mock-tds/src/server.rs | 110 +++++++++++++++--- .../rs-only-tests/test_mock_server_fedauth.py | 31 +---- 2 files changed, 96 insertions(+), 45 deletions(-) diff --git a/mssql-mock-tds/src/server.rs b/mssql-mock-tds/src/server.rs index 6ba695c7..bfe15db5 100644 --- a/mssql-mock-tds/src/server.rs +++ b/mssql-mock-tds/src/server.rs @@ -13,9 +13,10 @@ use crate::protocol::{ use crate::query_response::QueryRegistry; use bytes::BytesMut; use native_tls::Identity; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::net::SocketAddr; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::Mutex; @@ -51,6 +52,8 @@ impl RedirectionConfig { /// Each connection gets its own processor instance. /// FedAuth and username/password authentication are always supported. pub struct ConnectionProcessor { + /// Unique per-connection id assigned at accept time + conn_id: u64, /// Client socket address addr: SocketAddr, /// Whether the client has authenticated @@ -69,12 +72,20 @@ pub struct ConnectionProcessor { buffer: BytesMut, /// Optional redirection configuration redirection: Option, + /// Shared store used to record connection state as soon as it is known + connection_store: Option>>, } impl ConnectionProcessor { /// Create a new connection processor - pub fn new(addr: SocketAddr, query_registry: Arc>) -> Self { + pub fn new( + conn_id: u64, + addr: SocketAddr, + query_registry: Arc>, + connection_store: Option>>, + ) -> Self { Self { + conn_id, addr, is_authenticated: false, received_token: None, @@ -84,16 +95,20 @@ impl ConnectionProcessor { query_registry, buffer: BytesMut::with_capacity(4096), redirection: None, + connection_store, } } /// Create a new connection processor with redirection configuration pub fn new_with_redirection( + conn_id: u64, addr: SocketAddr, query_registry: Arc>, + connection_store: Option>>, redirection: Option, ) -> Self { Self { + conn_id, addr, is_authenticated: false, received_token: None, @@ -103,9 +118,15 @@ impl ConnectionProcessor { query_registry, buffer: BytesMut::with_capacity(4096), redirection, + connection_store, } } + /// Get the unique connection id + pub fn conn_id(&self) -> u64 { + self.conn_id + } + /// Get the client address pub fn addr(&self) -> SocketAddr { self.addr @@ -148,6 +169,15 @@ impl ConnectionProcessor { &mut self.buffer } + /// Upsert this connection's current state into the shared store. + /// Called eagerly during login so tokens are visible to callers the + /// moment the client's blocking LoginAck read returns. + async fn record_to_store(&self) { + if let Some(store) = &self.connection_store { + store.lock().await.store(self); + } + } + /// Process a single packet from the buffer and return the response pub async fn process_packet(&mut self) -> Result, ProtocolError> { if self.buffer.len() < PACKET_HEADER_SIZE { @@ -269,6 +299,7 @@ impl ConnectionProcessor { resp_header.write(&mut packet); packet.extend_from_slice(&response); + self.record_to_store().await; Some(packet) } } @@ -305,6 +336,7 @@ impl ConnectionProcessor { resp_header.write(&mut packet); packet.extend_from_slice(&response); + self.record_to_store().await; Some(packet) } Err(e) => { @@ -418,8 +450,9 @@ impl ConnectionProcessor { /// This allows tests to access per-connection state after connections complete. #[derive(Debug, Default)] pub struct ConnectionStore { - /// Completed connection processors keyed by client socket address - connections: HashMap, + /// Connection info keyed by unique connection id, ordered so that + /// iteration and `.values().last()` yield the most recent connection. + connections: BTreeMap, } /// Captured information from a completed connection @@ -461,11 +494,13 @@ impl ConnectionInfo { impl ConnectionStore { pub fn new() -> Self { Self { - connections: HashMap::new(), + connections: BTreeMap::new(), } } - /// Store connection info when a connection completes + /// Upsert connection info keyed by the connection's unique id. + /// Called both eagerly during login and again at connection teardown; + /// both updates target the same entry. pub fn store(&mut self, processor: &ConnectionProcessor) { let info = ConnectionInfo { addr: processor.addr(), @@ -474,16 +509,16 @@ impl ConnectionStore { user_agent: processor.user_agent.clone(), received_server_name: processor.received_server_name().map(|s| s.to_string()), }; - self.connections.insert(processor.addr(), info); + self.connections.insert(processor.conn_id(), info); } - /// Get connection info by address - pub fn get(&self, addr: &SocketAddr) -> Option<&ConnectionInfo> { - self.connections.get(addr) + /// Get connection info by connection id + pub fn get(&self, conn_id: u64) -> Option<&ConnectionInfo> { + self.connections.get(&conn_id) } /// Get all connection infos - pub fn all(&self) -> &HashMap { + pub fn all(&self) -> &BTreeMap { &self.connections } @@ -513,6 +548,8 @@ pub struct MockTdsServer { connection_store: Arc>, /// Optional redirection configuration for testing client redirection behavior redirection: Option, + /// Monotonic counter assigning a unique id to each accepted connection + connection_counter: Arc, } impl MockTdsServer { @@ -626,6 +663,7 @@ impl MockTdsServer { strict_mode, connection_store: Arc::new(Mutex::new(ConnectionStore::new())), redirection, + connection_counter: Arc::new(AtomicU64::new(0)), }) } @@ -653,11 +691,13 @@ impl MockTdsServer { let strict_mode = self.strict_mode; let connection_store = self.connection_store; let redirection = self.redirection.map(Arc::new); + let connection_counter = self.connection_counter; loop { let (socket, addr) = listener.accept().await?; info!("New connection from {}", addr); + let conn_id = connection_counter.fetch_add(1, Ordering::SeqCst); let registry_clone = Arc::clone(®istry); let tls_acceptor_clone = tls_acceptor.clone(); let store_clone = Arc::clone(&connection_store); @@ -668,6 +708,7 @@ impl MockTdsServer { if let Err(e) = handle_connection_with_tls( socket, addr, + conn_id, registry_clone, tls_acceptor_clone, strict_mode, @@ -693,6 +734,7 @@ impl MockTdsServer { let strict_mode = self.strict_mode; let connection_store = self.connection_store; let redirection = self.redirection.map(Arc::new); + let connection_counter = self.connection_counter; tokio::select! { result = async { @@ -703,13 +745,14 @@ impl MockTdsServer { info!("New connection from {}", addr); drop(listener); // Release lock before spawning + let conn_id = connection_counter.fetch_add(1, Ordering::SeqCst); let registry_clone = Arc::clone(®istry); let tls_acceptor_clone = tls_acceptor.clone(); let store_clone = Arc::clone(&connection_store); let redirection_clone = redirection.clone(); tokio::spawn(async move { - if let Err(e) = handle_connection_with_tls(socket, addr, registry_clone, tls_acceptor_clone, strict_mode, store_clone, redirection_clone).await { + if let Err(e) = handle_connection_with_tls(socket, addr, conn_id, registry_clone, tls_acceptor_clone, strict_mode, store_clone, redirection_clone).await { error!("Error handling connection from {}: {}", addr, e); } }); @@ -731,9 +774,11 @@ impl MockTdsServer { /// Handle a connection with optional TLS support. /// FedAuth and username/password authentication are always supported. +#[allow(clippy::too_many_arguments)] async fn handle_connection_with_tls( socket: TcpStream, addr: SocketAddr, + conn_id: u64, query_registry: Arc>, tls_acceptor: Option>, strict_mode: bool, @@ -764,6 +809,7 @@ async fn handle_connection_with_tls( handle_strict_encrypted_connection( tls_stream, addr, + conn_id, query_registry, connection_store, redirection, @@ -801,6 +847,7 @@ async fn handle_connection_with_tls( handle_encrypted_tds_wrapped_connection( tls_stream, addr, + conn_id, query_registry, connection_store, redirection, @@ -811,6 +858,7 @@ async fn handle_connection_with_tls( handle_unencrypted_connection( prelogin_socket, addr, + conn_id, query_registry, connection_store, redirection, @@ -876,6 +924,7 @@ async fn handle_prelogin_negotiation( async fn handle_strict_encrypted_connection( mut socket: TlsStream, addr: SocketAddr, + conn_id: u64, query_registry: Arc>, connection_store: Arc>, redirection: Option>, @@ -883,8 +932,13 @@ async fn handle_strict_encrypted_connection( let redir_config = redirection .as_ref() .map(|r| RedirectionConfig::new(r.redirect_host.clone(), r.redirect_port)); - let mut processor = - ConnectionProcessor::new_with_redirection(addr, query_registry, redir_config); + let mut processor = ConnectionProcessor::new_with_redirection( + conn_id, + addr, + query_registry, + Some(Arc::clone(&connection_store)), + redir_config, + ); let mut prelogin_handled = false; loop { @@ -952,10 +1006,16 @@ async fn handle_strict_encrypted_connection( async fn handle_encrypted_connection( mut socket: TlsStream, addr: SocketAddr, + conn_id: u64, query_registry: Arc>, connection_store: Arc>, ) -> Result<(), ProtocolError> { - let mut processor = ConnectionProcessor::new(addr, query_registry); + let mut processor = ConnectionProcessor::new( + conn_id, + addr, + query_registry, + Some(Arc::clone(&connection_store)), + ); loop { // Read data from TLS socket @@ -989,6 +1049,7 @@ async fn handle_encrypted_connection( async fn handle_encrypted_tds_wrapped_connection( mut socket: TlsStream, addr: SocketAddr, + conn_id: u64, query_registry: Arc>, connection_store: Arc>, redirection: Option>, @@ -996,8 +1057,13 @@ async fn handle_encrypted_tds_wrapped_connection( let redir_config = redirection .as_ref() .map(|r| RedirectionConfig::new(r.redirect_host.clone(), r.redirect_port)); - let mut processor = - ConnectionProcessor::new_with_redirection(addr, query_registry, redir_config); + let mut processor = ConnectionProcessor::new_with_redirection( + conn_id, + addr, + query_registry, + Some(Arc::clone(&connection_store)), + redir_config, + ); loop { // Read data from TLS socket (which wraps TdsTlsWrapper) @@ -1032,6 +1098,7 @@ async fn handle_encrypted_tds_wrapped_connection( async fn handle_unencrypted_connection( mut socket: TcpStream, addr: SocketAddr, + conn_id: u64, query_registry: Arc>, connection_store: Arc>, redirection: Option>, @@ -1039,8 +1106,13 @@ async fn handle_unencrypted_connection( let redir_config = redirection .as_ref() .map(|r| RedirectionConfig::new(r.redirect_host.clone(), r.redirect_port)); - let mut processor = - ConnectionProcessor::new_with_redirection(addr, query_registry, redir_config); + let mut processor = ConnectionProcessor::new_with_redirection( + conn_id, + addr, + query_registry, + Some(Arc::clone(&connection_store)), + redir_config, + ); loop { // Read data from plain socket diff --git a/mssql-py-core/tests/rs-only-tests/test_mock_server_fedauth.py b/mssql-py-core/tests/rs-only-tests/test_mock_server_fedauth.py index 9560bb2b..86bcca32 100644 --- a/mssql-py-core/tests/rs-only-tests/test_mock_server_fedauth.py +++ b/mssql-py-core/tests/rs-only-tests/test_mock_server_fedauth.py @@ -98,11 +98,7 @@ def test_connect_with_access_token(self, mock_server_port): # Clean up conn.close() assert not conn.is_connected() - - # Give the server a moment to process the connection info - import time - time.sleep(0.1) - + # Verify the server received the correct token assert server.connection_count() >= 1, "Server should have recorded at least one connection" assert server.has_received_token(mock_token), \ @@ -121,7 +117,6 @@ def test_connect_with_unique_access_token(self, mock_server_port): properly sent through the TDS protocol and received by the server. """ import mssql_py_core - import time # Generate a unique token for this test unique_token = f"unique_token_{secrets.token_hex(16)}" @@ -141,10 +136,7 @@ def test_connect_with_unique_access_token(self, mock_server_port): assert conn is not None assert conn.is_connected() conn.close() - - # Wait for connection info to be stored - time.sleep(0.1) - + # Verify the unique token was received received_token = server.get_last_access_token() assert received_token == unique_token, \ @@ -174,7 +166,6 @@ def test_execute_query_with_access_token(self, mock_server_port): Verifies both the query result and that the token was received. """ import mssql_py_core - import time mock_token = "mock_token_for_query_execution" @@ -209,10 +200,7 @@ def test_execute_query_with_access_token(self, mock_server_port): # Ensure references are dropped so TdsClient is fully released del cursor del conn - - # Wait for connection info to be stored - time.sleep(0.3) - + # Verify token was received assert server.has_received_token(mock_token), \ "Server should have received the access token used for query execution" @@ -220,7 +208,6 @@ def test_execute_query_with_access_token(self, mock_server_port): def test_get_all_connections(self, mock_server_port): """Test retrieving all connection info from the server.""" import mssql_py_core - import time token = "test_token_for_connection_list" @@ -237,9 +224,7 @@ def test_get_all_connections(self, mock_server_port): conn = mssql_py_core.PyCoreConnection(client_context) conn.close() - - time.sleep(0.1) - + # Get all connections connections = server.get_connections() assert len(connections) >= 1, "Should have at least one connection" @@ -252,7 +237,6 @@ def test_get_all_connections(self, mock_server_port): def test_clear_connections(self, mock_server_port): """Test clearing stored connection info.""" import mssql_py_core - import time server = mssql_mock_tds.PyMockTdsServer(port=mock_server_port, tls=True) @@ -267,9 +251,7 @@ def test_clear_connections(self, mock_server_port): conn = mssql_py_core.PyCoreConnection(client_context) conn.close() - - time.sleep(0.1) - + # Verify we have a connection assert server.connection_count() >= 1 @@ -282,7 +264,6 @@ def test_clear_connections(self, mock_server_port): def test_user_agent_format(self, mock_server_port): """Test that MS-PYTHON is correctly sent as the driver name in the user agent.""" import mssql_py_core - import time server = mssql_mock_tds.PyMockTdsServer(port=mock_server_port, tls=True) @@ -297,8 +278,6 @@ def test_user_agent_format(self, mock_server_port): conn = mssql_py_core.PyCoreConnection(client_context) conn.close() - time.sleep(0.1) - connections = server.get_connections() assert len(connections) >= 1, "Should have at least one connection" From 852fe560ae93337b158570eb71af6ff6967a52a3 Mon Sep 17 00:00:00 2001 From: Saurabh Singh <1623701+saurabh500@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:45:10 -0700 Subject: [PATCH 9/9] Add regression test for eager FedAuth token recording Assert the FedAuth token is visible in the shared ConnectionStore while the client connection is still open, guarding against a regression to teardown-only recording. Mirrors the downstream ODBC pooled-connection scenario where close() does not send EOF. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql-tds/tests/test_mock_server_fedauth.rs | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/mssql-tds/tests/test_mock_server_fedauth.rs b/mssql-tds/tests/test_mock_server_fedauth.rs index 108e05fa..7d439723 100644 --- a/mssql-tds/tests/test_mock_server_fedauth.rs +++ b/mssql-tds/tests/test_mock_server_fedauth.rs @@ -469,4 +469,63 @@ mod mock_server_fedauth_tests { Ok(()) } + + /// Regression guard for eager token recording: the token must be visible in the + /// shared store WHILE the client connection is still open, i.e. without relying on + /// connection teardown. This mirrors the downstream ODBC pooled-connection scenario + /// where close() returns the socket to a pool without sending EOF, so a teardown-only + /// record would never fire. + #[tokio::test] + async fn test_token_recorded_before_connection_close() -> Result<(), Box> + { + init_tracing(); + + let server = MockTdsServer::new("127.0.0.1:0").await?; + let server_addr = server.local_addr(); + let connection_store = server.connection_store(); + + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + let server_handle = + tokio::spawn(async move { server.run_with_shutdown(shutdown_rx).await }); + + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + let access_token = "eager_record_guard_token_67890".to_string(); + + let datasource = format!("tcp:{},{}", server_addr.ip(), server_addr.port()); + let mut context = ClientContext::default(); + context.access_token = Some(access_token.clone()); + context.tds_authentication_method = TdsAuthenticationMethod::AccessToken; + context.database = "master".to_string(); + context.encryption_options = EncryptionOptions { + mode: EncryptionSetting::PreferOff, + trust_server_certificate: true, + host_name_in_cert: None, + server_certificate: None, + }; + + let provider = TdsConnectionProvider {}; + let client = provider.create_client(context, &datasource, None).await?; + + // Assert the token is recorded WITHOUT closing/dropping the connection first. + let recorded = { + let store = connection_store.lock().await; + store + .all() + .values() + .any(|c| c.received_token_as_string().as_deref() == Some(&access_token)) + }; + assert!( + recorded, + "FedAuth token must be recorded before the client connection is closed (eager-record guard)" + ); + + // Keep the connection alive until after the assertion. + drop(client); + + let _ = shutdown_tx.send(()); + let _ = tokio::time::timeout(tokio::time::Duration::from_secs(2), server_handle).await; + + Ok(()) + } }