Some checks failed
Test start.ps1 (Windows) / test-windows (push) Has been cancelled
- docker-compose:REDIS_IMAGE、MONGO_IMAGE、RABBITMQ_IMAGE 可配置,默认保持原阿里云地址 - .env.sample:补充三镜像变量、Mongo 4.4 备选地址及用途说明、RabbitMQ 备选示例 - 新增 mirror-image-to-registry.ps1:从 Hub/镜像站同步指定架构到私有仓库,含凭据与并行复制优化 Made-with: Cursor
481 lines
18 KiB
PowerShell
481 lines
18 KiB
PowerShell
#Requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
Mirror a public image (default: Docker Hub via optional China mirror) to your registry,
|
|
keeping only selected platforms (default linux/amd64 + linux/arm64), then push one multi-arch tag.
|
|
|
|
.DESCRIPTION
|
|
Uses skopeo copy per digest + docker buildx imagetools create.
|
|
Docker Desktop users: credentials are resolved from credential helper into a UTF-8 no-BOM auth file.
|
|
|
|
.PARAMETER Source
|
|
Docker Hub style: redis:latest, nginx:alpine, bitnami/redis:7, library/redis:7, docker.io/prom/prometheus:latest
|
|
Or any registry: ghcr.io/org/image:tag (no Docker-Hub mirror rewrite)
|
|
|
|
.PARAMETER DestRepo
|
|
Target repository without tag, e.g. registry.cn-shanghai.aliyuncs.com/myns/redis
|
|
|
|
.PARAMETER DestTag
|
|
Tag on destination (default: tag parsed from -Source)
|
|
|
|
.PARAMETER MirrorRegistry
|
|
Pull-through mirror host for Docker Hub only, e.g. docker.m.daocloud.io. Empty = use docker.io directly.
|
|
|
|
.PARAMETER Platforms
|
|
OS/arch list to keep, default linux/amd64 and linux/arm64.
|
|
|
|
.PARAMETER SkopeoParallelCopies
|
|
skopeo --image-parallel-copies (layers in flight). 0 = auto (higher default for better upload utilization).
|
|
If upload still looks underused, try 32 or 40; if CPU/memory spikes, lower to 12.
|
|
|
|
Other tuning (outside this script): use ACR region closest to you; Docker Desktop -> Resources give more CPUs/RAM;
|
|
avoid VPN throttling; wired Ethernet; temporarily pause heavy uploads on same link.
|
|
|
|
.EXAMPLE
|
|
.\mirror-image-to-registry.ps1 -Source nginx:alpine -DestRepo registry.cn-shanghai.aliyuncs.com/myns/nginx -DestTag alpine
|
|
|
|
.EXAMPLE
|
|
.\mirror-image-to-registry.ps1 -Source docker.io/library/redis:latest -DestRepo registry.example.com/prod/redis -MirrorRegistry ""
|
|
#>
|
|
|
|
param(
|
|
[Parameter(Mandatory = $true)][string] $Source,
|
|
[Parameter(Mandatory = $true)][string] $DestRepo,
|
|
[string] $DestTag = "",
|
|
[string] $MirrorRegistry = "docker.m.daocloud.io",
|
|
[string[]] $Platforms = @("linux/amd64", "linux/arm64"),
|
|
[string] $SkopeoImage = "quay.io/skopeo/stable:latest",
|
|
[string] $DestUsername = "",
|
|
[string] $DestPassword = "",
|
|
[int] $SkopeoParallelCopies = 0
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# Parallel layer copies: 0 = auto (scale with CPU cores, capped)
|
|
$nCpu = [Environment]::ProcessorCount
|
|
if ($SkopeoParallelCopies -le 0) {
|
|
$script:EffectiveParallelCopies = [Math]::Min(40, [Math]::Max(16, $nCpu * 4))
|
|
Write-Host "SkopeoParallelCopies=auto -> $script:EffectiveParallelCopies (logical CPUs: $nCpu)"
|
|
} else {
|
|
$script:EffectiveParallelCopies = $SkopeoParallelCopies
|
|
Write-Host "SkopeoParallelCopies=$script:EffectiveParallelCopies"
|
|
}
|
|
|
|
function Test-CmdInPath {
|
|
param([string]$Name)
|
|
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
|
|
}
|
|
|
|
function Get-RegistryHostFromRepo {
|
|
param([string]$Repo)
|
|
return ($Repo -split '/')[0]
|
|
}
|
|
|
|
function Get-DockerConfigPath {
|
|
return (Join-Path $env:USERPROFILE ".docker\config.json")
|
|
}
|
|
|
|
function Write-JsonFileUtf8NoBom {
|
|
param([string]$Path, [string]$JsonText)
|
|
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
|
[System.IO.File]::WriteAllText($Path, $JsonText, $utf8NoBom)
|
|
}
|
|
|
|
function New-SkopeoAuthJsonFile {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$RegistryHost,
|
|
[Parameter(Mandatory = $true)][string]$Username,
|
|
[Parameter(Mandatory = $true)][string]$Password
|
|
)
|
|
$pair = "${Username}:${Password}"
|
|
$authB64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($pair))
|
|
$payload = @{
|
|
auths = @{
|
|
$RegistryHost = @{
|
|
auth = $authB64
|
|
}
|
|
}
|
|
}
|
|
$path = [System.IO.Path]::GetTempFileName() + ".json"
|
|
$jsonText = $payload | ConvertTo-Json -Compress -Depth 6
|
|
Write-JsonFileUtf8NoBom -Path $path -JsonText $jsonText
|
|
return $path
|
|
}
|
|
|
|
function Get-InlineAuthFromDockerConfig {
|
|
param([string]$DockerConfigPath, [string]$RegistryHost)
|
|
if (-not (Test-Path -LiteralPath $DockerConfigPath)) { return $null }
|
|
$txt = Get-Content -LiteralPath $DockerConfigPath -Raw -ErrorAction Stop
|
|
$cfg = $txt | ConvertFrom-Json
|
|
if (-not $cfg.auths) { return $null }
|
|
foreach ($p in $cfg.auths.PSObject.Properties) {
|
|
if ($p.Name -eq $RegistryHost -and $p.Value.auth) {
|
|
return [string]$p.Value.auth
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Get-CredentialFromDockerHelper {
|
|
param([string]$RegistryHost, [string]$HelperName)
|
|
if (-not $HelperName) { return $null }
|
|
$exeName = "docker-credential-$HelperName"
|
|
$exe = Get-Command $exeName -ErrorAction SilentlyContinue
|
|
if (-not $exe) {
|
|
$candidates = @(
|
|
(Join-Path ${env:ProgramFiles} "Docker\Docker\resources\bin\$exeName.exe"),
|
|
(Join-Path ${env:ProgramFiles} "Docker\Docker\resources\bin\$HelperName.exe")
|
|
)
|
|
foreach ($c in $candidates) {
|
|
if (Test-Path -LiteralPath $c) { $exe = Get-Command $c -ErrorAction SilentlyContinue; break }
|
|
}
|
|
}
|
|
if (-not $exe) {
|
|
Write-Warning "Credential helper '$exeName' not found on PATH or Docker resources."
|
|
return $null
|
|
}
|
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
$psi.FileName = $exe.Source
|
|
$psi.Arguments = "get"
|
|
$psi.RedirectStandardInput = $true
|
|
$psi.RedirectStandardOutput = $true
|
|
$psi.RedirectStandardError = $true
|
|
$psi.UseShellExecute = $false
|
|
$psi.CreateNoWindow = $true
|
|
$p = New-Object System.Diagnostics.Process
|
|
$p.StartInfo = $psi
|
|
[void]$p.Start()
|
|
$p.StandardInput.WriteLine($RegistryHost)
|
|
$p.StandardInput.Close()
|
|
$out = $p.StandardOutput.ReadToEnd()
|
|
$err = $p.StandardError.ReadToEnd()
|
|
$p.WaitForExit()
|
|
if ($p.ExitCode -ne 0) {
|
|
Write-Warning "docker-credential-$HelperName get failed (exit $($p.ExitCode)): $err"
|
|
return $null
|
|
}
|
|
try {
|
|
$j = $out | ConvertFrom-Json
|
|
return @{ Username = $j.Username; Secret = $j.Secret }
|
|
} catch {
|
|
return $null
|
|
}
|
|
}
|
|
|
|
function Resolve-DestinationAuthFile {
|
|
param([string]$RegistryHost)
|
|
if ($DestUsername -and $DestPassword) {
|
|
Write-Host "Using -DestUsername / -DestPassword for registry push auth."
|
|
return (New-SkopeoAuthJsonFile -RegistryHost $RegistryHost -Username $DestUsername -Password $DestPassword)
|
|
}
|
|
if ($env:ALIYUN_CR_USER -and $env:ALIYUN_CR_PASS) {
|
|
Write-Host "Using ALIYUN_CR_USER / ALIYUN_CR_PASS for registry push auth."
|
|
return (New-SkopeoAuthJsonFile -RegistryHost $RegistryHost -Username $env:ALIYUN_CR_USER -Password $env:ALIYUN_CR_PASS)
|
|
}
|
|
if ($env:DEST_REGISTRY_USER -and $env:DEST_REGISTRY_PASS) {
|
|
Write-Host "Using DEST_REGISTRY_USER / DEST_REGISTRY_PASS for registry push auth."
|
|
return (New-SkopeoAuthJsonFile -RegistryHost $RegistryHost -Username $env:DEST_REGISTRY_USER -Password $env:DEST_REGISTRY_PASS)
|
|
}
|
|
$dockerCfg = Get-DockerConfigPath
|
|
$inline = Get-InlineAuthFromDockerConfig -DockerConfigPath $dockerCfg -RegistryHost $RegistryHost
|
|
if ($inline) {
|
|
Write-Host "Using inline auth from Docker config.json for $RegistryHost"
|
|
$path = [System.IO.Path]::GetTempFileName() + ".json"
|
|
$payload = @{ auths = @{ $RegistryHost = @{ auth = $inline } } }
|
|
$jsonText = $payload | ConvertTo-Json -Compress -Depth 6
|
|
Write-JsonFileUtf8NoBom -Path $path -JsonText $jsonText
|
|
return $path
|
|
}
|
|
if (Test-Path -LiteralPath $dockerCfg) {
|
|
$cfg = (Get-Content -LiteralPath $dockerCfg -Raw) | ConvertFrom-Json
|
|
$helperName = $null
|
|
if ($cfg.credHelpers) {
|
|
foreach ($hp in $cfg.credHelpers.PSObject.Properties) {
|
|
if ($hp.Name -eq $RegistryHost) {
|
|
$helperName = [string]$hp.Value
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if (-not $helperName -and $cfg.credsStore) {
|
|
$helperName = $cfg.credsStore
|
|
}
|
|
if ($helperName) {
|
|
Write-Host "Resolving credentials via docker-credential-$helperName (Docker Desktop)..."
|
|
$c = Get-CredentialFromDockerHelper -RegistryHost $RegistryHost -HelperName $helperName
|
|
if ($c -and $c.Username -and $c.Secret) {
|
|
return (New-SkopeoAuthJsonFile -RegistryHost $RegistryHost -Username $c.Username -Password $c.Secret)
|
|
}
|
|
}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Split-ImageRefPathAndTag {
|
|
param([string]$Ref)
|
|
if ($Ref -match '@sha256:') {
|
|
throw "Digest-pinned sources (@sha256:...) are not supported. Use a tag."
|
|
}
|
|
$tag = "latest"
|
|
$path = $Ref
|
|
if ($Ref -match '^(.+):([^:/]+)$') {
|
|
$path = $matches[1]
|
|
$tag = $matches[2]
|
|
}
|
|
return @{ Path = $path; Tag = $tag }
|
|
}
|
|
|
|
function Test-IsDockerHubOnlyPath {
|
|
param([string]$PathPart)
|
|
if ($PathPart -match '^[a-zA-Z0-9][a-zA-Z0-9.-]*\.[a-zA-Z]+/') { return $false }
|
|
return $true
|
|
}
|
|
|
|
function Resolve-DockerHubRepoPath {
|
|
param([string]$PathPart)
|
|
if ($PathPart -notmatch '/') {
|
|
return "library/$PathPart"
|
|
}
|
|
return $PathPart
|
|
}
|
|
|
|
# Returns: IsDockerHub, InspectRef (docker://...), SourceRepoPath for copy by digest (host/repo without tag), Tag
|
|
function Resolve-SourceInspectReference {
|
|
param(
|
|
[string]$Source,
|
|
[string]$MirrorRegistry
|
|
)
|
|
$s = $Source.Trim()
|
|
if ($s.StartsWith("docker://")) {
|
|
$s = $s.Substring(9)
|
|
}
|
|
$st = Split-ImageRefPathAndTag $s
|
|
$path = $st.Path
|
|
$tag = $st.Tag
|
|
|
|
if (Test-IsDockerHubOnlyPath $path) {
|
|
$repo = Resolve-DockerHubRepoPath $path
|
|
$hubRef = "docker.io/${repo}:${tag}"
|
|
if ($MirrorRegistry) {
|
|
$inspect = "docker://${MirrorRegistry}/${repo}:${tag}"
|
|
} else {
|
|
$inspect = "docker://${hubRef}"
|
|
}
|
|
return @{
|
|
IsDockerHub = $true
|
|
InspectRef = $inspect
|
|
SourceRepoPath = if ($MirrorRegistry) { "${MirrorRegistry}/${repo}" } else { "docker.io/${repo}" }
|
|
Tag = $tag
|
|
}
|
|
}
|
|
|
|
# Full registry reference e.g. ghcr.io/org/img:tag
|
|
$firstSlash = $path.IndexOf('/')
|
|
if ($firstSlash -lt 1) { throw "Invalid source: $Source" }
|
|
$hostPart = $path.Substring(0, $firstSlash)
|
|
$repoPath = $path.Substring($firstSlash + 1)
|
|
if (-not $repoPath) { throw "Invalid source: $Source" }
|
|
$inspect = "docker://${hostPart}/${repoPath}:${tag}"
|
|
return @{
|
|
IsDockerHub = $false
|
|
InspectRef = $inspect
|
|
SourceRepoPath = "${hostPart}/${repoPath}"
|
|
Tag = $tag
|
|
}
|
|
}
|
|
|
|
function Get-DockerAuthVolumeArgs {
|
|
param([string]$AuthFilePath)
|
|
if ($AuthFilePath -and (Test-Path -LiteralPath $AuthFilePath)) {
|
|
return @("-v", "${AuthFilePath}:/auth.json:ro")
|
|
}
|
|
return @()
|
|
}
|
|
|
|
function Invoke-SkopeoCopyWithAuth {
|
|
param(
|
|
[string]$SkopeoImage,
|
|
[string]$SourceRef,
|
|
[string]$DestRef,
|
|
[string]$AuthFilePath,
|
|
[int]$ParallelCopies = 16,
|
|
[int]$GoMaxProcs = 0
|
|
)
|
|
$parallelArgs = @()
|
|
if ($ParallelCopies -gt 0) {
|
|
$parallelArgs = @("--image-parallel-copies", "$ParallelCopies")
|
|
}
|
|
if ($GoMaxProcs -le 0) { $GoMaxProcs = [Environment]::ProcessorCount }
|
|
$vol = Get-DockerAuthVolumeArgs -AuthFilePath $AuthFilePath
|
|
if (-not (Test-CmdInPath "skopeo")) {
|
|
if (-not $AuthFilePath) {
|
|
throw "No auth file for skopeo copy. Use -DestUsername/-DestPassword, DEST_REGISTRY_*, ALIYUN_CR_*, or docker login with resolvable credentials."
|
|
}
|
|
# GOMAXPROCS lets the Go runtime use more threads for TLS/registry I/O alongside parallel blob copies
|
|
$dockerLine = @("run", "--rm", "-e", "GOMAXPROCS=$GoMaxProcs") + $vol + @($SkopeoImage) + @("copy", "--authfile", "/auth.json") + $parallelArgs + @($SourceRef, $DestRef)
|
|
& docker @dockerLine
|
|
$script:SkopeoLastExitCode = $LASTEXITCODE
|
|
return
|
|
}
|
|
if ($AuthFilePath) {
|
|
$env:GOMAXPROCS = "$GoMaxProcs"
|
|
try {
|
|
& skopeo copy --authfile $AuthFilePath @parallelArgs $SourceRef $DestRef
|
|
} finally {
|
|
Remove-Item Env:GOMAXPROCS -ErrorAction SilentlyContinue
|
|
}
|
|
} else {
|
|
$env:GOMAXPROCS = "$GoMaxProcs"
|
|
try {
|
|
& skopeo copy @parallelArgs $SourceRef $DestRef
|
|
} finally {
|
|
Remove-Item Env:GOMAXPROCS -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
$script:SkopeoLastExitCode = $LASTEXITCODE
|
|
}
|
|
|
|
function Invoke-SkopeoInspectRaw {
|
|
param([string]$SkopeoImage, [string]$InspectRef)
|
|
if (Test-CmdInPath "skopeo") {
|
|
$rawLines = & skopeo inspect --raw $InspectRef 2>&1
|
|
$code = $LASTEXITCODE
|
|
} else {
|
|
$rawLines = & docker run --rm $SkopeoImage inspect --raw $InspectRef 2>&1
|
|
$code = $LASTEXITCODE
|
|
}
|
|
return [PSCustomObject]@{ RawLines = $rawLines; ExitCode = $code }
|
|
}
|
|
|
|
function Test-PlatformMatch {
|
|
param($ManifestEntry, [string]$OsWant, [string]$ArchWant)
|
|
if (-not $ManifestEntry.platform) { return $false }
|
|
$os = $ManifestEntry.platform.os
|
|
$arch = $ManifestEntry.platform.architecture
|
|
if ($os -ne $OsWant) { return $false }
|
|
if ($arch -eq $ArchWant) { return $true }
|
|
return $false
|
|
}
|
|
|
|
if (-not (Test-CmdInPath "docker")) {
|
|
Write-Error "docker not found."
|
|
}
|
|
|
|
$resolved = Resolve-SourceInspectReference -Source $Source -MirrorRegistry $MirrorRegistry
|
|
if (-not $DestTag) {
|
|
$DestTag = $resolved.Tag
|
|
}
|
|
|
|
$destHost = Get-RegistryHostFromRepo -Repo $DestRepo
|
|
$destImage = "${DestRepo}:${DestTag}"
|
|
|
|
if (-not (Test-CmdInPath "skopeo")) {
|
|
Write-Host "Using skopeo from Docker image: $SkopeoImage"
|
|
}
|
|
|
|
Write-Host "Inspect: $($resolved.InspectRef)"
|
|
Write-Host "Destination: $destImage"
|
|
Write-Host "Platforms: $($Platforms -join ', ')"
|
|
|
|
$inspectResult = Invoke-SkopeoInspectRaw -SkopeoImage $SkopeoImage -InspectRef $resolved.InspectRef
|
|
if ($inspectResult.ExitCode -ne 0) {
|
|
throw "skopeo inspect failed (exit $($inspectResult.ExitCode)): $($inspectResult.RawLines)"
|
|
}
|
|
|
|
$rawLines = $inspectResult.RawLines
|
|
$rawJson = if ($rawLines -is [array]) { $rawLines -join "`n" } else { [string]$rawLines }
|
|
$rawJson = $rawJson.Trim()
|
|
if ($rawJson -notmatch '^\s*\{') {
|
|
$idx = $rawJson.LastIndexOf('{')
|
|
if ($idx -ge 0) { $rawJson = $rawJson.Substring($idx) }
|
|
}
|
|
|
|
$index = $rawJson | ConvertFrom-Json
|
|
|
|
# Single manifest (not a list): copy tag-to-tag
|
|
if (-not $index.manifests) {
|
|
Write-Host "Source is a single manifest (not a multi-arch index). Copying tag as-is..."
|
|
$srcTagRef = $resolved.InspectRef
|
|
$authFileForPush = Resolve-DestinationAuthFile -RegistryHost $destHost
|
|
if (-not $authFileForPush) { throw "Could not resolve push credentials for $destHost." }
|
|
try {
|
|
Invoke-SkopeoCopyWithAuth -SkopeoImage $SkopeoImage -SourceRef $srcTagRef -DestRef "docker://${destImage}" -AuthFilePath $authFileForPush -ParallelCopies $script:EffectiveParallelCopies
|
|
if ($script:SkopeoLastExitCode -ne 0) { throw "skopeo copy failed (exit $($script:SkopeoLastExitCode))" }
|
|
Write-Host "Done: $destImage"
|
|
} finally {
|
|
if ($authFileForPush -and (Test-Path -LiteralPath $authFileForPush)) {
|
|
Remove-Item -LiteralPath $authFileForPush -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
exit 0
|
|
}
|
|
|
|
$digestByPlatform = @{}
|
|
foreach ($plat in $Platforms) {
|
|
$parts = $plat -split '/', 2
|
|
if ($parts.Length -ne 2) { throw "Invalid platform (use os/arch): $plat" }
|
|
$osW = $parts[0]
|
|
$archW = $parts[1]
|
|
$found = $null
|
|
foreach ($m in $index.manifests) {
|
|
if (Test-PlatformMatch -ManifestEntry $m -OsWant $osW -ArchWant $archW) {
|
|
$found = $m.digest
|
|
break
|
|
}
|
|
}
|
|
if (-not $found) {
|
|
throw "Platform $plat not found in source manifest list. Inspect with: docker buildx imagetools inspect $($resolved.InspectRef -replace '^docker://','')"
|
|
}
|
|
$digestByPlatform[$plat] = $found
|
|
}
|
|
|
|
foreach ($p in $Platforms) {
|
|
Write-Host "$p : $($digestByPlatform[$p])"
|
|
}
|
|
|
|
$srcBase = $resolved.SourceRepoPath
|
|
$dstBase = $DestRepo
|
|
|
|
$authFileForPush = Resolve-DestinationAuthFile -RegistryHost $destHost
|
|
if (-not $authFileForPush) {
|
|
throw @"
|
|
Could not resolve push credentials for $destHost.
|
|
|
|
Use: -DestUsername / -DestPassword, or env DEST_REGISTRY_USER + DEST_REGISTRY_PASS, or ALIYUN_CR_USER + ALIYUN_CR_PASS, or docker login.
|
|
"@
|
|
}
|
|
|
|
try {
|
|
Write-Host ""
|
|
Write-Host "Tip: After the 'Copying blob ...' lines, the console may stay quiet for many minutes while large layers upload to the registry (not frozen). Watch Task Manager -> Network (send) if unsure."
|
|
$step = 1
|
|
$total = $Platforms.Count
|
|
foreach ($plat in $Platforms) {
|
|
$d = $digestByPlatform[$plat]
|
|
Write-Host ""
|
|
Write-Host "[$step/$total] skopeo copy $plat ..."
|
|
$step++
|
|
Invoke-SkopeoCopyWithAuth -SkopeoImage $SkopeoImage -SourceRef "docker://${srcBase}@${d}" -DestRef "docker://${dstBase}@${d}" -AuthFilePath $authFileForPush -ParallelCopies $script:EffectiveParallelCopies
|
|
if ($script:SkopeoLastExitCode -ne 0) { throw "skopeo copy failed for $plat (exit $($script:SkopeoLastExitCode))" }
|
|
}
|
|
|
|
$refsForBuildx = @()
|
|
foreach ($plat in $Platforms) {
|
|
$refsForBuildx += "${dstBase}@$($digestByPlatform[$plat])"
|
|
}
|
|
|
|
Write-Host ""
|
|
Write-Host "[$($total + 1)/$($total + 1)] docker buildx imagetools create..."
|
|
& docker buildx imagetools create -t $destImage @refsForBuildx
|
|
if ($LASTEXITCODE -ne 0) { throw "docker buildx imagetools create failed" }
|
|
|
|
Write-Host ""
|
|
Write-Host "Done: $destImage"
|
|
Write-Host "Verify: docker buildx imagetools inspect $destImage"
|
|
} finally {
|
|
if ($authFileForPush -and (Test-Path -LiteralPath $authFileForPush)) {
|
|
Remove-Item -LiteralPath $authFileForPush -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|