Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
db0faa6
chore(sdk): regenerate November 2026 IoT clients
HangyiWang Oct 1, 2026
f890f40
feat: prepare azure-iot 1.0.0 GA command surface
HangyiWang Oct 1, 2026
ff274eb
docs: keep GA preparation documents out of PR
HangyiWang Oct 1, 2026
85e6cff
docs: replace removed GA plan link with release history
HangyiWang Oct 1, 2026
08e62e2
Fix GA port parity mistakes and restore cleanup coverage
HangyiWang Oct 1, 2026
062d2cf
Integrate release test infrastructure and reliability fixes for GA
HangyiWang Oct 2, 2026
0291866
Align explicit canary test mocks with release fixtures
HangyiWang Oct 2, 2026
11526e9
Complete stable 2026-11-01 GA SDK and command compatibility
HangyiWang Oct 9, 2026
86969d5
fix: address GA namespace concurrency and compatibility review
HangyiWang Oct 12, 2026
6a2aa69
fix(adr): accept late population of omitted Hub link settings
HangyiWang Oct 16, 2026
55510d6
test: cover missing DPS and Hub command happy paths
HangyiWang Oct 17, 2026
2eaf2bf
test: preserve the standard module lifecycle timeout
HangyiWang Oct 17, 2026
5dd136a
test: keep owned DPS management out of legacy partial runs
HangyiWang Oct 17, 2026
a8a4aea
test: request explicit paired DPS policy permissions
HangyiWang Oct 17, 2026
bc8409a
fix: exclude unsupported TLS migration metadata from Hub writes
HangyiWang Oct 17, 2026
c57b45b
test: handle void failover results without weakening ownership
HangyiWang Oct 17, 2026
ead9afa
fix: read Hub policies after successful policy mutations
HangyiWang Oct 17, 2026
3721d01
test: align full qualification with GA registration and hostname cont…
HangyiWang Oct 17, 2026
08661af
test: grant storage access using known managed-identity object IDs
HangyiWang Oct 17, 2026
3a70c03
test: diagnose rejected polling URLs without exposing URL secrets
HangyiWang Oct 17, 2026
9eaa6ca
test: accept equals in acknowledged ARM polling paths
HangyiWang Oct 17, 2026
f75e0ae
fix(adr): recognize explicit DPS grant propagation failures
HangyiWang Oct 18, 2026
cd9ab50
test(adr): bind contextual Hub read authorization recovery
HangyiWang Oct 18, 2026
e195cbe
test(adr): track recovery completion within the readiness deadline
HangyiWang Oct 18, 2026
b190afc
test(hub): allow fresh storage role grants to propagate
HangyiWang Oct 18, 2026
52e300e
test(hub): isolate query-heavy identity cases from protocol coverage
HangyiWang Oct 18, 2026
11fabc0
test(hub): budget storage role propagation in lifecycle timeout
HangyiWang Oct 18, 2026
d6b4af9
fix: align GA command behavior with preview
HangyiWang Oct 19, 2026
95beddb
test: align DPS fixture with automatic linked-hub hostname
HangyiWang Oct 19, 2026
4a6992c
test: tolerate coalesced interval signals in ownership regression
HangyiWang Oct 19, 2026
fec6dbd
test(hub): reduce owned fixture provisioning and propagation waits
HangyiWang Oct 19, 2026
c8efc83
test(hub): isolate ownership fixtures from the runner resource group
HangyiWang Oct 19, 2026
3967cbe
chore: set ADR GA bug bash version to 1.0.0a1
HangyiWang Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 0 additions & 1 deletion .azure-devops/integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ stages:
testADR: ${{ parameters.testADR }}
maxParallelADT: 2
maxParallelDPS: 2
maxParallelHub: 2
maxParallelADU: 4
maxParallelADR: 2
- job: "evaluateCmdTable"
Expand Down
2 changes: 1 addition & 1 deletion .azure-devops/merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ jobs:
vmImage: ${{ parameters.linuxImage }}

steps:
- template: templates/install-azure-cli-released.yml
- template: templates/setup-python.yml
- template: templates/install-azure-cli-released.yml
- template: templates/download-install-local-azure-iot-cli-extension-with-pip.yml

