Repository navigation
Description
Prerequisites
- Write a descriptive title.
- Make sure you are able to repro it on the latest released version
- Search the existing issues.
Steps to reproduce
When trying to install a package from an OCI registry that contains a prefix, the package prefix is removed when it goes to download the nupkg blob.
Due to #1946 I've had to add a hack that fixes up the authentication to GHCR. This uses PSNetDetour to patch the token logic. If/when that is ever fixed then this patch isn't needed to replicate this error.
$repoParams = @{
Name = 'GHCR'
Uri = 'https://ghcr.io/'
ApiVersion = 'ContainerRegistry'
}
Register-PSResourceRepository @repoParams
$psGetCmd = Get-Command -Name Install-PSResource -Module Microsoft.PowerShell.PSResourceGet
$psGetType = $psGetCmd.ImplementingType.Assembly.GetType(
'Microsoft.PowerShell.PSResourceGet.ContainerRegistryServerAPICalls')
$getRegistryTokenMeth = $psGetType.GetMethod(
'GetContainerRegistryAccessToken',
[Reflection.BindingFlags]'Instance, NonPublic',
[type[]]@([bool], [bool], [System.Management.Automation.ErrorRecord].MakeByRefType()))
$sendRequestMeth = $psGetType.GetMethod(
'GetHttpResponseJObjectUsingDefaultHeaders',
[Reflection.BindingFlags]'Instance, NonPublic',
[type[]]@(
[string],
[Net.Http.HttpMethod],
[Collections.ObjectModel.Collection[System.Collections.Generic.KeyValuePair[string, string]]],
[System.Management.Automation.ErrorRecord].MakeByRefType(),
[bool]))
Use-NetDetourContext {
# Fixes logic for getting anonymous token for GHCR
New-NetDetourHook -Method $getRegistryTokenMeth -Hook {
param ([bool]$NeedCatalogAccess, [bool]$IsPushOperation, [ref]$ErrorRecord)
try {
$registryUri = "https://ghcr.io/v2/"
$resp = Invoke-WebRequest -Uri $registryUri -SkipHttpErrorCheck
$bearer = $resp.Headers['WWW-Authenticate'] | Select-Object -First 1
if (-not $bearer) {
throw "No WWW-Authenticate found in response headers for '$registryUri'"
}
if ($bearer -match 'realm="([^"]+)"') {
$realm = $matches[1]
} else {
throw "Could not extract realm from WWW-Authenticate header '$bearer'"
}
if ($bearer -match 'service="([^"]+)"') {
$service = $matches[1]
} else {
throw "Could not extract service from WWW-Authenticate header '$bearer'"
}
$tokenUri = "${realm}?service=${service}&client_id=testclient&scope=repository:jborean93/publishtest:pull"
$tokenResponse = Invoke-WebRequest -Uri $tokenUri -Method Get
($tokenResponse.Content | ConvertFrom-Json).token
}
catch {
$_.ErrorDetails = "Failed to retrieve anonymous token: $_"
$ErrorRecord.Value = $_
}
}
# Prints the URIs requested to show what the problem is
New-NetDetourHook -Method $sendRequestMeth -Hook {
param($Url, $Method, $DefaultHeaders, $ErrRecord, $UsePagination)
[Console]::WriteLine("$Method -> $Url")
, $Detour.Invoke($Url, $Method, $DefaultHeaders, $ErrRecord, $UsePagination)
}
Install-PSResource -Name jborean93/publishtest -Repository GHCR -TrustRepository -Verbose -Debug
}Expected behavior
Package is installedActual behavior
DEBUG: Parameters passed in >>> Name: 'jborean93/publishtest'; VersionRange: ''; NuGetVersion: ''; VersionType: 'NoVersion'; Version: ''; Prerelease: 'False'; Repository: 'GHCR'; AcceptLicense: 'False'; Quiet: 'False'; Reinstall: 'False'; TrustRepository: 'True'; NoClobber: 'False'; AsNupkg: 'False'; IncludeXml 'True'; SavePackage 'False'; TemporaryPath ''; SkipDependencyCheck: 'False'; AuthenticodeCheck: 'False'; PathsToInstallPkg: '/home/jborean/.local/share/powershell/Modules,/home/jborean/.local/share/powershell/Scripts'; Scope 'CurrentUser'
DEBUG: In InstallHelper::ProcessRepositories()
VERBOSE: Setting Secret Management network credentials
VERBOSE: Attempting to search for packages in 'GHCR'
DEBUG: In InstallHelper::InstallPackages()
DEBUG: In InstallHelper::InstallPackage()
DEBUG: In ContainerRegistryServerAPICalls::FindName()
DEBUG: In ContainerRegistryServerAPICalls::FindPackagesWithVersionHelper()
DEBUG: In ContainerRegistryServerAPICalls::FindContainerRegistryImageTags()
GET -> https://ghcr.io/v2/jborean93/publishtest/tags/list
DEBUG: In ContainerRegistryServerAPICalls::GetHttpResponseJObjectUsingDefaultHeaders()
DEBUG: In ContainerRegistryServerAPICalls::GetPackagesWithRequiredVersion()
DEBUG: 'jborean93/publishtest' version parsed as '0.1.0'
DEBUG: 'jborean93/publishtest' version parsed as '0.1.1'
DEBUG: 'jborean93/publishtest' version parsed as '0.1.2'
DEBUG: In ContainerRegistryServerAPICalls::GetContainerRegistryMetadata()
DEBUG: In ContainerRegistryServerAPICalls::FindContainerRegistryManifest()
DEBUG: GET manifest url: https://ghcr.io/v2/jborean93/publishtest/manifests/0.1.2
GET -> https://ghcr.io/v2/jborean93/publishtest/manifests/0.1.2
DEBUG: In ContainerRegistryServerAPICalls::GetHttpResponseJObjectUsingDefaultHeaders()
DEBUG: In ContainerRegistryServerAPICalls::GetMetadataProperty()
DEBUG: 'jborean93/publishtest' version parsed as '0.1.2'
Confirm
Are you sure you want to perform this action?
Performing the operation "Install-PSResource" on target "Package to install: 'PublishTest', version: '0.1.2'".
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): y
DEBUG: In ContainerRegistryServerAPICalls::InstallPackage()
DEBUG: In ContainerRegistryServerAPICalls::InstallVersion()
VERBOSE: Getting manifest for publishtest - 0.1.2
DEBUG: In ContainerRegistryServerAPICalls::GetContainerRegistryRepositoryManifest()
GET -> https://ghcr.io/v2/publishtest/manifests/0.1.2
DEBUG: In ContainerRegistryServerAPICalls::GetHttpResponseJObjectUsingDefaultHeaders()
Use-NetDetourContext: Response returned error with status code BadRequest: Bad Request.
VERBOSE: Attempting to delete '/tmp/43de5d14-17ef-4a1a-9137-b0b3fa84dd3d'
VERBOSE: Successfully deleted '/tmp/43de5d14-17ef-4a1a-9137-b0b3fa84dd3d'
Use-NetDetourContext: Package(s) 'jborean93/publishtest' could not be installed from repository 'GHCR'.We can see that
- It is able to find the tags/versions for this package
- It selects the latest version
0.1.2GET -> https://ghcr.io/v2/jborean93/publishtest/tags/list
- It is able to retrieve the OCI manifest through
GET -> https://ghcr.io/v2/jborean93/publishtest/manifests/0.1.2
- It derives the module name through that manifest as
PublishTestbased on the confirmation message - The step to gettings the manifest after the confirmation fails with a HTTP 400
GET -> https://ghcr.io/v2/publishtest/manifests/0.1.2
This step is failing because the URL used no longer contains the package prefix (jborean93) and thus isn't a valid request. The code when calling InstallPackage after it has found the version now uses the module name contained in the manifest which is just PublishTest rather than the original package name provided with the prefix.
The solution to this is to not update the package name from the manifest details and just use the module name on the downloaded artifact locally.
Error details
Exception :
Type : Microsoft.PowerShell.PSResourceGet.UtilClasses.ResourceNotFoundException
Message : Package(s) 'jborean93/publishtest' could not be installed from repository 'GHCR'.
HResult : -2146233088
TargetObject : Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource
CategoryInfo : InvalidData: (Microsoft.PowerShel…s.InstallPSResource:InstallPSResource) [Use-NetDetourContext],
ResourceNotFoundException
FullyQualifiedErrorId : InstallPackageFailure,PSNetDetour.Commands.UseNetDetourContext
InvocationInfo :
MyCommand : Use-NetDetourContext
ScriptLineNumber : 1
OffsetInLine : 1
HistoryId : 7
Line : Use-NetDetourContext {
Statement : Use-NetDetourContext {
# Fixes logic for getting anonymous token for GHCR
New-NetDetourHook -Method $getRegistryTokenMeth -Hook {
param ([bool]$NeedCatalogAccess, [bool]$IsPushOperation, [ref]$ErrorRecord)
try {
$registryUri = "https://ghcr.io/v2/"
$resp = Invoke-WebRequest -Uri $registryUri -SkipHttpErrorCheck
$bearer = $resp.Headers['WWW-Authenticate'] | Select-Object -First 1
if (-not $bearer) {
throw "No WWW-Authenticate found in response headers for '$registryUri'"
}
if ($bearer -match 'realm="([^"]+)"') {
$realm = $matches[1]
} else {
throw "Could not extract realm from WWW-Authenticate header '$bearer'"
}
if ($bearer -match 'service="([^"]+)"') {
$service = $matches[1]
} else {
throw "Could not extract service from WWW-Authenticate header '$bearer'"
}
$tokenUri = "${realm}?service=${service}&client_id=testclient&scope=repository:jborean93/publishtest:pull"
$tokenResponse = Invoke-WebRequest -Uri $tokenUri -Method Get
($tokenResponse.Content | ConvertFrom-Json).token
}
catch {
$_.ErrorDetails = "Failed to retrieve anonymous token: $_"
$ErrorRecord.Value = $_
}
} -State @{ Name = $Name }
Install-PSResource -Name jborean93/publishtest -Repository GHCR -TrustRepository -Verbose -Debug
}
PositionMessage : At line:1 char:1
+ Use-NetDetourContext {
+ ~~~~~~~~~~~~~~~~~~~~~~
InvocationName : Use-NetDetourContext
CommandOrigin : Internal
ScriptStackTrace : at <ScriptBlock>, <No file>: line 36
at <ScriptBlock>, <No file>: line 1
at <ScriptBlock>, <No file>: line 1
PipelineIterationInfo :
0
1Environment data
ModuleType Version PreRelease Name ExportedCommands
---------- ------- ---------- ---- ----------------
Binary 1.2.0 rc3 Microsoft.PowerShell.PSResourceGet {Compress-PSResource, Find-PSResource, Get-InstalledPSResource, G…
Name Value
---- -----
PSVersion 7.5.1
PSEdition Core
GitCommitId 7.5.1
OS Fedora Linux 43 (Server Edition)
Platform Unix
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0Visuals
No response
Activity
jborean93 commentedon Feb 20, 2026
This and the authentication issues are the only 2 blockers that stop PSResourceGet from installing packages from other OCI registries like GHCR. I've been tracking the progress and other general information around this at https://github.com/jborean93/PowerShell-PublishTest?tab=readme-ov-file#github-container-registry-oci.
jborean93 commentedon Feb 22, 2026
I've tried to write up more around the problem and the potential solutions for this issue. I'm happy to do the work it would just need a consensus on the way forward before I do so.
OCI Registry Package Prefixes: Current Issues and Proposed Solutions
Summary
PSResourceGet cannot properly install packages with a prefix before the module name. This prefix may be required by the OCI registry (like GHCR) or just published with that prefix. This document outlines:
ghcr.io/user/packagenamebut then tries to download fromghcr.io/packagenamepsresource/prefix, but this doesn't scale to other registriesInstall-PSResource -Name ghuser/package(simplest UX wise)-Prefixparameter toRegister-PSResourceRepository(requires multiple repos per namespace)-Prefixto other cmdlets likeInstall-PSResourcerather than a per repository configurationGlossary
OCI Registry Namespacing Concepts
Root Namespace - Packages directly under the registry (e.g.,
myacr.azurecr.io/packagename)Namespace/Prefix - Organizational grouping before the package name
ghcr.io/username/packagename,docker.io/organization/imagenameRegistry Path - The full path used in OCI API calls
{namespace}/{packagename}or just{packagename}for root namespaceghcr.io/ghuser/mymodule, the repository path isghuser/mymoduleModule Name - The PowerShell module name from the manifest (.psd1)
org.opencontainers.image.titleMyModule(notghuser/MyModule)Key Registries and Their Namespace Requirements
psresource/by design)mcr.microsoft.com/psresource/packagenamemyregistry.azurecr.io/packagenameormyregistry.azurecr.io/namespace/packagenameghcr.io/username/packagenameCurrent Behavior
What Works Today
Microsoft Artifact Registry (MAR):
This works because:
psresource/prefix for PowerShell resourcesPrependMARPrefix()in ContainerRegistryServerAPICalls.csAzure Container Registry (ACR) with no prefix:
What Breaks
GitHub Container Registry (GHCR):
Azure Container Registry (ACR) published under explicit prefix:
Same problem occurs where it can find the package tags and manifest but then strips off the package during the install stage.
Root Cause Analysis
The OCI Manifest Problem
When PSResourceGet retrieves the OCI manifest, it extracts the module name from the
org.opencontainers.image.titleannotation:{ "schemaVersion": 2, "layers": [{ "digest": "sha256:abc123...", "annotations": { "org.opencontainers.image.title": "MyModule", "resourceType": "Nupkg" } }] }The Download Problem
For subsequent operations (downloading blobs, pulling dependencies), PSResourceGet uses the module name from the manifest. But OCI API calls need the full repository path:
The Dependency Problem
Dependencies listed in the module manifest don't include namespace prefixes:
When installing
ghuser/MyModule, PSResourceGet needs to findghuser/DependencyModule, but the dependency is just listed asDependencyModule.Proposed Solutions
Solution 1: Parse Prefix from Package Name
Concept: When a user provides a package name with a
/, treat the prefix portion as a namespace to use for all operations.Implementation:
Code changes:
Pros:
Cons:
psresource/prefixExample usage:
Solution 2: Repository-Level Prefix Configuration
Concept: Add a
-Prefix/-ResourcePrefix/-ModulePrefixparameter toRegister-PSResourceRepositorythat gets prepended to all package operations.Implementation:
Code changes:
Prefixproperty toPSRepositoryInfoRegister-PSResourceRepositoryto accept-Prefixparameterpsresourceprefix added as part of the repository registration-ModulePrefixinPublish-PSResourceas the prefix can be derived from the repo registration insteadPros:
ContainerRegistryServerAPICalls.csand instead makes it a repository configurationCons:
Find-PSResourceuses the/v2/_catalogAPI which does not support server side filtering, it would have to filter it in the cmdletPublish-PSResource -ModulePrefixand repository prefix configurationExample usage:
Solution 3: Add -Prefix Parameter for Install-PSResource
Concept: Keep
-Nameas the module name but add-Prefixparameter to specify a prefix onInstall-PSResourceImplementation:
Code changes:
-Prefixparameter toInstall-PSResourceprefix/packagenamefor any API requestsPros:
-ModulePrefixexposed inPublish-PSResourceCons:
MARto be auto prefixedDependency Resolution Strategy
None of these solutions support finding dependencies in other repositories. I don't think this should be solved as dependencies for a package should all be contained in the same registry/namespace.
Migration Path for MAR
Current hardcoded MAR behavior should be preserved for backwards compatibility but instead of the prefix work being done in
ContainerRegistryServerAPICalls.csthe prefix could be applied using the same mechanisms proposed in the various solutions just in an automatic way. There would be no changes for the end user here just how it is dealt with internally.Comparison Matrix
Parse Name
Repo Prefix
Prefix Parameter
Recommended Solution
I would recommend either option 1 so that the cmdlets favour less UX complexity with a slight tradeoff in code complexity. Option 3 as a second option if that trade off isn't desired.
From an end user experience if they see a package in a repository as
namespace/modulethen they would expect to be able to use that as the-Namerather than having to know that the prefix is some OCI specific configuration that needs to be handled separately.Example User Experience
After implementation, users could work with GHCR or other OCI registry packages with a prefix like this:
SydneyhSmith commentedon Aug 13, 2026
Thanks Jordan Borean (@jborean93) let's revisit this after the ORAS PR goes in