From 3e4359ad18cf890b9eb7309a5db63170c6a7aead Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Thu, 23 Jul 2026 11:45:47 -0700 Subject: [PATCH 01/22] Route update_appsettings version lookups through private Azure Artifacts feed The update_appsettings job's update-versions.ps1 queried public package registries (NuGet, npm, PyPI, Maven, etc.) directly, causing 350+ unauthenticated egress requests. Repoint the NuGet, npm, PyPI and Maven lookups at the authenticated GraphDeveloperExperiences_Public Azure Artifacts feed (which has upstream sources for those ecosystems), authenticating with the pipeline System.AccessToken. PyPI switches to the PEP 691 JSON simple index since Azure Artifacts does not expose the legacy pypi.org JSON API. NuGet resolves the registrations base from the service index instead of a hard-coded path. PHP (Packagist), Dart (pub.dev), Go and Ruby have no Azure Artifacts upstream type and remain on public registries. Feed URLs default to the public registries so local runs are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 19 ++++++++- scripts/update-versions.ps1 | 79 +++++++++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 444401e199..bb9c3144f4 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -107,6 +107,11 @@ extends: name: Azure-Pipelines-1ESPT-ExDShared os: linux image: ubuntu-latest + variables: + # Base URL for the authenticated Azure Artifacts feed (GraphDeveloperExperiences_Public) used + # to route package-version lookups through configured upstream sources instead of public registries. + # NOTE: confirm these against the feed's "Connect to Feed" dialog if the feed/org/project changes. + privateFeedBaseUrl: "https://microsoftgraph.pkgs.visualstudio.com/0985d294-5762-4bc2-a565-161ef349ca3e/_packaging/GraphDeveloperExperiences_Public" templateContext: sdl: baseline: @@ -118,8 +123,20 @@ extends: clean: true submodules: true - - pwsh: $(Build.SourcesDirectory)/scripts/update-versions.ps1 + # Route package-version lookups through the authenticated Azure Artifacts feed + # (GraphDeveloperExperiences_Public) so CI does not call public registries directly. + # NuGet, npm, PyPI and Maven Central are configured as upstream sources on that feed. + # PHP (Packagist), Dart (pub.dev), Go (api.github.com) and Ruby (rubygems.org) have no + # Azure Artifacts upstream type, so those lookups remain on public (allow-listed) registries. + - pwsh: > + $(Build.SourcesDirectory)/scripts/update-versions.ps1 + -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json" + -NpmRegistryUrl "$(privateFeedBaseUrl)/npm/registry" + -PyPiSimpleIndexUrl "$(privateFeedBaseUrl)/pypi/simple" + -MavenRepositoryUrl "$(privateFeedBaseUrl)/maven/v1" displayName: "Update dependencies versions" + env: + FEED_ACCESS_TOKEN: $(System.AccessToken) - pwsh: | New-Item -Path $(Build.ArtifactStagingDirectory)/AppSettings -ItemType Directory -Force -Verbose diff --git a/scripts/update-versions.ps1 b/scripts/update-versions.ps1 index 7e675cafd2..2c6218f8fb 100644 --- a/scripts/update-versions.ps1 +++ b/scripts/update-versions.ps1 @@ -1,11 +1,60 @@ +[CmdletBinding()] +param( + # NuGet v3 service index. Point at a private Azure Artifacts feed's nuget/v3/index.json to route through upstreams. + [string]$NuGetServiceIndexUrl = "https://api.nuget.org/v3/index.json", + # npm registry base URL. Point at a private feed's npm/registry endpoint to route through upstreams. + [string]$NpmRegistryUrl = "https://registry.npmjs.org", + # PyPI PEP 503/691 simple index base URL. Point at a private feed's pypi/simple endpoint to route through upstreams. + [string]$PyPiSimpleIndexUrl = "https://pypi.org/simple", + # Maven repository base URL. Point at a private feed's maven/v1 endpoint to route through upstreams. + [string]$MavenRepositoryUrl = "https://repo1.maven.org/maven2", + # Access token used to authenticate to the private feed (e.g. the pipeline System.AccessToken). + # Leave empty for anonymous access to the public registries. + [string]$FeedAccessToken = $env:FEED_ACCESS_TOKEN +) + +# Normalize trailing slashes so URL composition is predictable. +$NpmRegistryUrl = $NpmRegistryUrl.TrimEnd('/') +$PyPiSimpleIndexUrl = $PyPiSimpleIndexUrl.TrimEnd('/') +$MavenRepositoryUrl = $MavenRepositoryUrl.TrimEnd('/') + +# Build the auth headers once. Azure Artifacts protocol endpoints accept the pipeline OAuth token as a Bearer token. +$script:FeedAuthHeaders = @{} +if (-not [string]::IsNullOrWhiteSpace($FeedAccessToken)) { + $script:FeedAuthHeaders["Authorization"] = "Bearer $FeedAccessToken" +} + +# Cache for the resolved NuGet registrations base URL so the service index is only read once. +$script:NuGetRegistrationsBaseUrl = $null + +# Resolve the NuGet registrations base URL from the service index. Prefers the gzipped SemVer 2.0.0 +# resource, matching the previously hard-coded registration5-gz-semver2 endpoint, and works against a +# private Azure Artifacts feed as well as the public api.nuget.org index. +function Get-NugetRegistrationsBaseUrl { + if ($null -ne $script:NuGetRegistrationsBaseUrl) { + return $script:NuGetRegistrationsBaseUrl + } + $index = Invoke-RestMethod -Uri $NuGetServiceIndexUrl -Method Get -Headers $script:FeedAuthHeaders + $preferredTypes = @("RegistrationsBaseUrl/3.6.0", "RegistrationsBaseUrl/Versioned", "RegistrationsBaseUrl") + foreach ($type in $preferredTypes) { + $resource = $index.resources | Where-Object { $_.'@type' -eq $type } | Select-Object -First 1 + if ($null -ne $resource) { + $script:NuGetRegistrationsBaseUrl = $resource.'@id'.TrimEnd('/') + return $script:NuGetRegistrationsBaseUrl + } + } + throw "Could not find a RegistrationsBaseUrl resource in the NuGet service index at $NuGetServiceIndexUrl" +} + # Get the latest version of a package from NuGet function Get-LatestNugetVersion { param( [string]$packageId ) - $url = "https://api.nuget.org/v3/registration5-gz-semver2/$($packageId.ToLowerInvariant())/index.json" - $response = Invoke-RestMethod -Uri $url -Method Get + $registrationsBaseUrl = Get-NugetRegistrationsBaseUrl + $url = "$registrationsBaseUrl/$($packageId.ToLowerInvariant())/index.json" + $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:FeedAuthHeaders $version = $response.items | Select-Object -ExpandProperty upper | ForEach-Object { [System.Management.Automation.SemanticVersion]$_ } | sort-object | Select-Object -Last 1 $version.ToString() } @@ -24,8 +73,8 @@ function Get-LatestNpmVersion { param( [string]$packageId ) - $url = "https://registry.npmjs.org/$($packageId.ToLowerInvariant())/latest" - $response = Invoke-RestMethod -Uri $url -Method Get + $url = "$NpmRegistryUrl/$($packageId.ToLowerInvariant())/latest" + $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:FeedAuthHeaders $response.version } # Get the latest version of a maven package @@ -33,8 +82,8 @@ function Get-LatestMavenVersion { param( [string]$packageId ) - $url = "https://repo1.maven.org/maven2/$($packageId.Replace(":", "/").Replace(".", "/").Replace("|", "."))/maven-metadata.xml" - $response = Invoke-RestMethod -Uri $url -Method Get + $url = "$MavenRepositoryUrl/$($packageId.Replace(":", "/").Replace(".", "/").Replace("|", "."))/maven-metadata.xml" + $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:FeedAuthHeaders $response.metadata.versioning.latest } # Get the latest version of a composer package @@ -51,9 +100,21 @@ function Get-LatestPypiVersion { param( [string]$packageId ) - $url = "https://pypi.org/pypi/$($packageId.ToLowerInvariant())/json" - $response = Invoke-RestMethod -Uri $url -Method Get - $response.info.version + # Use the PEP 503/691 simple index (supported by both public PyPI and Azure Artifacts feeds) instead of + # the legacy pypi.org "/pypi/{pkg}/json" API, which private Azure Artifacts feeds do not expose. + $normalizedId = $packageId.ToLowerInvariant().Replace("_", "-").Replace(".", "-") + $url = "$PyPiSimpleIndexUrl/$normalizedId/" + $headers = $script:FeedAuthHeaders.Clone() + $headers["Accept"] = "application/vnd.pypi.simple.v1+json" + $response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers + # Prefer the highest final (non pre-release) version to match the previous behaviour of pypi's info.version. + $stableVersions = $response.versions | Where-Object { $_ -match '^[0-9]+(\.[0-9]+)*$' } + if ($stableVersions) { + ($stableVersions | ForEach-Object { [version]$_ } | Sort-Object | Select-Object -Last 1).ToString() + } + else { + $response.versions | Select-Object -Last 1 + } } # Get the latest version of a rubygem package function Get-LatestRubygemVersion { From b8971bf83e6a59b08ccca2654b79efb42887538a Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Thu, 23 Jul 2026 12:07:53 -0700 Subject: [PATCH 02/22] ci: route build_binaries NuGet restore through private feed The *_build_binaries jobs run 'dotnet publish' without authenticating to or pointing at the GraphDeveloperExperiences_Public Azure Artifacts feed, so NuGet restore fell back to the public api.nuget.org and failed network isolation. Add NuGetAuthenticate@1 and generate a private-feed nuget.config before the publish steps, mirroring the existing build job. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index bb9c3144f4..21eaaf6199 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -487,6 +487,21 @@ extends: - pwsh: $(Build.SourcesDirectory)/scripts/update-version-suffix-for-source-generator.ps1 -versionSuffix "$(versionSuffix)" displayName: "Set version suffix in csproj for generators" + - task: NuGetAuthenticate@1 + displayName: 'Authenticate to Azure Artifacts' + + - pwsh: | + @" + + + + + + + + "@ | Set-Content -Path "$(Build.SourcesDirectory)/nuget.config" -Encoding UTF8 + displayName: 'Create nuget.config (central feed)' + - pwsh: dotnet publish src/kiota/kiota.csproj -c Release --runtime ${{ distribution.architecture }} -p:PublishSingleFile=true --self-contained --output $(Build.ArtifactStagingDirectory)/binaries/${{ distribution.architecture }} --version-suffix "$(versionSuffix)" -f net10.0 condition: eq(variables['isPrerelease'], 'true') displayName: publish kiota as executable From 714018dc57b6f95e3ac65ae0dbf9f68cf4916b5d Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Thu, 23 Jul 2026 12:17:00 -0700 Subject: [PATCH 03/22] ci: exclude it/ integration fixtures from Component Governance scanning The 1ES Official template injects Component Governance / Component Detection into build-stage jobs (including *_build_binaries). Its Maven detector runs 'mvn dependency:tree' against the it/java and it/mockserver pom.xml fixtures, reaching repo.maven.apache.org, and other per-language detectors resolve the it/ fixtures against their public registries -- all failing network isolation. These fixtures are integration-test artifacts, not dependencies of the shipped kiota binary, so exclude the it/ directory via sdl.componentgovernance.ignoreDirectories. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 21eaaf6199..e5a29fafef 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -99,6 +99,13 @@ extends: name: Azure-Pipelines-1ESPT-ExDShared os: windows image: windows-latest + componentgovernance: + # The it/ directory holds per-language integration-test fixtures (Java/Maven, PHP/Composer, + # Dart/Pub, Python, Go, Ruby, TypeScript). Component Detection would otherwise resolve those + # manifests against public registries (e.g. the Maven detector runs `mvn dependency:tree`, + # reaching repo.maven.apache.org), which fails network isolation. These fixtures are not + # dependencies of the shipped kiota binary, so exclude them from CG scanning. + ignoreDirectories: "$(Build.SourcesDirectory)/it" stages: - stage: build jobs: From 622a127e7d256ff49cd4c4d1b04cb0a0cb27619c Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Thu, 23 Jul 2026 13:23:04 -0700 Subject: [PATCH 04/22] fix(update-versions): use npm packument instead of /latest dist-tag path Azure Artifacts npm feeds do not implement the /{package}/latest dist-tag shortcut and return 404 for it. Fetch the full packument at /{package} and read dist-tags.latest instead, which works against both the private feed and public registry.npmjs.org. Scoped names have their '/' encoded as %2F. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- scripts/update-versions.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/update-versions.ps1 b/scripts/update-versions.ps1 index 2c6218f8fb..2dc99eebeb 100644 --- a/scripts/update-versions.ps1 +++ b/scripts/update-versions.ps1 @@ -73,9 +73,14 @@ function Get-LatestNpmVersion { param( [string]$packageId ) - $url = "$NpmRegistryUrl/$($packageId.ToLowerInvariant())/latest" + # Azure Artifacts npm feeds do not implement the /{package}/latest dist-tag shortcut (they return + # 404 for it). Fetch the full packument at /{package} instead and read dist-tags.latest, which works + # against both the private feed and public registry.npmjs.org. Scoped names (e.g. + # @microsoft/kiota-abstractions) must have their '/' encoded as %2F for the packument request. + $encodedId = $packageId.ToLowerInvariant().Replace('/', '%2F') + $url = "$NpmRegistryUrl/$encodedId" $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:FeedAuthHeaders - $response.version + $response.'dist-tags'.latest } # Get the latest version of a maven package function Get-LatestMavenVersion { From fc515346bc397b374674231567e84f6238194579 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Thu, 23 Jul 2026 14:09:07 -0700 Subject: [PATCH 05/22] fix(ci): exclude it/ from Component Governance under both scan roots Component Detection --IgnoreDirectories matches comma-separated absolute paths against its scan root, which can be either Build.SourcesDirectory or Build.Repository.LocalPath depending on the injected job. Listing the it/ integration-test fixtures under only one root let the Maven detector still run mvn dependency:tree against it/**/pom.xml, egressing to repo.maven.apache.org and failing network isolation in the update_appsettings job. List both roots so the exclusion always matches. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index e5a29fafef..8785c2bd8e 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -105,7 +105,11 @@ extends: # manifests against public registries (e.g. the Maven detector runs `mvn dependency:tree`, # reaching repo.maven.apache.org), which fails network isolation. These fixtures are not # dependencies of the shipped kiota binary, so exclude them from CG scanning. - ignoreDirectories: "$(Build.SourcesDirectory)/it" + # ignoreDirectories maps to Component Detection's --IgnoreDirectories (comma-separated absolute + # paths, matched against the scan root — not globs). The scan root can be either + # $(Build.SourcesDirectory) or $(Build.Repository.LocalPath) depending on the injected job, so + # list the fixture directory under both roots to guarantee the exclusion matches. + ignoreDirectories: "$(Build.SourcesDirectory)/it,$(Build.Repository.LocalPath)/it" stages: - stage: build jobs: From 96b33293dbe5cf63edbc5763de3f77418e7facb9 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Thu, 23 Jul 2026 15:39:22 -0700 Subject: [PATCH 06/22] ci: add CFSClean network isolation policy Set parameters.settings.networkIsolationPolicy to "Permissive,CFSClean" on the 1ES Official template so the pipeline enforces Central Feed Service isolation: package restores are constrained to approved/authenticated feeds and direct calls to public registries are blocked. Complements routing package-version lookups and NuGet restores through the GraphDeveloperExperiences_Public feed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 8785c2bd8e..8eddf00e99 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -94,6 +94,13 @@ resources: extends: template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates parameters: + settings: + # Network isolation policy. "Permissive" is the base outbound policy; "CFSClean" overlays the + # Central Feed Service enforcement so package restores are constrained to approved/authenticated + # feeds and direct calls to public registries (nuget.org, npmjs, pypi.org, Maven Central, etc.) + # are blocked. This complements routing package-version lookups and restores through the + # GraphDeveloperExperiences_Public Azure Artifacts feed. See https://aka.ms/1espt-networkisolation + networkIsolationPolicy: Permissive,CFSClean sdl: sourceAnalysisPool: name: Azure-Pipelines-1ESPT-ExDShared From 89301f6bed202a4106f49db9c6cc831bf5f60947 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Thu, 23 Jul 2026 19:22:10 -0700 Subject: [PATCH 07/22] ci: drop Permissive from network isolation policy Set networkIsolationPolicy to "CFSClean" only (removing the "Permissive" base) so calls to banned public endpoints fail the run instead of being allowed. This surfaces exactly which steps still egress to public registries for diagnosis. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 8eddf00e99..123b024b74 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -95,12 +95,13 @@ extends: template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates parameters: settings: - # Network isolation policy. "Permissive" is the base outbound policy; "CFSClean" overlays the - # Central Feed Service enforcement so package restores are constrained to approved/authenticated - # feeds and direct calls to public registries (nuget.org, npmjs, pypi.org, Maven Central, etc.) - # are blocked. This complements routing package-version lookups and restores through the - # GraphDeveloperExperiences_Public Azure Artifacts feed. See https://aka.ms/1espt-networkisolation - networkIsolationPolicy: Permissive,CFSClean + # Network isolation policy. "CFSClean" enforces Central Feed Service isolation: package restores + # are constrained to approved/authenticated feeds and direct calls to public registries + # (nuget.org, npmjs, pypi.org, Maven Central, etc.) are blocked. The "Permissive" base policy is + # intentionally omitted so banned-endpoint calls fail the run (rather than being allowed), + # surfacing exactly which steps still egress to public registries. + # See https://aka.ms/1espt-networkisolation + networkIsolationPolicy: CFSClean sdl: sourceAnalysisPool: name: Azure-Pipelines-1ESPT-ExDShared From 6b4260f94868fdeb547a0bc3e866292fb632cc73 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 24 Jul 2026 11:12:27 -0700 Subject: [PATCH 08/22] ci: add GitHub allow policy to network isolation Set networkIsolationPolicy to "CFSClean,GitHub" so github.com / api.github.com (used by the Go dependency release lookups) is permitted while CFSClean still blocks direct public package-registry egress. Permissive remains omitted so other banned-endpoint calls continue to fail for diagnosis. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 123b024b74..97fb3c0011 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -96,12 +96,14 @@ extends: parameters: settings: # Network isolation policy. "CFSClean" enforces Central Feed Service isolation: package restores - # are constrained to approved/authenticated feeds and direct calls to public registries - # (nuget.org, npmjs, pypi.org, Maven Central, etc.) are blocked. The "Permissive" base policy is - # intentionally omitted so banned-endpoint calls fail the run (rather than being allowed), - # surfacing exactly which steps still egress to public registries. + # are constrained to approved/authenticated feeds and direct calls to public package registries + # (nuget.org, npmjs, pypi.org, Maven Central, etc.) are blocked. "GitHub" is a shared allow policy + # that permits github.com / api.github.com (needed for the Go dependency release lookups). The + # "Permissive" base policy is intentionally omitted so other banned-endpoint calls still fail the + # run, surfacing exactly which steps egress to public registries. # See https://aka.ms/1espt-networkisolation - networkIsolationPolicy: CFSClean + networkIsolationPolicy: CFSClean,GitHub + sdl: sdl: sourceAnalysisPool: name: Azure-Pipelines-1ESPT-ExDShared From 66d5741a2d6766066e3fc30682e87fe2209f7686 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 24 Jul 2026 11:25:42 -0700 Subject: [PATCH 09/22] ci: reorder network isolation policy to GitHub,CFSClean Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 97fb3c0011..ea84b3b5af 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -102,8 +102,7 @@ extends: # "Permissive" base policy is intentionally omitted so other banned-endpoint calls still fail the # run, surfacing exactly which steps egress to public registries. # See https://aka.ms/1espt-networkisolation - networkIsolationPolicy: CFSClean,GitHub - sdl: + networkIsolationPolicy: GitHub,CFSClean sdl: sourceAnalysisPool: name: Azure-Pipelines-1ESPT-ExDShared From d183efa6fb82f5a3112e3fc88ae4f8a06c95e6a1 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 24 Jul 2026 14:51:58 -0700 Subject: [PATCH 10/22] add preferred policy --- .azure-pipelines/ci-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index ea84b3b5af..86a8da26e1 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -102,7 +102,7 @@ extends: # "Permissive" base policy is intentionally omitted so other banned-endpoint calls still fail the # run, surfacing exactly which steps egress to public registries. # See https://aka.ms/1espt-networkisolation - networkIsolationPolicy: GitHub,CFSClean + networkIsolationPolicy: Preferred,GitHub,CFSClean sdl: sourceAnalysisPool: name: Azure-Pipelines-1ESPT-ExDShared From 93a917361fd4df8aceef25d2599bcd46458c8164 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 24 Jul 2026 15:37:26 -0700 Subject: [PATCH 11/22] disable version update script to trace other violations --- .azure-pipelines/ci-build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 86a8da26e1..368cf61f68 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -155,6 +155,8 @@ extends: -PyPiSimpleIndexUrl "$(privateFeedBaseUrl)/pypi/simple" -MavenRepositoryUrl "$(privateFeedBaseUrl)/maven/v1" displayName: "Update dependencies versions" + name: update_versions + condition: false env: FEED_ACCESS_TOKEN: $(System.AccessToken) From 2276ff70de8c401be15fe3d5de23171b3f3b361c Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 24 Jul 2026 16:41:15 -0700 Subject: [PATCH 12/22] disable network isolation policies --- .azure-pipelines/ci-build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 368cf61f68..9248219bbb 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -102,7 +102,8 @@ extends: # "Permissive" base policy is intentionally omitted so other banned-endpoint calls still fail the # run, surfacing exactly which steps egress to public registries. # See https://aka.ms/1espt-networkisolation - networkIsolationPolicy: Preferred,GitHub,CFSClean + # explicitly unset until we can get an approval for our required use-cases as these policies will break the build. + # networkIsolationPolicy: Preferred,GitHub,CFSClean sdl: sourceAnalysisPool: name: Azure-Pipelines-1ESPT-ExDShared @@ -156,7 +157,6 @@ extends: -MavenRepositoryUrl "$(privateFeedBaseUrl)/maven/v1" displayName: "Update dependencies versions" name: update_versions - condition: false env: FEED_ACCESS_TOKEN: $(System.AccessToken) From 6bc0f8b629ea2eb6e8489620145276332340827e Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 24 Jul 2026 16:52:12 -0700 Subject: [PATCH 13/22] ci: route ReportGenerator tool install through private feed dotnet tool install --global does not reliably pick up the repo nuget.config, so pass --configfile explicitly to force the authenticated central feed and avoid a blocked nuget.org call under network isolation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 9248219bbb..fe2d24ceeb 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -316,9 +316,10 @@ extends: projects: '$(Build.SourcesDirectory)\kiota.slnx' arguments: '--configuration $(BuildConfiguration) --no-build --collect:"XPlat Code Coverage"' - - pwsh: dotnet tool install --global dotnet-reportgenerator-globaltool + - pwsh: dotnet tool install --global dotnet-reportgenerator-globaltool --configfile "$(Build.SourcesDirectory)/nuget.config" condition: succeededOrFailed() displayName: "Install ReportGenerator" + name: install_reportgenerator - pwsh: reportgenerator -reports:$(Agent.TempDirectory)/**/coverage.cobertura.xml -targetdir:$(Build.SourcesDirectory)/reports/coverage -reporttypes:"HtmlInline_AzurePipelines;Cobertura" condition: succeeded() From 4ec34edd30c1ae7f5be345762d6d4b9aa432cfaa Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 24 Jul 2026 17:01:51 -0700 Subject: [PATCH 14/22] ci: bootstrap arm64 PowerShell install via private feed On win-arm64, PowerShell Core is not preinstalled and is acquired with dotnet tool install. Authenticate and write nuget.config before that step so the restore uses the central feed instead of nuget.org. The config is authored with cmd because pwsh is not yet available on this host (it is what is being bootstrapped), and the install now passes --configfile. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index fe2d24ceeb..339436fa68 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -495,7 +495,24 @@ extends: inputs: version: 9.x - - script: dotnet tool install --global PowerShell + # On arm64 (Windows) PowerShell Core is not preinstalled, so it is acquired via + # `dotnet tool install`. That restore must flow through the authenticated central feed, + # so both NuGetAuthenticate and a nuget.config have to exist *before* this step. The + # config is authored with cmd rather than pwsh because pwsh is not available yet on + # this host (that is exactly what we are bootstrapping). + - task: NuGetAuthenticate@1 + displayName: "Authenticate to Azure Artifacts (arm64 PowerShell bootstrap)" + condition: and(succeeded(), eq('${{ distribution.hostArchitecture }}', 'arm64')) + - script: | + set "CFG=$(Build.SourcesDirectory)\nuget.config" + > "%CFG%" echo ^ + >>"%CFG%" echo ^ + >>"%CFG%" echo ^ + >>"%CFG%" echo ^ + >>"%CFG%" echo ^ + >>"%CFG%" echo ^ + >>"%CFG%" echo ^ + dotnet tool install --global PowerShell --configfile "%CFG%" displayName: "Install PowerShell" condition: and(succeeded(), eq('${{ distribution.hostArchitecture }}', 'arm64')) # powershell is not installed by default on arm64 images From 3e50f980925024f53c6b2b0cbdad53ed0fbe59fb Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 24 Jul 2026 17:07:48 -0700 Subject: [PATCH 15/22] ci: single unconditional NuGetAuthenticate in binary job Make the earlier NuGetAuthenticate run for all distributions and drop the later duplicate, so authentication is established once before both the arm64 PowerShell bootstrap and the publish restore. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 339436fa68..30e3f36c3c 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -501,8 +501,7 @@ extends: # config is authored with cmd rather than pwsh because pwsh is not available yet on # this host (that is exactly what we are bootstrapping). - task: NuGetAuthenticate@1 - displayName: "Authenticate to Azure Artifacts (arm64 PowerShell bootstrap)" - condition: and(succeeded(), eq('${{ distribution.hostArchitecture }}', 'arm64')) + displayName: "Authenticate to Azure Artifacts" - script: | set "CFG=$(Build.SourcesDirectory)\nuget.config" > "%CFG%" echo ^ @@ -527,9 +526,6 @@ extends: - pwsh: $(Build.SourcesDirectory)/scripts/update-version-suffix-for-source-generator.ps1 -versionSuffix "$(versionSuffix)" displayName: "Set version suffix in csproj for generators" - - task: NuGetAuthenticate@1 - displayName: 'Authenticate to Azure Artifacts' - - pwsh: | @" From 31a6dd3e841ddff7d20dc4e6628e090ca5974d14 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Fri, 24 Jul 2026 17:27:49 -0700 Subject: [PATCH 16/22] ci: remove unused it/ fixtures after checkout in all jobs Nothing in this pipeline builds or runs the it/ integration-test fixtures, but their per-language manifests cause Component Governance to reach public registries under network isolation. Delete it/ with git rm right after each checkout so no later scan or build sees it. Uses a plain script/git invocation (no pwsh) so it also works on the win-arm64 host before PowerShell is bootstrapped. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 30e3f36c3c..438a8675dc 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -144,6 +144,12 @@ extends: clean: true submodules: true + # The it/ integration-test fixtures are not used by this pipeline and their per-language + # manifests trigger Component Governance to reach public registries under network isolation. + # Remove them right after checkout so no later scan/build sees them. + - script: git -C "$(Build.SourcesDirectory)" rm -r --quiet --ignore-unmatch -- it + displayName: "Remove unused integration-test fixtures (it/)" + # Route package-version lookups through the authenticated Azure Artifacts feed # (GraphDeveloperExperiences_Public) so CI does not call public registries directly. # NuGet, npm, PyPI and Maven Central are configured as upstream sources on that feed. @@ -230,6 +236,9 @@ extends: clean: true submodules: true + - script: git -C "$(Build.SourcesDirectory)" rm -r --quiet --ignore-unmatch -- it + displayName: "Remove unused integration-test fixtures (it/)" + - pwsh: | Copy-Item $(Build.ArtifactStagingDirectory)/AppSettings/appsettings.json $(Build.SourcesDirectory)/src/kiota/appsettings.json -Force -Verbose displayName: Copy the appsettings.json @@ -480,6 +489,8 @@ extends: - checkout: self clean: true submodules: true + - script: git -C "$(Build.SourcesDirectory)" rm -r --quiet --ignore-unmatch -- it + displayName: "Remove unused integration-test fixtures (it/)" - task: UseDotNet@2 displayName: "Use .NET 6" # needed for ESRP signing inputs: @@ -723,6 +734,8 @@ extends: - checkout: self clean: true submodules: true + - script: git -C "$(Build.SourcesDirectory)" rm -r --quiet --ignore-unmatch -- it + displayName: "Remove unused integration-test fixtures (it/)" - task: UseNode@1 inputs: version: "22.x" @@ -842,6 +855,8 @@ extends: - checkout: self clean: true submodules: true + - script: git -C "$(Build.SourcesDirectory)" rm -r --quiet --ignore-unmatch -- it + displayName: "Remove unused integration-test fixtures (it/)" - task: UseNode@1 inputs: version: "22.x" @@ -1158,6 +1173,9 @@ extends: steps: - checkout: self + - script: git -C "$(Build.SourcesDirectory)" rm -r --quiet --ignore-unmatch -- it + displayName: "Remove unused integration-test fixtures (it/)" + - task: AzureCLI@2 displayName: "Login to Azure Container Registry" inputs: From 12e0b89ea2b31a55c7fdfdedc8da3dba1ee5d900 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Sun, 26 Jul 2026 13:04:48 -0700 Subject: [PATCH 17/22] ci: drop redundant CG it/ exclusion and centralize privateFeedBaseUrl The it/ fixtures are now deleted right after checkout, so the Component Governance ignoreDirectories exclusion is no longer needed - remove it. Promote privateFeedBaseUrl to the pipeline-level variables block and reference it as $(privateFeedBaseUrl) everywhere (the binary-build nuget.config sources previously hard-coded the raw feed URL), so the feed URL is defined in exactly one place. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 438a8675dc..1f18add073 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -36,6 +36,11 @@ variables: IMAGE_NAME: "public/openapi/kiota" PREVIEW_BRANCH: "refs/heads/main" TAG: "$(Build.BuildId)" + # Base URL for the authenticated Azure Artifacts feed (GraphDeveloperExperiences_Public) used to route + # package operations (version lookups + NuGet restore) through configured upstream sources instead of + # public registries. Defined once here and referenced as $(privateFeedBaseUrl) throughout the pipeline. + # NOTE: confirm this against the feed's "Connect to Feed" dialog if the feed/org/project changes. + privateFeedBaseUrl: "https://microsoftgraph.pkgs.visualstudio.com/0985d294-5762-4bc2-a565-161ef349ca3e/_packaging/GraphDeveloperExperiences_Public" parameters: - name: previewBranch @@ -109,17 +114,6 @@ extends: name: Azure-Pipelines-1ESPT-ExDShared os: windows image: windows-latest - componentgovernance: - # The it/ directory holds per-language integration-test fixtures (Java/Maven, PHP/Composer, - # Dart/Pub, Python, Go, Ruby, TypeScript). Component Detection would otherwise resolve those - # manifests against public registries (e.g. the Maven detector runs `mvn dependency:tree`, - # reaching repo.maven.apache.org), which fails network isolation. These fixtures are not - # dependencies of the shipped kiota binary, so exclude them from CG scanning. - # ignoreDirectories maps to Component Detection's --IgnoreDirectories (comma-separated absolute - # paths, matched against the scan root — not globs). The scan root can be either - # $(Build.SourcesDirectory) or $(Build.Repository.LocalPath) depending on the injected job, so - # list the fixture directory under both roots to guarantee the exclusion matches. - ignoreDirectories: "$(Build.SourcesDirectory)/it,$(Build.Repository.LocalPath)/it" stages: - stage: build jobs: @@ -128,11 +122,6 @@ extends: name: Azure-Pipelines-1ESPT-ExDShared os: linux image: ubuntu-latest - variables: - # Base URL for the authenticated Azure Artifacts feed (GraphDeveloperExperiences_Public) used - # to route package-version lookups through configured upstream sources instead of public registries. - # NOTE: confirm these against the feed's "Connect to Feed" dialog if the feed/org/project changes. - privateFeedBaseUrl: "https://microsoftgraph.pkgs.visualstudio.com/0985d294-5762-4bc2-a565-161ef349ca3e/_packaging/GraphDeveloperExperiences_Public" templateContext: sdl: baseline: @@ -296,7 +285,7 @@ extends: - + "@ | Set-Content -Path "$(Build.SourcesDirectory)/nuget.config" -Encoding UTF8 @@ -519,7 +508,7 @@ extends: >>"%CFG%" echo ^ >>"%CFG%" echo ^ >>"%CFG%" echo ^ - >>"%CFG%" echo ^ + >>"%CFG%" echo ^ >>"%CFG%" echo ^ >>"%CFG%" echo ^ dotnet tool install --global PowerShell --configfile "%CFG%" @@ -543,7 +532,7 @@ extends: - + "@ | Set-Content -Path "$(Build.SourcesDirectory)/nuget.config" -Encoding UTF8 From ca8c979a17e577cc0593709420976c6e4693d2c7 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Sun, 26 Jul 2026 13:10:50 -0700 Subject: [PATCH 18/22] ci: route PushDockerImage update-versions through private feed The second update-versions.ps1 invocation (PushDockerImage job) was called with no feed arguments, so it resolved package versions against public registries. Pass the same $(privateFeedBaseUrl) NuGet/npm/PyPI/Maven URLs and FEED_ACCESS_TOKEN as the update_appsettings job so it uses the authenticated Azure Artifacts feed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- .azure-pipelines/ci-build.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 1f18add073..5a2b76b57d 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -1199,9 +1199,17 @@ extends: displayName: "Get Kiota version number" name: getversion - - pwsh: | + # Route package-version lookups through the authenticated Azure Artifacts feed + # (GraphDeveloperExperiences_Public) so CI does not call public registries directly. + - pwsh: > ./scripts/update-versions.ps1 + -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json" + -NpmRegistryUrl "$(privateFeedBaseUrl)/npm/registry" + -PyPiSimpleIndexUrl "$(privateFeedBaseUrl)/pypi/simple" + -MavenRepositoryUrl "$(privateFeedBaseUrl)/maven/v1" displayName: "Update dependencies versions" + env: + FEED_ACCESS_TOKEN: $(System.AccessToken) - script: | docker run --privileged --rm msgraphprodregistry.azurecr.io/tonistiigi/binfmt --install all From 54b0cf0200e0d6c7c96e2ff961ac24d2cd7faefb Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Sun, 26 Jul 2026 13:23:36 -0700 Subject: [PATCH 19/22] fix(scripts): only send feed OAuth token to Azure Artifacts hosts update-versions.ps1 built one Authorization header and attached it to every NuGet/npm/PyPI/Maven request, so when a feed URL was left at its public default (api.nuget.org, npmjs.org, pypi.org, Maven Central) the pipeline OAuth token was sent to a third-party registry. Decide per-feed instead: attach the Bearer token only when the target URL is a private Azure Artifacts feed (pkgs.dev.azure.com or *.pkgs.visualstudio.com) and a token is available; public registries get no auth header. The host check is exact/suffix-anchored so lookalike hosts do not match. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- scripts/update-versions.ps1 | 47 ++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/scripts/update-versions.ps1 b/scripts/update-versions.ps1 index 2dc99eebeb..e0519c9970 100644 --- a/scripts/update-versions.ps1 +++ b/scripts/update-versions.ps1 @@ -18,12 +18,41 @@ $NpmRegistryUrl = $NpmRegistryUrl.TrimEnd('/') $PyPiSimpleIndexUrl = $PyPiSimpleIndexUrl.TrimEnd('/') $MavenRepositoryUrl = $MavenRepositoryUrl.TrimEnd('/') -# Build the auth headers once. Azure Artifacts protocol endpoints accept the pipeline OAuth token as a Bearer token. -$script:FeedAuthHeaders = @{} -if (-not [string]::IsNullOrWhiteSpace($FeedAccessToken)) { - $script:FeedAuthHeaders["Authorization"] = "Bearer $FeedAccessToken" +# Azure Artifacts protocol endpoints accept the pipeline OAuth token as a Bearer token. That token must +# only ever be sent to a private Azure Artifacts feed - never to a public registry (nuget.org, npmjs.org, +# pypi.org, Maven Central, ...) where it could be leaked. Decide per-feed whether to attach it. +$script:HasFeedAccessToken = -not [string]::IsNullOrWhiteSpace($FeedAccessToken) + +# True when the URL points at an Azure Artifacts feed. Those are served from pkgs.dev.azure.com and the +# legacy *.pkgs.visualstudio.com hosts; anything else is treated as a public registry (no token sent). +function Test-IsAzureArtifactsUrl { + param([string]$Url) + if ([string]::IsNullOrWhiteSpace($Url)) { return $false } + try { + $feedHost = ([uri]$Url).Host + } + catch { + return $false + } + return $feedHost -eq "pkgs.dev.azure.com" -or $feedHost.EndsWith(".pkgs.visualstudio.com") } +# Build the auth headers for a feed URL: attach the Bearer token only when a token is available AND the +# URL is a private Azure Artifacts feed; otherwise return empty headers so nothing is sent to public hosts. +function Get-FeedAuthHeaders { + param([string]$Url) + if ($script:HasFeedAccessToken -and (Test-IsAzureArtifactsUrl -Url $Url)) { + return @{ "Authorization" = "Bearer $FeedAccessToken" } + } + return @{} +} + +# Resolve the headers once per feed type, since each feed's URL is fixed for the run. +$script:NuGetAuthHeaders = Get-FeedAuthHeaders -Url $NuGetServiceIndexUrl +$script:NpmAuthHeaders = Get-FeedAuthHeaders -Url $NpmRegistryUrl +$script:PyPiAuthHeaders = Get-FeedAuthHeaders -Url $PyPiSimpleIndexUrl +$script:MavenAuthHeaders = Get-FeedAuthHeaders -Url $MavenRepositoryUrl + # Cache for the resolved NuGet registrations base URL so the service index is only read once. $script:NuGetRegistrationsBaseUrl = $null @@ -34,7 +63,7 @@ function Get-NugetRegistrationsBaseUrl { if ($null -ne $script:NuGetRegistrationsBaseUrl) { return $script:NuGetRegistrationsBaseUrl } - $index = Invoke-RestMethod -Uri $NuGetServiceIndexUrl -Method Get -Headers $script:FeedAuthHeaders + $index = Invoke-RestMethod -Uri $NuGetServiceIndexUrl -Method Get -Headers $script:NuGetAuthHeaders $preferredTypes = @("RegistrationsBaseUrl/3.6.0", "RegistrationsBaseUrl/Versioned", "RegistrationsBaseUrl") foreach ($type in $preferredTypes) { $resource = $index.resources | Where-Object { $_.'@type' -eq $type } | Select-Object -First 1 @@ -54,7 +83,7 @@ function Get-LatestNugetVersion { $registrationsBaseUrl = Get-NugetRegistrationsBaseUrl $url = "$registrationsBaseUrl/$($packageId.ToLowerInvariant())/index.json" - $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:FeedAuthHeaders + $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:NuGetAuthHeaders $version = $response.items | Select-Object -ExpandProperty upper | ForEach-Object { [System.Management.Automation.SemanticVersion]$_ } | sort-object | Select-Object -Last 1 $version.ToString() } @@ -79,7 +108,7 @@ function Get-LatestNpmVersion { # @microsoft/kiota-abstractions) must have their '/' encoded as %2F for the packument request. $encodedId = $packageId.ToLowerInvariant().Replace('/', '%2F') $url = "$NpmRegistryUrl/$encodedId" - $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:FeedAuthHeaders + $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:NpmAuthHeaders $response.'dist-tags'.latest } # Get the latest version of a maven package @@ -88,7 +117,7 @@ function Get-LatestMavenVersion { [string]$packageId ) $url = "$MavenRepositoryUrl/$($packageId.Replace(":", "/").Replace(".", "/").Replace("|", "."))/maven-metadata.xml" - $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:FeedAuthHeaders + $response = Invoke-RestMethod -Uri $url -Method Get -Headers $script:MavenAuthHeaders $response.metadata.versioning.latest } # Get the latest version of a composer package @@ -109,7 +138,7 @@ function Get-LatestPypiVersion { # the legacy pypi.org "/pypi/{pkg}/json" API, which private Azure Artifacts feeds do not expose. $normalizedId = $packageId.ToLowerInvariant().Replace("_", "-").Replace(".", "-") $url = "$PyPiSimpleIndexUrl/$normalizedId/" - $headers = $script:FeedAuthHeaders.Clone() + $headers = $script:PyPiAuthHeaders.Clone() $headers["Accept"] = "application/vnd.pypi.simple.v1+json" $response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers # Prefer the highest final (non pre-release) version to match the previous behaviour of pypi's info.version. From b76f7d4c89da109325e8bfd624be4bf6a772b089 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Sun, 26 Jul 2026 13:31:34 -0700 Subject: [PATCH 20/22] fix(scripts): use PEP 503 name normalization for PyPI simple index The PyPI lookup normalized project names with per-character Replace() calls (_ -> -, . -> -), which does not collapse runs of separators the way PEP 503 requires (re.sub(r\"[-_.]+\", \"-\", name).lower()). Names like \"a..b\" became \"a--b\" and existing \"--\" runs were left intact, yielding a non-canonical name that the simple index returns 404 for. Replace with a single regex that collapses any run of -, _ or . into one - before lowercasing. Current kiota Python package names are unaffected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- scripts/update-versions.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/update-versions.ps1 b/scripts/update-versions.ps1 index e0519c9970..46b25835f5 100644 --- a/scripts/update-versions.ps1 +++ b/scripts/update-versions.ps1 @@ -136,7 +136,10 @@ function Get-LatestPypiVersion { ) # Use the PEP 503/691 simple index (supported by both public PyPI and Azure Artifacts feeds) instead of # the legacy pypi.org "/pypi/{pkg}/json" API, which private Azure Artifacts feeds do not expose. - $normalizedId = $packageId.ToLowerInvariant().Replace("_", "-").Replace(".", "-") + # Normalize the project name per PEP 503: collapse any run of '-', '_' or '.' into a single '-' and + # lowercase. The previous per-character Replace() did not collapse consecutive separators (e.g. "a..b" + # -> "a--b", "a--b" left as-is), which produces a non-canonical name the simple index 404s on. + $normalizedId = ($packageId -replace '[-_.]+', '-').ToLowerInvariant() $url = "$PyPiSimpleIndexUrl/$normalizedId/" $headers = $script:PyPiAuthHeaders.Clone() $headers["Accept"] = "application/vnd.pypi.simple.v1+json" From cf24e732f8f5ae5d4afe7ad8576f2678f8fe2073 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Sun, 26 Jul 2026 13:44:34 -0700 Subject: [PATCH 21/22] fix(scripts): make PyPI resolver work without a PEP 700 versions array The Python resolver read $response.versions, but the top-level versions array is a PEP 700 addition that PEP 691 does not guarantee; against a feed that implements PEP 691 only, versions is null and selection breaks. Recover the candidate version list from the distribution filenames in files[] when versions is absent. Also drop the [version] cast (limited to four segments and throws on longer ones) in favour of a zero-padded, cast-free numeric sort so selection is robust and deterministic. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- scripts/update-versions.ps1 | 39 ++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/scripts/update-versions.ps1 b/scripts/update-versions.ps1 index 46b25835f5..fc9567faeb 100644 --- a/scripts/update-versions.ps1 +++ b/scripts/update-versions.ps1 @@ -129,6 +129,27 @@ function Get-LatestComposerVersion { $response = Invoke-RestMethod -Uri $url -Method Get $response.packages.$packageId[0].version } +# Extract the version from a PyPI distribution filename. In both wheel ("{name}-{version}-...whl") and +# sdist ("{name}-{version}.tar.gz") filenames the normalized project name contains no '-' (PEP 427/625 +# replace separators with '_'), and versions never contain '-', so the version is always the second +# '-'-separated field once the archive extension is removed. +function Get-VersionFromPypiFilename { + param([string]$filename) + if ([string]::IsNullOrWhiteSpace($filename)) { return $null } + $stem = $filename -replace '\.(whl|egg|tar\.gz|tar\.bz2|tar\.xz|zip)$', '' + $parts = $stem.Split('-') + if ($parts.Count -ge 2) { return $parts[1] } + return $null +} + +# Build a sortable key for a purely-numeric (final) release version without using [version], which is +# limited to four segments and throws on longer ones. Each dotted segment is zero-padded so an ordinal +# string sort orders the versions numerically for any number of segments. +function Get-PypiVersionSortKey { + param([string]$version) + ($version.Split('.') | ForEach-Object { '{0:D10}' -f [int]$_ }) -join '.' +} + # Get the latest version of a pypi package function Get-LatestPypiVersion { param( @@ -144,13 +165,25 @@ function Get-LatestPypiVersion { $headers = $script:PyPiAuthHeaders.Clone() $headers["Accept"] = "application/vnd.pypi.simple.v1+json" $response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers + # The top-level "versions" array is a PEP 700 addition and is optional; PEP 691 only guarantees "files". + # Prefer "versions" when present, otherwise recover the version list from the distribution filenames so + # the resolver works against feeds that implement PEP 691 but not PEP 700. + $candidateVersions = $response.versions + if (-not $candidateVersions) { + $candidateVersions = $response.files | + ForEach-Object { Get-VersionFromPypiFilename -filename $_.filename } | + Where-Object { $_ } | + Select-Object -Unique + } # Prefer the highest final (non pre-release) version to match the previous behaviour of pypi's info.version. - $stableVersions = $response.versions | Where-Object { $_ -match '^[0-9]+(\.[0-9]+)*$' } + $stableVersions = $candidateVersions | Where-Object { $_ -match '^[0-9]+(\.[0-9]+)*$' } if ($stableVersions) { - ($stableVersions | ForEach-Object { [version]$_ } | Sort-Object | Select-Object -Last 1).ToString() + $stableVersions | Sort-Object -Property @{ Expression = { Get-PypiVersionSortKey -version $_ } } | Select-Object -Last 1 } else { - $response.versions | Select-Object -Last 1 + # No final release found (only pre/post/dev releases). Sort deterministically so the choice does not + # depend on the order the index happened to return, then take the highest. + $candidateVersions | Sort-Object | Select-Object -Last 1 } } # Get the latest version of a rubygem package From 8e38d5b0d688d344c7982a685c07bcc625732a11 Mon Sep 17 00:00:00 2001 From: "Gavin Barron (from Dev Box)" Date: Sun, 26 Jul 2026 13:53:11 -0700 Subject: [PATCH 22/22] Strip known PyPI project name when parsing version from distribution filename The filename fallback assumed the normalized project name contained no hyphen and took the second hyphen-separated field as the version. That is wrong for legacy sdists whose name keeps hyphens (e.g. microsoft-kiota-abstractions-1.10.1.tar.gz). The parser now strips the known project name (separator-tolerant) so the version is the field that follows it, falling back to the old heuristic only when the name is unknown. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bfe57333-036a-47d6-a9e4-9917ba6bc414 --- scripts/update-versions.ps1 | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/update-versions.ps1 b/scripts/update-versions.ps1 index fc9567faeb..16acd9e35c 100644 --- a/scripts/update-versions.ps1 +++ b/scripts/update-versions.ps1 @@ -129,14 +129,23 @@ function Get-LatestComposerVersion { $response = Invoke-RestMethod -Uri $url -Method Get $response.packages.$packageId[0].version } -# Extract the version from a PyPI distribution filename. In both wheel ("{name}-{version}-...whl") and -# sdist ("{name}-{version}.tar.gz") filenames the normalized project name contains no '-' (PEP 427/625 -# replace separators with '_'), and versions never contain '-', so the version is always the second -# '-'-separated field once the archive extension is removed. +# Extract the version from a PyPI distribution filename ("{name}-{version}-...whl" or "{name}-{version}.tar.gz"). +# The distribution name in the filename may separate its components with '-', '_' or '.' (wheels/PEP 625 +# sdists normalize to '_', but legacy sdists can keep '-'), so we cannot assume the name is '-'-free. When the +# expected project name is known we match it tolerant of any separator and take the field that immediately +# follows it as the version (versions never contain '-'). Only when the name is unknown do we fall back to the +# second '-'-separated field. function Get-VersionFromPypiFilename { - param([string]$filename) + param( + [string]$filename, + [string]$packageId + ) if ([string]::IsNullOrWhiteSpace($filename)) { return $null } $stem = $filename -replace '\.(whl|egg|tar\.gz|tar\.bz2|tar\.xz|zip)$', '' + if (-not [string]::IsNullOrWhiteSpace($packageId)) { + $namePattern = ($packageId -split '[-_.]+' | Where-Object { $_ } | ForEach-Object { [regex]::Escape($_) }) -join '[-_.]' + if ($stem -match "(?i)^$namePattern-([^-]+)") { return $matches[1] } + } $parts = $stem.Split('-') if ($parts.Count -ge 2) { return $parts[1] } return $null @@ -171,7 +180,7 @@ function Get-LatestPypiVersion { $candidateVersions = $response.versions if (-not $candidateVersions) { $candidateVersions = $response.files | - ForEach-Object { Get-VersionFromPypiFilename -filename $_.filename } | + ForEach-Object { Get-VersionFromPypiFilename -filename $_.filename -packageId $normalizedId } | Where-Object { $_ } | Select-Object -Unique }