- script: "pylint azext_iot/ --rcfile=.pylintrc"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ steps:
displayName: "Install Azure CLI test SDK"
workingDirectory: "../azure-cli/src/azure-cli-testsdk/"

- script: "python -m build"
- script: |
python -c "import shutil; shutil.rmtree('build', ignore_errors=True)"
python -m build
displayName: "Build wheel for Azure CLI test SDK"
workingDirectory: "../azure-cli/src/azure-cli-testsdk/"

Expand Down
2 changes: 1 addition & 1 deletion .azure-devops/templates/install-azure-cli-released.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
steps:
- script: pip install azure-cli
- script: python -m pip install azure-cli
displayName: "Install Azure CLI released"
129 changes: 110 additions & 19 deletions .azure-devops/templates/run-tests-parallel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ parameters:
- name: serviceConnection
type: string
default: ""
- name: hubSuite
type: string
# ADO allowed values cannot be empty; sentinel means no owned Hub suite.
default: "sentinel"
values: ["sentinel", "HubControl", "HubData"]
- name: hubSubscription
type: string
default: ""
- name: hubResourceGroup
type: string
default: cli-int-test-rg
- name: hubRegion
type: string
default: centraluseuap
- name: num_threads
type: number
default: 6
Expand All @@ -49,7 +63,33 @@ steps:
pythonVersion: ${{ parameters.pythonVersion }}
azureCLIVersion: ${{ parameters.azureCLIVersion }}

- template: set-testenv-sentinel.yml
# Owned Hub phases must never inherit the legacy pinned-resource pytest.ini.
- ${{ if eq(parameters.hubSuite, 'sentinel') }}:
- template: set-testenv-sentinel.yml

- ${{ if ne(parameters.hubSuite, 'sentinel') }}:
- task: PythonScript@0
displayName: "Validate owned Hub scope and platform before Azure login"
inputs:
scriptSource: inline
script: |
import os
import runpy
import sys
from pathlib import Path
if sys.platform != "linux":
raise SystemExit("Hub live phases are unsupported on Windows; this job cannot qualify Hub coverage.")
scope = (os.environ["azext_iot_hub_subscription"], os.environ["azext_iot_testrg"],
os.environ["azext_iot_testhub_location"])
if scope != ("a386d5ea-ea90-441a-8263-d816368c84a1", "cli-int-test-rg", "centraluseuap"):
raise SystemExit("Hub live phases require the authorized subscription, resource group and canary region.")
environment = runpy.run_path("azext_iot/tests/_hub_phase_runner.py")["environment"]
environment(dict(os.environ), "${{ parameters.hubSuite }}", "preflight",
Path("test-result/hub-phases"), "preflight", scope[0], scope[1])
env:
azext_iot_hub_subscription: ${{ parameters.hubSubscription }}
azext_iot_testrg: ${{ parameters.hubResourceGroup }}
azext_iot_testhub_location: ${{ parameters.hubRegion }}

# Fix for DNS resolution issues: https://github.com/actions/virtual-environments/issues/798
- script: |
Expand All @@ -66,7 +106,17 @@ steps:

