Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
36520e2
Change pull_request to pull_request_target
ayeshurun Sep 17, 2025
444d351
Merge pull request #4 from ayeshurun/fix-semantic-pr
ayeshurun Sep 17, 2025
8b34e28
Merge branch 'microsoft:main' into main
ayeshurun Sep 18, 2025
0c47fda
Merge branch 'microsoft:main' into main
ayeshurun Sep 18, 2025
ccd7b91
Merge branch 'microsoft:main' into main
ayeshurun Sep 21, 2025
43cc353
Merge branch 'microsoft:main' into main
ayeshurun Sep 21, 2025
ea76426
Merge branch 'microsoft:main' into main
ayeshurun Sep 28, 2025
c702b91
Merge branch 'microsoft:main' into main
ayeshurun Sep 29, 2025
5e847ef
Merge branch 'microsoft:main' into main
ayeshurun Oct 16, 2025
69c733e
Merge branch 'microsoft:main' into main
ayeshurun Oct 20, 2025
5d5f1ad
Merge branch 'microsoft:main' into main
ayeshurun Oct 20, 2025
0c5de9c
Merge branch 'microsoft:main' into main
ayeshurun Oct 21, 2025
025f456
Merge branch 'microsoft:main' into main
ayeshurun Oct 22, 2025
df0c5a4
Merge branch 'microsoft:main' into main
ayeshurun Oct 23, 2025
c6892ed
Merge branch 'microsoft:main' into main
ayeshurun Oct 26, 2025
f30ce9b
Merge branch 'microsoft:main' into main
ayeshurun Nov 4, 2025
cf7e06b
Merge branch 'microsoft:main' into main
ayeshurun Nov 11, 2025
3a727dd
Merge branch 'main' of https://github.com/ayeshurun/fabric-cli
Nov 13, 2025
1de4d6b
Support host app
Nov 13, 2025
d3cfbb4
Merge branch 'main' of https://github.com/ayeshurun/fabric-cli
Nov 17, 2025
bb051b1
Merge branch 'main' of https://github.com/ayeshurun/fabric-cli
Nov 26, 2025
f46f685
Merge branch 'microsoft:main' into main
ayeshurun Dec 9, 2025
19e3a75
Merge branch 'main' of https://github.com/ayeshurun/fabric-cli
Dec 14, 2025
55020f0
Fix new functionality section in release notes
Dec 14, 2025
0c08fb5
Merge branch 'microsoft:main' into main
ayeshurun Dec 15, 2025
beb0149
Fix new functionality section in release notes
Dec 14, 2025
c0e3672
Merge branch 'main' of https://github.com/ayeshurun/fabric-cli
Dec 17, 2025
ca313c3
Introduce release workflow
Dec 17, 2025
8aa05e7
Merge pull request #14 from ayeshurun/release-workflow
ayeshurun Dec 17, 2025
39f2d85
Add GH_TOKEN to create release step
ayeshurun Dec 17, 2025
5e3b68e
Initial plan
Copilot Dec 17, 2025
7c573b0
Enhance create-release workflow with GITHUB_STEP_SUMMARY and optional…
Copilot Dec 17, 2025
f9a4bb2
Refactor GITHUB_STEP_SUMMARY messages to use heredoc syntax
Copilot Dec 18, 2025
f04c7b7
Merge pull request #16 from ayeshurun/copilot/enhance-create-release-…
ayeshurun Dec 18, 2025
641c4e7
Update release notes for v1.3.1
ayeshurun Dec 22, 2025
516dbb8
Fix new functionality section in release notes
Dec 14, 2025
484993f
Introduce release workflow
Dec 17, 2025
3a42a5c
Add GH_TOKEN to create release step
ayeshurun Dec 17, 2025
0755733
Initial plan
Copilot Dec 17, 2025
5917da7
Enhance create-release workflow with GITHUB_STEP_SUMMARY and optional…
Copilot Dec 17, 2025
635eadc
Refactor GITHUB_STEP_SUMMARY messages to use heredoc syntax
Copilot Dec 18, 2025
22cc9cb
Update release notes for v1.3.1
ayeshurun Dec 22, 2025
44f44b3
Merge branch 'main' of https://github.com/ayeshurun/fabric-cli
Dec 24, 2025
d9c9eaf
Merge branch 'main' of https://github.com/ayeshurun/fabric-cli into h…
Dec 25, 2025
bd2ad0f
Add changelog entry
Dec 25, 2025
aa06952
Remove changelog
Dec 25, 2025
756dd40
Update host app value
Dec 25, 2025
ec9ee93
Add fab host app version
Dec 25, 2025
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
52 changes: 51 additions & 1 deletion src/fabric_cli/client/fab_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Licensed under the MIT License.

