Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8ddd2b7
fix(wrapper-generator): map numeric parameter types by OpenAPI format
Joywambui-maina Aug 4, 2026
ca215f3
fix(wrapper-generator): correct four singularization words found by o…
Joywambui-maina Aug 3, 2026
c4d8545
fix(wrapper-generator): correct HostWhoi name, add Statistics invaria…
Joywambui-maina Aug 4, 2026
695cfe7
feat(wrapper-generator): add module packaging and smoke-test scripts
Joywambui-maina Aug 6, 2026
f2de362
fix(wrapper-generator): align emitted code with real kiota client output
Joywambui-maina Aug 6, 2026
26948e8
fix(wrapper-generator): correct PlaceCheckIn names, add Rights invari…
Joywambui-maina Aug 6, 2026
668a28d
fix(wrapper-generator): fail loudly on cmdlet file collisions
Joywambui-maina Aug 11, 2026
e6c98e4
feat(wrapper-generator): resolve cmdlet collisions via oracle-derived…
Joywambui-maina Aug 12, 2026
5608540
Potential fix for pull request finding
Joywambui-maina Aug 14, 2026
b7bf79a
Potential fix for pull request finding
Joywambui-maina Aug 14, 2026
2ed1ab3
Merge pull request #3713 from microsoftgraph/fix/cmdlet-file-collisions
Joywambui-maina Aug 14, 2026
e287a91
feat(wrapper-generator): complete v1.0 request-body binding
Joywambui-maina Aug 14, 2026
1b6f282
Merge powershell-v3 into feature/wrapper-module-packaging
Joywambui-maina Aug 14, 2026
3f99034
Merge feature/wrapper-module-packaging into feat/bind-request-body-pr…
Joywambui-maina Aug 14, 2026
dec945a
fix(wrapper-generator): align wrapper packaging projects
Joywambui-maina Aug 14, 2026
b1c7cb0
Merge branch 'feature/wrapper-module-packaging' into feat/bind-reques…
Joywambui-maina Aug 14, 2026
6addca0
feat(wrapper-generator): generate remaining OData operation shapes
Joywambui-maina Aug 18, 2026
db9f611
Merge remote-tracking branch 'origin/feat/bind-request-body-propertie…
Joywambui-maina Aug 18, 2026
c450ffc
feat(wrapper-generator): commit v1.0 wrapper sources under src/{Modul…
Joywambui-maina Aug 20, 2026
afebe5f
fix(wrapper-generator): correct wrapper package identity and Authenti…
Joywambui-maina Aug 21, 2026
7877dab
feat(wrapper-runtime): add shared runtime library with client-lifetim…
Joywambui-maina Aug 21, 2026
1689c38
feat(wrapper-generator): emit cmdlets on the shared runtime base class
Joywambui-maina Aug 21, 2026
faeef51
fix(wrapper-generator): resolve shared dependencies from the installe…
Joywambui-maina Aug 22, 2026
71478f7
fix(wrapper-generator): make wrapper packages installable from a feed
Joywambui-maina Aug 24, 2026
d17f0d9
feat(wrapper-generator): emit -All and nextLink following for list cm…
Joywambui-maina Aug 25, 2026
8ecfb88
feat(wrapper-runtime): cancel an in-flight request on Ctrl+C
Joywambui-maina Aug 26, 2026
40f7b22
test(wrapper-generator): pin emitter asserts to the cancellation-awar…
Joywambui-maina Aug 27, 2026
7037b20
Merge powershell-v3 into feat/wrapper-pagination-generator
Joywambui-maina Aug 27, 2026
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
27 changes: 26 additions & 1 deletion src/GraphWrapperRuntime/Runtime/GraphClientCmdlet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Microsoft.Graph.Wrapper.Runtime
// The shared skeleton of every generated wrapper cmdlet: the -AccessToken/-Headers surface,
// transport acquisition, and Graph error translation. Derived cmdlets own only what is
// unique to their operation - path parameters, body binding, and the request itself.
public abstract class GraphClientCmdlet : PSCmdlet
public abstract class GraphClientCmdlet : PSCmdlet, IDisposable
{
// One shared HttpClient for every -AccessToken invocation in the process. The token
// rides per-request (see StaticBearerTokenAuthenticationProvider), so different tokens
Expand Down Expand Up @@ -79,6 +79,31 @@ protected void AddRequestHeaders(RequestHeaders requestHeaders)
}
}