- ${{ if eq(parameters.runIntTests, 'true') }}:
- task: AzureCLI@2
continueOnError: true
${{ if ne(parameters.hubSuite, 'sentinel') }}:
continueOnError: false
env:
azext_iot_hub_subscription: ${{ parameters.hubSubscription }}
azext_iot_testrg: ${{ parameters.hubResourceGroup }}
azext_iot_testhub_location: ${{ parameters.hubRegion }}
${{ else }}:
continueOnError: true
# The product defaults to public ARM; these legacy suites target centraluseuap resources.
env:
AZURE_IOT_ADR_ARM_ENDPOINT: https://centraluseuap.management.azure.com
displayName: "${{ parameters.name }} integration tests"
inputs:
azureSubscription: ${{ parameters.serviceConnection }}
Expand All @@ -81,32 +131,73 @@ steps:
inlineScript: |
export COVERAGE_FILE=.coverage.${{ parameters.name }}
pytest -vv ${{ parameters.path }} -k "_int.py" --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-int.xml
${{ if contains(parameters.path, 'azext_iot/tests/iothub') }}:
${{ if ne(parameters.hubSuite, 'sentinel') }}:
inlineScript: |
set -euo pipefail
export COVERAGE_FILE=.coverage.${{ parameters.name }}
pytest -vv ${{ parameters.path }} -k "_int.py" --dist=loadfile -n ${{ parameters.num_threads }} --reruns ${{ parameters.num_reruns }} --reruns-delay ${{ parameters.reruns_delay }} --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-int.xml --deselect=azext_iot/tests/iothub/core/test_iothub_discovery_int.py::TestIoTHubDiscovery::test_iothub_targets --durations=0
subscription=$(az account show --query id -o tsv)
if [ "$subscription" != "$azext_iot_hub_subscription" ]; then
echo "Hub service connection subscription does not match the authorized scope." >&2
exit 2
fi
python -m azext_iot.tests._hub_phase_runner --suite "${{ parameters.hubSuite }}" \
--subscription "$azext_iot_hub_subscription" --resource-group "$azext_iot_testrg" \
--region "$azext_iot_testhub_location" --output test-result/hub-phases
${{ if contains(parameters.path, 'azext_iot/tests/dps') }}:
inlineScript: |
set -e
export COVERAGE_FILE=.coverage.${{ parameters.name }}
pytest -vv ${{ parameters.path }} -k "_int.py" --ignore=azext_iot/tests/dps/core/test_dps_disable_local_auth_int.py --dist=loadfile -n ${{ parameters.num_threads }} --reruns ${{ parameters.num_reruns }} --reruns-delay ${{ parameters.reruns_delay }} --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-int.xml --durations=0
pytest -vv azext_iot/tests/dps/core/test_dps_disable_local_auth_int.py -n 0 --reruns ${{ parameters.num_reruns }} --reruns-delay ${{ parameters.reruns_delay }} --cov=azext_iot --cov-append --cov-config .coveragerc --junitxml=junit/test-iotext-dps-rbac-int.xml --durations=0
# This legacy service-connection/pinned-resource template also supports Windows
# and parallel matrix jobs; it does not supply the isolated controller's scope.
if [ -n "${azext_iot_dps_test_phase:-}${azext_iot_dps_phase_receipts:-}${azext_iot_dps_run_uid:-}${azext_iot_dps_test_subscription:-}${azext_iot_dps_test_resource_group:-}" ]; then
echo "Legacy Azure DevOps DPS coverage rejects isolated phase overrides; use the GitHub DPS controller with explicit scope." >&2
exit 2
fi
echo "DPS coverage: legacy partial regular only, not full qualification. Owned management, normal registration, CSR issuance, service-SAS and local-auth-toggle coverage require the GitHub DPS controller."
pytest -vv ${{ parameters.path }} -k "_int.py and not test_register_and_issue_certificate_contract and not test_register_without_csr_deadline_contract and not test_dps_management_happy_paths" --ignore=azext_iot/tests/dps/core/test_dps_disable_local_auth_int.py --dist=loadfile -n ${{ parameters.num_threads }} --reruns ${{ parameters.num_reruns }} --reruns-delay ${{ parameters.reruns_delay }} --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-int.xml --durations=0
${{ if contains(parameters.path, 'azext_iot/tests/adr') }}:
inlineScript: |
export COVERAGE_FILE=.coverage.${{ parameters.name }}
pytest -vv ${{ parameters.path }} -k "_int.py" --dist=loadfile -n ${{ parameters.num_threads }} --reruns ${{ parameters.num_reruns }} --reruns-delay ${{ parameters.reruns_delay }} --cov=azext_iot --cov-config .coveragerc --junitxml=junit/test-iotext-int.xml --durations=0
- ${{ if ne(parameters.hubSuite, 'sentinel') }}:
- task: PublishBuildArtifacts@1
condition: succeededOrFailed()
displayName: "Publish Hub phase evidence and redacted logs"
inputs:
pathToPublish: test-result/
publishLocation: "Container"
artifactName: "hub-phases-${{ parameters.hubSuite }}-py${{ parameters.pythonVersion }}-$(Agent.OS)"

- task: PublishBuildArtifacts@1
condition: succeededOrFailed()
inputs:
pathToPublish: .coverage.${{ parameters.name }}
publishLocation: "Container"
artifactName: "coverage"