import json
import os
import platform
import re
import time
Expand Down Expand Up @@ -96,9 +97,10 @@ def do_request(
from fabric_cli.core.fab_context import Context as FabContext

ctxt_cmd = FabContext().command

headers = {
"Authorization": "Bearer " + str(token),
"User-Agent": f"{fab_constant.API_USER_AGENT}/{fab_constant.FAB_VERSION} ({ctxt_cmd}; {platform.system()}; {platform.machine()}; {platform.release()})",
"User-Agent": _build_user_agent(ctxt_cmd),
}

if files is None:
Expand Down Expand Up @@ -279,6 +281,54 @@ def _handle_successful_response(args: Namespace, response: ApiResponse) -> ApiRe
return response


def _build_user_agent(ctxt_cmd: str) -> str:
"""Build the User-Agent header for API requests.

Example:
ms-fabric-cli/1.0.0 (create; Windows/10; Python/3.10.2) host-app/ado/2.0.0
"""
user_agent = f"{fab_constant.API_USER_AGENT}/{fab_constant.FAB_VERSION} ({ctxt_cmd}; {platform.system()}/{platform.release()}; Python/{platform.python_version()})"
host_app = _get_host_app()
if host_app:
user_agent += host_app

return user_agent


def _get_host_app() -> str:
"""Get the HostApp suffix for the User-Agent header based on environment variables.

Returns an empty string if the environment variable is not set or has an invalid value.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we want to print warning in those cases?
More broadly, what is the purpose of the host_app value in the request headers?

"""
_host_app_in_env = os.environ.get(fab_constant.FAB_HOST_APP_ENV_VAR)
if not _host_app_in_env:
return ""

host_app_name = next(
(
allowed_app
for allowed_app in fab_constant.ALLOWED_FAB_HOST_APP_VALUES
if _host_app_in_env.lower() == allowed_app.lower()
),
None,
)

if not host_app_name:
return ""

host_app = f" host-app/{host_app_name.lower()}"

# Check for optional version
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit - remove "what" comment

host_app_version = os.environ.get(fab_constant.FAB_HOST_APP_VERSION_ENV_VAR)

# validate host_app_version format is a valid version (e.g., 1.0.0)
if host_app_version and re.match(
r"^\d+(\.\d+){0,2}(-[a-zA-Z0-9\.-]+)?$", host_app_version
):
host_app += f"/{host_app_version}"
return host_app


def _print_response_details(response: ApiResponse) -> None:
response_details = dict(
{
Expand Down
9 changes: 9 additions & 0 deletions src/fabric_cli/core/fab_constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@
IDENTITY_TYPE: ["user", "service_principal", "managed_identity"],
}

FAB_HOST_APP_ENV_VAR = "FAB_HOST_APP"
FAB_HOST_APP_VERSION_ENV_VAR = "FAB_HOST_APP_VERSION"

# Other constants
FAB_CAPACITY_NAME_NONE = "none"
FAB_DEFAULT_OPEN_EXPERIENCE_FABRIC = "fabric-developer"
Expand Down Expand Up @@ -319,6 +322,12 @@
"folderId",
}

################################################
### Only allowed for modification by CLI team ##

ALLOWED_FAB_HOST_APP_VALUES = ["Fabric-AzureDevops-Extension"]
################################################

# Item set constants
ITEM_QUERY_DEFINITION = "definition"
ITEM_QUERY_PROPERTIES = "properties"
Expand Down
175 changes: 175 additions & 0 deletions tests/test_core/test_fab_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import pytest

from fabric_cli.client.fab_api_client import (
_get_host_app,
_transform_workspace_url_for_private_link_if_needed,
do_request,
)
Expand Down Expand Up @@ -309,6 +310,180 @@ def __init__(self):
assert "ErrorCode" == excinfo.value.status_code


@pytest.mark.parametrize(
"host_app_env, host_app_version_env, expected_suffix",
[
(
"Fabric-AzureDevops-Extension",
None,
" host-app/fabric-azuredevops-extension",
),
(
"Fabric-AzureDevops-Extension",
"1.2.0",
" host-app/fabric-azuredevops-extension/1.2.0",
),
(
"fabric-azuredevops-extension",
"1.2.0",
" host-app/fabric-azuredevops-extension/1.2.0",
),
("Invalid-App", "1.0.0", ""),
("", None, ""),
(None, None, ""),
(
"Fabric-AzureDevops-Extension",
"1.2.0.4", # Invalid format
" host-app/fabric-azuredevops-extension",
),
(
"Fabric-AzureDevops-Extension",
"1.2.a", # Invalid format
" host-app/fabric-azuredevops-extension",
),
(
"Fabric-AzureDevops-Extension",
"a.b.c", # Invalid format
" host-app/fabric-azuredevops-extension",
),
(
"Fabric-AzureDevops-Extension",
"1", # valid format
" host-app/fabric-azuredevops-extension/1",
),
(
"Fabric-AzureDevops-Extension",
"1.2", # valid format
" host-app/fabric-azuredevops-extension/1.2",
),
(
"Fabric-AzureDevops-Extension",
"1.0.0", # valid format
" host-app/fabric-azuredevops-extension/1.0.0",
),
(
"Fabric-AzureDevops-Extension",
"1.0.0-rc.1", # valid format
" host-app/fabric-azuredevops-extension/1.0.0-rc.1",
),
(
"Fabric-AzureDevops-Extension",
"1.0.0-alpha", # valid format
" host-app/fabric-azuredevops-extension/1.0.0-alpha",
),
(
"Fabric-AzureDevops-Extension",
"1.0.0-beta", # valid format
" host-app/fabric-azuredevops-extension/1.0.0-beta",
),
],
)
def test_get_host_app(host_app_env, host_app_version_env, expected_suffix, monkeypatch):
"""Test the _get_host_app helper function."""
if host_app_env is not None:
monkeypatch.setenv(fab_constant.FAB_HOST_APP_ENV_VAR, host_app_env)
else:
monkeypatch.delenv(fab_constant.FAB_HOST_APP_ENV_VAR, raising=False)

if host_app_version_env is not None:
monkeypatch.setenv(
fab_constant.FAB_HOST_APP_VERSION_ENV_VAR, host_app_version_env
)
else:
monkeypatch.delenv(fab_constant.FAB_HOST_APP_VERSION_ENV_VAR, raising=False)

result = _get_host_app()

assert result == expected_suffix


@pytest.fixture()
def setup_default_private_links(mock_fab_set_state_config):
mock_fab_set_state_config(fab_constant.FAB_WS_PRIVATE_LINKS_ENABLED, "true")


@patch("platform.python_version", return_value="3.11.5")
@patch("platform.release", return_value="5.4.0")
@patch("platform.system", return_value="Linux")
@patch("requests.Session.request")
@patch("fabric_cli.core.fab_auth.FabAuth")
@patch("fabric_cli.core.fab_context.Context")
@pytest.mark.parametrize(
"host_app_env, host_app_version_env, expected_suffix",
[
(None, None, ""),
(
"Fabric-AzureDevops-Extension",
None,
" host-app/fabric-azuredevops-extension",
),
(
"Fabric-AzureDevops-Extension",
"1.2.0",
" host-app/fabric-azuredevops-extension/1.2.0",
),
("Invalid-App", "1.0.0", ""),
],
)
def test_do_request_user_agent_header(
mock_context,
mock_auth,
mock_request,
mock_system,
mock_release,
mock_python_version,
host_app_env,
host_app_version_env,
expected_suffix,
monkeypatch,
):
"""Test User-Agent header construction with and without host app identifier."""
if host_app_env is not None:
monkeypatch.setenv(fab_constant.FAB_HOST_APP_ENV_VAR, host_app_env)
else:
monkeypatch.delenv(fab_constant.FAB_HOST_APP_ENV_VAR, raising=False)

if host_app_version_env is not None:
monkeypatch.setenv(
fab_constant.FAB_HOST_APP_VERSION_ENV_VAR, host_app_version_env
)
else:
monkeypatch.delenv(fab_constant.FAB_HOST_APP_VERSION_ENV_VAR, raising=False)

# Configure mocks
mock_auth.return_value.get_access_token.return_value = "dummy-token"
mock_context.return_value.command = "test-command"

class DummyResponse:
status_code = 200
text = "{}"
content = b"{}"
headers = {}

mock_request.return_value = DummyResponse()

dummy_args = Namespace(
uri="items",
method="get",
audience=None,
headers=None,
wait=False,
raw_response=True,
request_params={},
json_file=None,
)

do_request(dummy_args)

# Verify the User-Agent header from the actual request call
call_kwargs = mock_request.call_args.kwargs
headers = call_kwargs["headers"]
user_agent = headers["User-Agent"]

base_user_agent = (
f"{fab_constant.API_USER_AGENT}/{fab_constant.FAB_VERSION} "
f"(test-command; Linux/5.4.0; Python/3.11.5)"
)
expected_user_agent = base_user_agent + expected_suffix

assert user_agent == expected_user_agent