private readonly System.Threading.CancellationTokenSource _stopping = new System.Threading.CancellationTokenSource();

// Ctrl+C while a single request is in flight. Checking Stopping between requests cannot
// interrupt one that is already running, so the token is handed to the kiota call instead.
//
// Cmdlet.PipelineStopToken would be the direct route and IS declared by
// PowerShellStandard.Library, but it does not exist in the Windows PowerShell 5.1 runtime
// these netstandard2.0 modules also target - it would compile, pass on PowerShell 7, and
// throw on 5.1. StopProcessing is virtual on every supported edition, so the token is
// raised from there.
protected System.Threading.CancellationToken StoppingToken => _stopping.Token;

protected override void StopProcessing()
{
_stopping.Cancel();
base.StopProcessing();
}

// PowerShell disposes a cmdlet that implements IDisposable once the pipeline ends.
public void Dispose()
{
_stopping.Dispose();
GC.SuppressFinalize(this);
}

// The single error surface for a failed Graph call, identical across every cmdlet.
protected void ThrowGraphRequestFailed(Exception exception, object? targetObject)
{
Expand Down
105 changes: 19 additions & 86 deletions tools/Build-WrapperModule.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,6 @@ dotnet build configuration. Default: Debug.
.PARAMETER SkipKiota
Reuse the previously generated client (fast inner loop when only the wrappers changed).

.PARAMETER ModuleVersion
Three-part version for the wrapper packages. Default 3.0.0 - the wrapper modules are the v3
line, not a build of the v2 service-module release train whose version lives in
config/ModuleMetadata.json.

.PARAMETER Prerelease
Prerelease label carried by every package (alphanumeric, never empty). Defaults to
alpha<BuildId> in CI and alpha<UTC timestamp> locally, so no two builds ever publish the same
id and version with different contents. Wrapper packages stay prereleases until the quality
bar for public distribution is agreed.

.PARAMETER Pack
Also produce a package per module under <ArtifactsLocation>/<Module>/.

Expand All @@ -90,23 +79,6 @@ param(
[string]$Configuration = 'Debug',
[string]$ModuleMappingConfigPath,
[string]$ArtifactsLocation,
# The wrapper modules are the v3 line, not a build of the v2 service-module release train,
# so their version is their own rather than ModuleMetadata.json's (which belongs to v2 and
# is still read here for authors, tags and the rest of the package identity).
[ValidatePattern('^\d+\.\d+\.\d+$')]
[string]$ModuleVersion = '3.0.0',
# Wrapper packages always carry a prerelease label, and every build gets a DISTINCT one:
# two packages that share an id and version but not their contents are indistinguishable to
# a feed and to anyone who already installed one. The build id supplies that distinctness in
# CI; a UTC timestamp does locally, where no build id exists.
# The pattern is DELIBERATELY narrower than the PowerShellGet prerelease grammar rather than a
# restatement of it: PowerShellGet also accepts a hyphen, which this rejects. A label is joined
# to the version by a hyphen already, so one that itself begins with a hyphen reads as
# '3.0.0--label', and one containing a hyphen splits the label when a version string is parsed
# by eye. Alphanumeric-only keeps every generated label unambiguous, and both defaults above
# ('alpha' plus a build id or a UTC timestamp) satisfy it by construction.
[ValidatePattern('^[A-Za-z0-9]+$')]
[string]$Prerelease = "alpha$(if ($env:BUILD_BUILDID) { $env:BUILD_BUILDID } else { (Get-Date).ToUniversalTime().ToString('yyyyMMddHHmm') })",
[switch]$SkipKiota,
[switch]$Pack
)
Expand All @@ -119,7 +91,7 @@ $generatorProject = Join-Path $repoRoot 'tools\WrapperGenerator'
$authCsproj = Join-Path $repoRoot 'src\Authentication\Authentication\Microsoft.Graph.Authentication.csproj'
$runtimeCsproj = Join-Path $repoRoot 'src\GraphWrapperRuntime\Runtime\Microsoft.Graph.Wrapper.Runtime.csproj'
# The Authentication version the wrappers compile against, for the manifest's RequiredModules
# minimum and the nuspec dependency floor. Read from the project, never written here.
# minimum. Read from the project, never written here.
$authVersion = ([xml](Get-Content $authCsproj -Raw)).Project.PropertyGroup.Version |
Where-Object { $_ } | Select-Object -First 1
if (-not $authVersion) { throw "no Version in $authCsproj" }
Expand All @@ -138,17 +110,16 @@ $targetFramework = "$targetFramework".Trim()
if (-not $ModuleMappingConfigPath) { $ModuleMappingConfigPath = Join-Path $repoRoot 'config\ModulesMapping.jsonc' }
if (-not $ArtifactsLocation) { $ArtifactsLocation = Join-Path $repoRoot 'artifacts' }

# Package identity (authors, owners, licence, tags) comes from the same single source the
# AutoRest service modules use, so a wrapper package and the module it will eventually replace
# cannot disagree about ownership. The VERSION is deliberately not taken from there: that
# entry tracks the v2 release train, and a wrapper package stamped with it would claim a
# version the real SDK is about to publish.
# Package metadata comes from the same single source the AutoRest service modules use, so a
# wrapper package and the module it will eventually replace cannot disagree about version or
# ownership.
$moduleMetadataPath = Join-Path $repoRoot 'config\ModuleMetadata.json'
[hashtable]$moduleMetadata = Get-Content $moduleMetadataPath -Raw | ConvertFrom-Json -AsHashTable
# The metadata prerelease field belongs to the service-module release train and is deliberately
# not read; the wrapper label (validated non-empty) is appended unconditionally, because a
# wrapper package without one cannot exist.
$fullVersion = "$ModuleVersion-$Prerelease"
$versionEntry = $moduleMetadata.versions[$ApiVersion]
if (-not $versionEntry -or -not $versionEntry.version) { throw "No version configured for '$ApiVersion' in $moduleMetadataPath." }
$moduleVersion = $versionEntry.version
$modulePrerelease = $versionEntry.prerelease
$fullVersion = if ($modulePrerelease) { "$moduleVersion-$modulePrerelease" } else { $moduleVersion }

# The population is the specs this generator can actually read, intersected with the modules the
# repository is configured to ship - the same two inputs tools/GenerateServiceModule.ps1 uses.
Expand Down Expand Up @@ -191,32 +162,6 @@ function New-ProjectFromTemplate {
Set-Content -Path $DestinationPath -Value $content -Encoding utf8
}

# The manifest GUID is a module's identity for ModuleSpecification matching (RequiredModules,
# Import-Module -FullyQualifiedName), so it must be the SAME across builds. The shipped SDK
# locks GUIDs to the published gallery entry (tools/BuildModule.ps1, autorest.powershell#981);
# a never-published wrapper has no gallery entry to lock to, so its identity is DERIVED
# instead: an RFC 4122 name-based (v5) UUID - SHA-1 over a fixed namespace GUID plus the module
# name - identical on every build with no lookup table to maintain. The namespace constant and
# the algorithm ARE the identity contract: changing either orphans every previously installed
# wrapper module.
function Get-WrapperModuleGuid {
param([Parameter(Mandatory)][string]$ModuleName)

$namespaceBytes = ([guid]'8a11e1b5-95b3-4dbf-b0ba-6d58b0f6f6a4').ToByteArray()
# Guid.ToByteArray() emits the first three fields little-endian; RFC 4122 hashes network order.
[Array]::Reverse($namespaceBytes, 0, 4); [Array]::Reverse($namespaceBytes, 4, 2); [Array]::Reverse($namespaceBytes, 6, 2)
$sha1 = [System.Security.Cryptography.SHA1]::Create()
try {
$hash = $sha1.ComputeHash([byte[]]($namespaceBytes + [System.Text.Encoding]::UTF8.GetBytes($ModuleName)))
}
finally { $sha1.Dispose() }
$guidBytes = $hash[0..15]
$guidBytes[6] = ($guidBytes[6] -band 0x0F) -bor 0x50 # version 5
$guidBytes[8] = ($guidBytes[8] -band 0x3F) -bor 0x80 # RFC 4122 variant
[Array]::Reverse($guidBytes, 0, 4); [Array]::Reverse($guidBytes, 4, 2); [Array]::Reverse($guidBytes, 6, 2)
[guid][byte[]]$guidBytes
}

function Get-CompiledCmdletNames {
param([Parameter(Mandatory)][string]$AssemblyPath)

Expand Down Expand Up @@ -395,9 +340,7 @@ function Build-Module {
$manifestArgs = @{
Path = $psd1Path
RootModule = "$moduleName.dll"
Guid = Get-WrapperModuleGuid -ModuleName $moduleName
ModuleVersion = $ModuleVersion
Prerelease = $Prerelease
ModuleVersion = $moduleVersion
RequiredModules = @(@{ ModuleName = 'Microsoft.Graph.Authentication'; ModuleVersion = $authVersion })
Author = 'Microsoft Graph'
CompanyName = 'Microsoft'
Expand All @@ -407,15 +350,14 @@ function Build-Module {
AliasesToExport = @()
VariablesToExport = @()
}
# Std.UriTemplate is requested by Microsoft.Kiota.Abstractions (measured: the AssemblyRef
# lives there, not in the HTTP library). That requester sits in Authentication's isolated
# load context, so its directory probing can never reach this module folder - and
# Authentication cannot serve the request either, because its Dependencies folder does
# not ship the assembly. Preloading it here puts it in the default context, where the
# isolated context's fallback resolution unifies with it. The Multipart serializer needs
# no entry: its requester is the client assembly in this folder, so normal
# module-directory probing finds it. Proven live by tools/Test-WrapperLive.ps1 on the
# session path.
if ($modulePrerelease) { $manifestArgs.Prerelease = $modulePrerelease }
# Std.UriTemplate is requested by the kiota HTTP library, which lives in Authentication's
# isolated load context - a requester whose directory probing can never reach this module
# folder, and whose request Authentication cannot serve because its Dependencies folder
# does not ship the assembly. Preloading it here puts it in the default context, where
# the isolated context's fallback resolution unifies with it. The Multipart serializer
# needs no entry: its requester is the client assembly in this folder, so normal
# module-directory probing finds it. Proven live in Test-LiveSmoke on the session path.
$manifestArgs.RequiredAssemblies = @('Std.UriTemplate.dll')
New-ModuleManifest @manifestArgs

Expand All @@ -426,12 +368,6 @@ function Build-Module {
# and the shared Authentication/kiota closure via the PruneModuleBin target - both at
# the project level, the only place that intent can be expressed. See
# tools/Templates/WrapperModule.csproj.template for why the closure must not ship.
# The nuspec must declare the Authentication dependency even though the psd1 already
# does: Install-Module resolves dependencies from NUGET metadata, not the manifest,
# so without this element a clean machine gets the wrapper with no Authentication and
# import fails. Declared as an open floor, not the shipped SDK's exact bracket pin -
# matching the manifest's RequiredModules minimum and the use-latest ruling (Ramses,
# 2026-08-19: pins existed only for AutoRest limitations).
$nuspecPath = Join-Path $binDir "$moduleName.nuspec"
$tags = ($moduleMetadata['tags']) -join ' '
# Native runtime payloads only exist when a dependency ships them; dotnet pack fails
Expand All @@ -456,9 +392,6 @@ function Build-Module {
<releaseNotes>$($moduleMetadata['releaseNotes'])</releaseNotes>
<copyright>$($moduleMetadata['copyright'])</copyright>
<tags>$tags</tags>
<dependencies>
<dependency id="Microsoft.Graph.Authentication" version="$authVersion" />
</dependencies>
</metadata>
<files>
<file src="$moduleName.psd1" />
Expand All @@ -475,7 +408,7 @@ function Build-Module {
# a PowerShell module package. NuspecBasePath resolves the globs against the build
# output so the nuspec need not know how deep bin/<Config>/<TFM> is.
$packArgs = @($csprojPath, '-c', $Configuration, '--no-build', '--nologo', '-v', 'minimal',
"-p:NuspecFile=$nuspecPath", "-p:NuspecBasePath=$binDir", "-p:Version=$ModuleVersion",
"-p:NuspecFile=$nuspecPath", "-p:NuspecBasePath=$binDir", "-p:Version=$moduleVersion",
'-p:NoPackageAnalysis=true', '-o', $moduleArtifacts)
$packOut = & dotnet pack @packArgs 2>&1
if ($LASTEXITCODE -ne 0) {
Expand Down
Loading
Loading