- task: PublishBuildArtifacts@1
inputs:
pathToPublish: .coverage.${{ parameters.name }}
publishLocation: "Container"
artifactName: "coverage"
- task: PublishTestResults@2
condition: succeededOrFailed()
displayName: "Publish sanitized Hub phase results"
inputs:
testResultsFormat: "JUnit"
testResultsFiles: "test-result/hub-phases/**/junit.xml"
testRunTitle: "${{ parameters.hubSuite }} Python ${{ parameters.pythonVersion }}"
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
searchFolder: "$(System.DefaultWorkingDirectory)"

- task: PublishTestResults@2
condition: succeededOrFailed()
displayName: "Publish Test Results"
inputs:
testResultsFormat: "JUnit"
testResultsFiles: "**/test-*.xml"
testRunTitle: "Publish ${{ parameters.name }} test results for Python ${{ parameters.pythonVersion }} on OS $(Agent.OS)"
searchFolder: "$(System.DefaultWorkingDirectory)"
- ${{ if eq(parameters.hubSuite, 'sentinel') }}:
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: .coverage.${{ parameters.name }}
publishLocation: "Container"
artifactName: "coverage"

- task: PublishTestResults@2
condition: succeededOrFailed()
displayName: "Publish Test Results"
inputs:
testResultsFormat: "JUnit"
testResultsFiles: "**/test-*.xml"
testRunTitle: "Publish ${{ parameters.name }} test results for Python ${{ parameters.pythonVersion }} on OS $(Agent.OS)"
searchFolder: "$(System.DefaultWorkingDirectory)"
6 changes: 6 additions & 0 deletions .azure-devops/templates/setup-dev-test-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ steps:
parameters:
pythonVersion: ${{ parameters.pythonVersion }}

- bash: |
source scripts/select-openssl.sh
echo "##vso[task.prependpath]$openssl_bin"
displayName: Select OpenSSL 3 for CSR signing
condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin'))

- ${{ if eq(parameters.azureCLIVersion, 'min') }}:
- template: install-azure-cli-min.yml
- ${{ elseif eq(parameters.azureCLIVersion, 'released') }}:
Expand Down
47 changes: 30 additions & 17 deletions .azure-devops/templates/trigger-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,15 @@ parameters:
- name: "maxParallelDPS"
type: number
default: 1
- name: "maxParallelHub"
type: number
default: 1
- name: hubSubscription
type: string
default: a386d5ea-ea90-441a-8263-d816368c84a1
- name: hubResourceGroup
type: string
default: cli-int-test-rg
- name: hubRegion
type: string
default: centraluseuap
- name: "maxParallelADU"
type: number
default: 1
Expand Down Expand Up @@ -95,35 +101,42 @@ jobs:
pythonVersion: $(python)
serviceConnection: $(AzureServiceConnection)

- job: "testHub_job_1"
timeoutInMinutes: 120
displayName: "Test IoT Hub - certificate, config, core, jobs, state"
condition: and(succeeded(), eq('${{ parameters.testHub }}', true))
- job: "HubControl"
timeoutInMinutes: 140
displayName: "HubControl - owned control-plane"
dependsOn: [testCentral, testADT, testDPS, testADU, testADR]
condition: and(not(canceled()), eq('${{ parameters.testHub }}', true))
strategy:
matrix: $[ variables['pythonVersions'] ]
maxParallel: ${{ parameters.maxParallelHub }}
maxParallel: 1
steps:
- template: run-tests-parallel.yml
parameters:
path: "azext_iot/tests/iothub/configurations azext_iot/tests/iothub/core azext_iot/tests/iothub/jobs azext_iot/tests/iothub/state"
name: "iot-hub-1"
num_threads: 7
hubSuite: HubControl
hubSubscription: ${{ parameters.hubSubscription }}
hubResourceGroup: ${{ parameters.hubResourceGroup }}
hubRegion: ${{ parameters.hubRegion }}
name: "HubControl"
azureCLIVersion: ${{ parameters.azureCLIVersion }}
pythonVersion: $(python)
serviceConnection: $(AzureServiceConnection)

