Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ permissions:
contents: read

jobs:
# Detect which product areas changed so unrelated suites can be skipped while
# workflow edits and ambiguous Git history still trigger every suite.
changes:
name: detect changed areas
runs-on: ubuntu-latest
Expand Down Expand Up @@ -63,6 +65,8 @@ jobs:
echo "${area//_/-}=${matched}" >> "${GITHUB_OUTPUT}"
done

# Give non-main branch pushes fast CU CLI feedback without duplicating the
# full pull-request matrix that runs for the same commit.
quick-test:
name: quick test (ubuntu-latest, py3.12)
needs: changes
Expand All @@ -83,6 +87,73 @@ jobs:
- name: Run CI
run: bash cu-cli/scripts/ci.sh

# Build and install the extension/core wheel pair in an isolated Azure CLI
# directory, catching missing dependencies and extension loading failures.
extension-wheel-smoke:
Comment thread
chienyuanchang marked this conversation as resolved.
name: extension wheel smoke test (ubuntu-latest, py${{ matrix.python-version }})
needs: changes
if: needs.changes.outputs.cu-cli == 'true' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: |
cu-cli/packages/core/pyproject.toml
cu-cli/packages/azure-cli-extension/pyproject.toml
- name: Build extension and core wheels
working-directory: cu-cli
run: |
python -m pip install "build==1.3.0"
python -m build --wheel packages/core
python -m build --wheel packages/azure-cli-extension
- name: Test clean extension installation
working-directory: cu-cli
run: >-
bash scripts/validate_extension_wheel.sh
packages/azure-cli-extension/dist/content_understanding-*.whl
packages/core/dist/cu_cli_core-*.whl

# Run the upstream Azure CLI command linter and source style checks, catching
# CLI convention violations before the extension is submitted upstream.
extension-azdev-linter:
name: extension azdev linter (ubuntu-latest, py3.14)
needs: changes
if: needs.changes.outputs.cu-cli == 'true' && (github.event_name == 'pull_request' || github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
cache: pip
cache-dependency-path: |
cu-cli/packages/core/pyproject.toml
cu-cli/packages/azure-cli-extension/pyproject.toml
- name: Build extension and core wheels
working-directory: cu-cli
run: |
python -m pip install "build==1.3.0"
python -m build --wheel packages/core
python -m build --wheel packages/azure-cli-extension
- name: Run Azure CLI extension linter
working-directory: cu-cli
run: >-
bash scripts/validate_extension_azdev.sh
packages/azure-cli-extension/dist/content_understanding-*.whl
packages/core/dist/cu_cli_core-*.whl
packages/azure-cli-extension

# Exercise calibration behavior only when Dynamic HITL changes, independently
# from CU CLI packaging and command tests.
dynamic-hitl-test:
name: dynamic HITL calibration tests (ubuntu-latest, py3.14)
needs: changes
Expand All @@ -106,6 +177,8 @@ jobs:
- name: Run calibration tests
run: python -m pytest test_calibration.py -q

# Run the complete CU CLI suite across supported Python versions and operating
# systems, catching platform-specific behavior and packaging regressions.
full-test:
name: full test (${{ matrix.os }}, py${{ matrix.python-version }})
needs: changes
Expand Down
31 changes: 30 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ jobs:
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
# Reject mismatched versions, commits, targets, changelogs, dependency
# bounds, or release notes before building immutable artifacts.
- name: Validate release request
env:
RELEASE_TARGET: ${{ inputs.target }}
Expand All @@ -74,9 +76,13 @@ jobs:
--actual-commit "${GITHUB_SHA}" \
--repository "${GITHUB_REPOSITORY}" \
--ref "${GITHUB_REF}" \
--release-notes-output dist/release-notes.md \
${{ (inputs.package == 'cli' || inputs.package == 'extension') && '--verify-core-on-index' || '' }}
# Re-run the full local suite against the exact requested release commit.
- name: Test exact release commit
run: bash scripts/ci.sh
# Build only the selected distribution and verify its package metadata;
# extension releases also receive a checksum artifact.
- name: Build selected distribution
env:
PACKAGE: ${{ inputs.package }}
Expand All @@ -100,6 +106,29 @@ jobs:
if [[ "${PACKAGE}" == "extension" ]]; then
(cd dist && sha256sum *.whl > "content-understanding-${{ inputs.version }}.sha256")
fi
# Install the candidate extension/core wheels in an isolated Azure CLI
# directory to catch unresolved dependencies and command loading failures.
- name: Test clean extension installation
Comment thread
chienyuanchang marked this conversation as resolved.
if: inputs.package == 'extension'
run: >-
bash scripts/validate_extension_wheel.sh
dist/content_understanding-*.whl
packages/core/dist/cu_cli_core-*.whl
# Match the Python version used for the upstream Azure CLI tooling checks.
- name: Set up Azure CLI validation Python
if: inputs.package == 'extension'
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.14"
# Catch Azure CLI command convention and source style violations using the
# same pinned azdev checks exercised by CI.
- name: Run Azure CLI extension linter
if: inputs.package == 'extension'
run: >-
bash scripts/validate_extension_azdev.sh
dist/content_understanding-*.whl
packages/core/dist/cu_cli_core-*.whl
packages/azure-cli-extension
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dist-${{ inputs.target }}-${{ inputs.package }}-${{ inputs.version }}
Expand Down Expand Up @@ -168,5 +197,5 @@ jobs:
--repo "${GITHUB_REPOSITORY}"
--target "${RELEASE_COMMIT}"
--title "Azure Content Understanding CLI extension ${RELEASE_VERSION}"
--notes "Preview release of the Azure Content Understanding CLI extension."
--notes-file dist/release-notes.md
--prerelease
6 changes: 6 additions & 0 deletions cu-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

## 0.1.0b3 (2026-09-14)

### Bugs Fixed

- Minor bug fixes.

## 0.1.0b2 (2026-09-11)

### Features Added
Expand Down
26 changes: 26 additions & 0 deletions cu-cli/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,32 @@ az cu --help
In PowerShell, set `$env:PIP_FIND_LINKS = (Resolve-Path ../core/dist)` before
running `az extension add`.

Comment thread
chienyuanchang marked this conversation as resolved.
Before releasing the extension, validate the built wheels in a clean Azure CLI
environment. This check installs only dependencies resolved from the wheels and
loads the command group with `az cu --help`:

```bash
cd ../..
bash scripts/validate_extension_wheel.sh \
packages/azure-cli-extension/dist/content_understanding-*.whl \
packages/core/dist/cu_cli_core-*.whl
```

Run the Azure CLI extensions linter before publishing. This clones clean,
temporary copies of the Azure CLI `dev` branch and the extensions repository,
then runs the same pinned `azdev` wheel linter used by their pipeline:

```bash
bash scripts/validate_extension_azdev.sh \
packages/azure-cli-extension/dist/content_understanding-*.whl \
packages/core/dist/cu_cli_core-*.whl \
packages/azure-cli-extension
```

The clean wheel installation and pinned `azdev` checks run automatically in
both `.github/workflows/ci.yml` for pull requests and
`.github/workflows/release.yml` before an extension artifact is published.

## Running checks

```bash
Expand Down
6 changes: 6 additions & 0 deletions cu-cli/packages/azure-cli-extension/HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Release History
===============

0.1.0b2 (2026-09-14)
+++++++++++++++++++++

* Correct loading of the Azure AI Content Understanding SDK installed through
the shared ``cu-cli-core`` 0.1.0b3 package in isolated Azure CLI environments.

0.1.0b1 (2026-09-11)
+++++++++++++++++++++

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from azure.cli.core import AzCommandsLoader

from ._help import helps as helps
from ._help import helps as helps # pylint: disable=unused-import,useless-import-alias

__version__ = version("content-understanding")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
)
from cu_cli_core.profiles import Profile
from cu_cli_core.reporting import build_analysis_report
from cu_cli_core.serialization import to_plain_value
from cu_cli_core.serialization import render_llm_input, to_plain_value

from ._client_factory import create_content_understanding_client
from ._io import write_json, write_text
Expand Down Expand Up @@ -170,9 +170,7 @@ def persist(outcome: Any) -> None:
usage = response.usage if isinstance(response, AnalyzeResponse) else None
result = response.result if isinstance(response, AnalyzeResponse) else response
if request.llm_input:
from azure.ai.contentunderstanding import to_llm_input

payload = to_llm_input(result)
payload = render_llm_input(result)
if not isinstance(payload, str) or not payload.strip():
raise ServiceError("analysis succeeded, but the model-input result was empty.")
else:
Expand Down Expand Up @@ -232,4 +230,4 @@ def persist(outcome: Any) -> None:
f"{len(batch.failures)} analysis job(s) failed.",
hint="Use --report-file to retain per-input status without exposing SAS tokens.",
)
return streamed if len(jobs) == 1 and jobs[0].out_path is None else records
return streamed if len(jobs) == 1 and jobs[0].out_path is None else records
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,4 @@ def copy_analyzer(cmd: Any, **values: Any) -> Any:
target_region=destination.resource.region if cross_resource else None,
source_analyzer=source_analyzer,
target_cli_options=target_options,
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ def set_defaults(cmd: Any, **values: Any) -> Any:
updated, _ = resolve_identifier(DEFAULTS_SET.operation)(
_client(cmd, values), desired, replace=request.replace
)
return updated
return updated
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ def doctor(cmd: Any, **values: Any) -> dict[str, Any]:
def list_environment_variables(_cmd: Any, **values: Any) -> Any:
request = build_request(ENV_VAR_LIST, values)
del request
return resolve_identifier(ENV_VAR_LIST.operation)()
return resolve_identifier(ENV_VAR_LIST.operation)()
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
from pathlib import Path
from typing import Any, Iterable

from azure.ai.contentunderstanding import ContentUnderstandingClient
from azure.core.credentials import AzureKeyCredential
from azure.cli.core.util import get_az_user_agent
from azure.mgmt.cognitiveservices.models import Deployment, DeploymentModel, DeploymentProperties, Sku
from knack.prompting import prompt

from cu_cli_core.client import build_content_understanding_client
from cu_cli_core.errors import ConflictError, ServiceError, UsageError
from cu_cli_core.defaults import apply_defaults
from cu_cli_core.infra_models import (
Expand All @@ -33,6 +33,7 @@
)
from ._resources import _management_client


def _value(value: Any, snake: str, camel: str | None = None) -> Any:
if isinstance(value, dict):
return value.get(snake, value.get(camel or snake))
Expand Down Expand Up @@ -120,7 +121,7 @@ def setup_models(cmd: Any, **values: Any) -> dict[str, Any]:
key = str(keys.key1 or keys.key2 or "")
if not key:
raise ServiceError("the Microsoft Foundry resource returned no account key.")
cu_client = ContentUnderstandingClient(
cu_client = build_content_understanding_client(
endpoint=endpoint,
credential=AzureKeyCredential(key),
api_version=api_version,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ def write_text(path: Path, content: str, *, overwrite: bool) -> None:

def write_json(path: Path, value: Any, *, overwrite: bool) -> None:
payload = json.dumps(to_plain_value(value), indent=2, ensure_ascii=False)
write_text(path, payload + "\n", overwrite=overwrite)
write_text(path, payload + "\n", overwrite=overwrite)
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
}


# pylint: disable=too-few-public-methods
class _ExplicitArgumentContext:
"""Register arguments absent from variadic command wrapper signatures."""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,4 @@ def sync_profile_defaults(cmd: Any, **values: Any) -> dict[str, Any]:
"name": target,
"path": str(path),
"modelDeployments": models,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,4 @@ def list_model_deployments(cmd: Any, endpoint: str) -> list[dict[str, Any]]:
"capacity": getattr(sku, "capacity", None),
}
)
return sorted(result, key=lambda item: item["name"].casefold())
return sorted(result, key=lambda item: item["name"].casefold())
4 changes: 2 additions & 2 deletions cu-cli/packages/azure-cli-extension/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "content-understanding"
version = "0.1.0b1"
version = "0.1.0b2"
description = "Azure CLI extension for Azure Content Understanding."
readme = "README.md"
requires-python = ">=3.10"
Expand All @@ -20,7 +20,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
]
dependencies = [
"cu-cli-core>=0.1.0b2,<0.2.0",
"cu-cli-core>=0.1.0b3,<0.2.0",
"azure-mgmt-cognitiveservices>=13.6.0,<14.0.0",
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,9 @@ def execute(_client, _request, *, jobs, on_result, **_kwargs):
return SimpleNamespace(failures=[])

monkeypatch.setattr(_analysis, "resolve_identifier", lambda _operation: execute)
import azure.ai.contentunderstanding as content_understanding

monkeypatch.setattr(
content_understanding,
"to_llm_input",
_analysis,
"render_llm_input",
lambda result: captured.setdefault("result", result) and formatted,
)

Expand Down
Loading
Loading