- job: "testHub_job_2"
- job: "HubData"
timeoutInMinutes: 120
condition: and(succeeded(), eq('${{ parameters.testHub }}', true))
displayName: "Test IoT Hub - devices, message endpoints, messaging, and modules"
dependsOn: HubControl
condition: and(not(canceled()), eq('${{ parameters.testHub }}', true))
displayName: "HubData - owned Entra and SAS phases"
strategy:
matrix: $[ variables['pythonVersions'] ]
maxParallel: ${{ parameters.maxParallelHub }}
maxParallel: 1
steps:
- template: run-tests-parallel.yml
parameters:
path: "azext_iot/tests/iothub/devices azext_iot/tests/iothub/messaging azext_iot/tests/iothub/modules azext_iot/tests/iothub/message_endpoint"
name: "iot-hub-2"
hubSuite: HubData
hubSubscription: ${{ parameters.hubSubscription }}
hubResourceGroup: ${{ parameters.hubResourceGroup }}
hubRegion: ${{ parameters.hubRegion }}
name: "HubData"
azureCLIVersion: ${{ parameters.azureCLIVersion }}
pythonVersion: $(python)
serviceConnection: $(AzureServiceConnection)
Expand Down
6 changes: 5 additions & 1 deletion .github/skills/generate-typespec-sdk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Known control-plane mappings:
| Service | Source directory | Entrypoint | Namespace | Client | Destination |
| --- | --- | --- | --- | --- | --- |
| IoT Hub | `specification/iothub/resource-manager/Microsoft.Devices/IoTHub` | `client.tsp` | `azext_iot.sdk.iothub.mgmt` | `IotHubClient` | `azext_iot/sdk/iothub/mgmt` |
| DPS | `specification/deviceprovisioningservices/resource-manager/Microsoft.Devices/ProvisioningService` | `client.tsp` | `azext_iot.sdk.dps.mgmt` | `IotDpsClient` | `azext_iot/sdk/dps/mgmt` |
| DPS | `specification/deviceprovisioningservices/resource-manager/Microsoft.Devices/DeviceProvisioningServices` | `client.tsp` | `azext_iot.sdk.dps.mgmt` | `IotDpsClient` | `azext_iot/sdk/dps/mgmt` |

Do not guess data-plane mappings. Ask for explicit source directory, entrypoint, namespace, client, and destination.

Expand Down Expand Up @@ -168,6 +168,7 @@ Before touching the extension repository, assert:
- the expected namespace directory exists;
- the expected client class is exported by the package;
- no `models/` directory exists;
- `types.py` containing `TypedDict` and `Literal` annotations is allowed in modeless output;
- no `aio/` directory exists;
- operation methods use synchronous definitions;
- generated Python files compile;
Expand All @@ -191,6 +192,7 @@ Compare the generated directory with the existing destination before replacement
- operation method additions, removals, and signature changes;
- default API-version and endpoint/base-URL changes;
- generated package version changes;
- runtime dependency minimums from generated `pyproject.toml` compared with extension `setup.py`;
- imports and extension call sites that may become incompatible.

Pay particular attention to:
Expand All @@ -203,6 +205,8 @@ Pay particular attention to:

This report does not block replacement when generation checks pass, but compatibility warnings must be prominent.
Do not edit call sites automatically.
Report dependency increases for the separate compatibility task as well; copying only the namespace
does not remove the generated SDK's runtime requirements.

## 7. Replace automatically with rollback

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/azdev_linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ jobs:
runs-on: ubuntu-latest
steps:
# checkout source (for linter_exclusions)
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0

# download built wheel (from ./release_build.yml)
- name: Download Wheel
uses: actions/download-artifact@v7
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: azure-iot-cli-ext
path: ./extension

# Install python
- uses: actions/setup-python@v6
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
name: Setup python
with:
python-version: "3.13"
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ jobs:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: Setup python
uses: actions/setup-python@v6
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.13"
- name: Build Wheel
run: |
pip install -r dev_requirements
python -m build
- name: Upload Wheel Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
path: dist/*.whl
name: azure-iot-cli-ext
10 changes: 10 additions & 0 deletions .github/workflows/ci_workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ concurrency:
on:
pull_request:
push:
branches:
- dev
- preview
- 1.1.0-preview
- "release/**"
tags:
- "**"
workflow_dispatch:
jobs:
build:
Expand All @@ -17,3 +24,6 @@ jobs:
linter:
needs: [build]
uses: ./.github/workflows/azdev_linter.yml
index-compatibility:
needs: [build]
uses: ./.github/workflows/index_compatibility.yml
Loading
Loading