diff --git a/AU/Plugins/GitReleases.ps1 b/AU/Plugins/GitReleases.ps1 index 118fc453..fd76babf 100644 --- a/AU/Plugins/GitReleases.ps1 +++ b/AU/Plugins/GitReleases.ps1 @@ -55,13 +55,37 @@ function GetOrCreateRelease() { "prerelease" = $false } | ConvertTo-Json -Compress - Write-Verbose "Trying to create the new release $tagName..." + Write-Host "Creating the new release $tagName..." return Invoke-RestMethod -UseBasicParsing -Method Post -Uri "https://api.github.com/repos/$repository/releases" -Body $json -Headers $headers } [array]$packages = if ($Force) { $Info.result.updated } else { $Info.result.pushed } + if ($packages.Length -eq 0) { Write-Host "No package updated, skipping"; return } +$packagesToRelease = New-Object 'System.Collections.Generic.List[hashtable]' + +$packages | % { + if ($_.Streams) { + $_.Streams.Values | ? { $_.Updated } | % { + $packagesToRelease.Add(@{ + Name = $_.Name + NuspecVersion = $_.NuspecVersion + RemoteVersion = $_.RemoteVersion + NuFile = Resolve-Path ("$($_.Path)/$($_.Name).$($_.RemoteVersion).nupkg") + }) + } + } + else { + $packagesToRelease.Add(@{ + Name = $_.Name + NuspecVersion = $_.NuspecVersion + RemoteVersion = $_.RemoteVersion + NuFile = Resolve-Path ("$($_.Path)/$($_.Name).$($_.RemoteVersion).nupkg") + }) + } +} + $origin = git config --get remote.origin.url if (!($origin -match "github.com\/([^\/]+\/[^\/\.]+)")) { @@ -71,64 +95,69 @@ if (!($origin -match "github.com\/([^\/]+\/[^\/\.]+)")) { $repository = $Matches[1] $headers = @{ - Authorization = "token $ApiToken" + Authorization = "token $ApiToken" } if ($releaseType -eq 'date' -and !$releaseHeader) { - $releaseHeader = 'Packages updated on ' -} elseif (!$releaseHeader) { - $releaseHeader = ' ' + $releaseHeader = 'Packages updated on ' +} +elseif (!$releaseHeader) { + $releaseHeader = ' ' } if ($releaseType -eq 'date' -and !$releaseDescription) { - $releaseDescription = 'We had packages that was updated on ' -} elseif (!$releaseDescription) { - $releaseDescription = ' was updated from version to ' + $releaseDescription = 'We had packages that was updated on ' +} +elseif (!$releaseDescription) { + $releaseDescription = ' was updated from version to ' } $date = Get-Date -UFormat $dateFormat if ($releaseType -eq 'date') { - $release = GetOrCreateRelease ` - -tagName $date ` - -releaseName ($releaseHeader -replace '',$date) ` - -releaseDescription ($releaseDescription -replace '',$date) ` - -repository $repository ` - -headers $headers - - if (!$release) { - Write-Error "Unable to create a new release, please check your permissions..." - return - } + $release = GetOrCreateRelease ` + -tagName $date ` + -releaseName ($releaseHeader -replace '', $date) ` + -releaseDescription ($releaseDescription -replace '', $date) ` + -repository $repository ` + -headers $headers + + if (!$release) { + Write-Error "Unable to create a new release, please check your permissions..." + return + } } $uploadHeaders = $headers.Clone() $uploadHeaders['Content-Type'] = 'application/zip' -$packages | % { - if ($releaseType -eq 'package') { - $releaseName = $releaseHeader -replace '',$_.Name -replace '',$_.RemoteVersion -replace '',$_.NuspecVersion -replace '',$date - $packageDesc = $releaseDescription -replace '',$_.Name -replace '',$_.RemoteVersion -replace '',$_.NuspecVersion -replace '',$date +$packagesToRelease | % { + # Because we grab all streams previously, we need to ignore + # cases when a stream haven't been updated (no nupkg file created) + if (!$_.NuFile) { return } + + if ($releaseType -eq 'package') { + $releaseName = $releaseHeader -replace '', $_.Name -replace '', $_.RemoteVersion -replace '', $_.NuspecVersion -replace '', $date + $packageDesc = $releaseDescription -replace '', $_.Name -replace '', $_.RemoteVersion -replace '', $_.NuspecVersion -replace '', $date + + $release = GetOrCreateRelease ` + -tagName "$($_.Name)-$($_.RemoteVersion)" ` + -releaseName $releaseName ` + -releaseDescription $packageDesc ` + -repository $repository ` + -headers $headers + } - $release = GetOrCreateRelease ` - -tagName "$($_.Name)-$($_.RemoteVersion)" ` - -releaseName $releaseName ` - -releaseDescription $packageDesc ` - -repository $repository ` - -headers $headers - } - - $path = Resolve-Path "$($_.Path)\*.nupkg" - $fileName = [System.IO.Path]::GetFileName($path) - - $existing = $release.assets | ? name -eq $fileName - if ($existing) { - Write-Verbose "Removing existing $fileName asset..." - Invoke-RestMethod -UseBasicParsing -Uri $existing.url -method Delete -Headers $headers | Out-Null - } - - $uploadUrl = $release.upload_url -replace '\{.*\}$','' - $rawContent = [System.IO.File]::ReadAllBytes($path) - Write-Host "Uploading $fileName asset..." - Invoke-RestMethod -UseBasicParsing -Uri "${uploadUrl}?name=${fileName}&label=$($_.Name) v$($_.RemoteVersion)" -Body $rawContent -Headers $uploadHeaders -Method Post | Out-Null + $fileName = [System.IO.Path]::GetFileName($_.NuFile) + + $existing = $release.assets | ? name -eq $fileName + if ($existing) { + Write-Verbose "Removing existing $fileName asset..." + Invoke-RestMethod -UseBasicParsing -Uri $existing.url -method Delete -Headers $headers | Out-Null + } + + $uploadUrl = $release.upload_url -replace '\{.*\}$', '' + $rawContent = [System.IO.File]::ReadAllBytes($_.NuFile) + Write-Host "Uploading $fileName asset..." + Invoke-RestMethod -UseBasicParsing -Uri "${uploadUrl}?name=${fileName}&label=$($_.Name) v$($_.RemoteVersion)" -Body $rawContent -Headers $uploadHeaders -Method Post | Out-Null } diff --git a/AU/Private/AUPackage.ps1 b/AU/Private/AUPackage.ps1 index c9c20ac4..83dd3f64 100644 --- a/AU/Private/AUPackage.ps1 +++ b/AU/Private/AUPackage.ps1 @@ -11,6 +11,8 @@ class AUPackage { [xml] $NuspecXml [bool] $Ignored [string] $IgnoreMessage + [string] $StreamsPath + [System.Collections.Specialized.OrderedDictionary] $Streams AUPackage([string] $Path ){ if ([String]::IsNullOrWhiteSpace( $Path )) { throw 'Package path can not be empty' } @@ -23,6 +25,18 @@ class AUPackage { $this.NuspecXml = [AUPackage]::LoadNuspecFile( $this.NuspecPath ) $this.NuspecVersion = $this.NuspecXml.package.metadata.version + + $this.StreamsPath = '{0}\{1}.json' -f $this.Path, $this.Name + $this.Streams = [AUPackage]::LoadStreams( $this.StreamsPath ) + } + + [hashtable] GetStreamDetails() { + return @{ + Path = $this.Path + Name = $this.Name + Updated = $this.Updated + RemoteVersion = $this.RemoteVersion + } } static [xml] LoadNuspecFile( $NuspecPath ) { @@ -37,6 +51,30 @@ class AUPackage { [System.IO.File]::WriteAllText($this.NuspecPath, $this.NuspecXml.InnerXml, $Utf8NoBomEncoding) } + static [System.Collections.Specialized.OrderedDictionary] LoadStreams( $streamsPath ) { + if (!(Test-Path $streamsPath)) { return $null } + $res = [ordered] @{} + $versions = Get-Content $streamsPath | ConvertFrom-Json + $versions.psobject.Properties | % { + $stream = $_.Name + $res.Add($stream, @{ NuspecVersion = $versions.$stream }) + } + return $res + } + + UpdateStream( $stream, $version ){ + $s = $stream.ToString() + $v = $version.ToString() + if (!$this.Streams) { $this.Streams = [ordered] @{} } + if (!$this.Streams.Contains($s)) { $this.Streams.$s = @{} } + if ($this.Streams.$s -ne 'ignore') { $this.Streams.$s.NuspecVersion = $v } + $versions = [ordered] @{} + $this.Streams.Keys | % { + $versions.Add($_, $this.Streams.$_.NuspecVersion) + } + $versions | ConvertTo-Json | Set-Content $this.StreamsPath -Encoding UTF8 + } + Backup() { $d = "$Env:TEMP\au\" + $this.Name @@ -54,4 +92,28 @@ class AUPackage { return "$d\_output" } + AUPackage( [hashtable] $obj ) { + if (!$obj) { throw 'Obj can not be empty' } + $obj.Keys | ? { $_ -ne 'Streams' } | % { + $this.$_ = $obj.$_ + } + if ($obj.Streams) { + $this.Streams = [ordered] @{} + $obj.Streams.psobject.Properties | % { + $this.Streams.Add($_.Name, $_.Value) + } + } + } + + [hashtable] Serialize() { + $res = @{} + $this | Get-Member -Type Properties | ? { $_.Name -ne 'Streams' } | % { + $property = $_.Name + $res.Add($property, $this.$property) + } + if ($this.Streams) { + $res.Add('Streams', [PSCustomObject] $this.Streams) + } + return $res + } } diff --git a/AU/Private/AUVersion.ps1 b/AU/Private/AUVersion.ps1 new file mode 100644 index 00000000..ba57070d --- /dev/null +++ b/AU/Private/AUVersion.ps1 @@ -0,0 +1,112 @@ +class AUVersion : System.IComparable { + [version] $Version + [string] $Prerelease + [string] $BuildMetadata + + AUVersion([version] $version, [string] $prerelease, [string] $buildMetadata) { + if (!$version) { throw 'Version cannot be null.' } + $this.Version = $version + $this.Prerelease = $prerelease + $this.BuildMetadata = $buildMetadata + } + + AUVersion($input) { + if (!$input) { throw 'Input cannot be null.' } + $v = [AUVersion]::Parse($input -as [string]) + $this.Version = $v.Version + $this.Prerelease = $v.Prerelease + $this.BuildMetadata = $v.BuildMetadata + } + + static [AUVersion] Parse([string] $input) { return [AUVersion]::Parse($input, $true) } + + static [AUVersion] Parse([string] $input, [bool] $strict) { + if (!$input) { throw 'Version cannot be null.' } + $reference = [ref] $null + if (![AUVersion]::TryParse($input, $reference, $strict)) { throw "Invalid version: $input." } + return $reference.Value + } + + static [bool] TryParse([string] $input, [ref] $result) { return [AUVersion]::TryParse($input, $result, $true) } + + static [bool] TryParse([string] $input, [ref] $result, [bool] $strict) { + $result.Value = [AUVersion] $null + if (!$input) { return $false } + $pattern = [AUVersion]::GetPattern($strict) + if ($input -notmatch $pattern) { return $false } + $reference = [ref] $null + if (![version]::TryParse($Matches['version'], $reference)) { return $false } + $result.Value = [AUVersion]::new($reference.Value, $Matches['prerelease'], $Matches['buildMetadata']) + return $true + } + + hidden static [string] GetPattern([bool] $strict) { + $versionPattern = '(?\d+(?:\.\d+){0,3})' + # for now, chocolatey does only support SemVer v1 (no dot separated identifiers in pre-release): + $identifierPattern = "[0-9A-Za-z-]+" + # here is the SemVer v2 equivalent: + #$identifierPattern = "[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*" + if ($strict) { + return "^$versionPattern(?:-(?$identifierPattern))?(?:\+(?$identifierPattern))?`$" + } else { + return "$versionPattern(?:-?(?$identifierPattern))?(?:\+(?$identifierPattern))?" + } + } + + [AUVersion] WithVersion([version] $version) { return [AUVersion]::new($version, $this.Prerelease, $this.BuildMetadata) } + + [int] CompareTo($obj) { + if ($obj -eq $null) { return 1 } + if ($obj -isnot [AUVersion]) { throw "AUVersion expected: $($obj.GetType())" } + $t = $this.GetParts() + $o = $obj.GetParts() + for ($i = 0; $i -lt $t.Length -and $i -lt $o.Length; $i++) { + if ($t[$i].GetType() -ne $o[$i].GetType()) { + $t[$i] = [string] $t[$i] + $o[$i] = [string] $o[$i] + } + if ($t[$i] -gt $o[$i]) { return 1 } + if ($t[$i] -lt $o[$i]) { return -1 } + } + if ($t.Length -eq 1 -and $o.Length -gt 1) { return 1 } + if ($o.Length -eq 1 -and $t.Length -gt 1) { return -1 } + if ($t.Length -gt $o.Length) { return 1 } + if ($t.Length -lt $o.Length) { return -1 } + return 0 + } + + [bool] Equals($obj) { return $this.CompareTo($obj) -eq 0 } + + [int] GetHashCode() { return $this.GetParts().GetHashCode() } + + [string] ToString() { + $result = $this.Version.ToString() + if ($this.Prerelease) { $result += "-$($this.Prerelease)" } + if ($this.BuildMetadata) { $result += "+$($this.BuildMetadata)" } + return $result + } + + [string] ToString([int] $fieldCount) { + if ($fieldCount -eq -1) { return $this.Version.ToString() } + return $this.Version.ToString($fieldCount) + } + + hidden [object[]] GetParts() { + $result = @($this.Version) + if ($this.Prerelease) { + $this.Prerelease -split '\.' | % { + # if identifier is exclusively numeric, cast it to an int + if ($_ -match '^[0-9]+$') { + $result += [int] $_ + } else { + $result += $_ + } + } + } + return $result + } +} + +function ConvertTo-AUVersion($Version) { + return [AUVersion] $Version +} diff --git a/AU/Private/is_version.ps1 b/AU/Private/is_version.ps1 index 4e7db117..71d07577 100644 --- a/AU/Private/is_version.ps1 +++ b/AU/Private/is_version.ps1 @@ -1,8 +1,4 @@ # Returns [bool] function is_version( [string] $Version ) { - $re = '^(\d{1,16})\.(\d{1,16})\.*(\d{1,16})*\.*(\d{1,16})*(-[^.-]+)*$' - if ($Version -notmatch $re) { return $false } - - $v = $Version -replace '-.+' - return [version]::TryParse($v, [ref]($__)) + return [AUVersion]::TryParse($Version, [ref]($__)) } diff --git a/AU/Public/Get-Version.ps1 b/AU/Public/Get-Version.ps1 new file mode 100644 index 00000000..2e727695 --- /dev/null +++ b/AU/Public/Get-Version.ps1 @@ -0,0 +1,24 @@ +# Author: Thomas Démoulins + +<# +.SYNOPSIS + Get a semver-like object from a given version string. + +.DESCRIPTION + This function parses a string containing a semver-like version + and returns an object that represents both the version (with up to 4 parts) + and optionally a pre-release and a build metadata. + + The parsing is quite flexible: + - the string can starts with a 'v' + - there can be no hyphen between the version and the pre-release + - extra spaces (between any parts of the semver-like version) are ignored +#> +function Get-Version { + [CmdletBinding()] + param( + # Version string to parse. + [string] $Version + ) + return [AUVersion]::Parse($Version, $false) +} diff --git a/AU/Public/Push-Package.ps1 b/AU/Public/Push-Package.ps1 index 20ea16e7..dc05998b 100644 --- a/AU/Public/Push-Package.ps1 +++ b/AU/Public/Push-Package.ps1 @@ -3,22 +3,26 @@ <# .SYNOPSIS - Push latest created package to the Chocolatey community repository. + Push latest (or all) created package(s) to the Chocolatey community repository. .DESCRIPTION The function uses they API key from the file api_key in current or parent directory, environment variable or cached nuget API key. #> function Push-Package() { + param( + [switch] $All + ) $api_key = if (Test-Path api_key) { gc api_key } elseif (Test-Path ..\api_key) { gc ..\api_key } elseif ($Env:api_key) { $Env:api_key } - $package = ls *.nupkg | sort -Property CreationTime -Descending | select -First 1 - if (!$package) { throw 'There is no nupkg file in the directory'} + $packages = ls *.nupkg | sort -Property CreationTime -Descending + if (!$All) { $packages = $packages | select -First 1 } + if (!$packages) { throw 'There is no nupkg file in the directory'} if ($api_key) { - cpush $package.Name --api-key $api_key --source https://push.chocolatey.org + $packages | % { cpush $_.Name --api-key $api_key --source https://push.chocolatey.org } } else { - cpush $package.Name --source https://push.chocolatey.org + $packages | % { cpush $_.Name --source https://push.chocolatey.org } } } diff --git a/AU/Public/Set-DescriptionFromReadme.ps1 b/AU/Public/Set-DescriptionFromReadme.ps1 index 1406d293..eedfdd45 100644 --- a/AU/Public/Set-DescriptionFromReadme.ps1 +++ b/AU/Public/Set-DescriptionFromReadme.ps1 @@ -14,16 +14,18 @@ #> function Set-DescriptionFromReadme{ param( - [AUPackage] $Package, + [AUPackage] $Package, # Number of start lines to skip from the README.md, by default 0. [int] $SkipFirst=0, # Number of end lines to skip from the README.md, by default 0. - [int] $SkipLast=0 + [int] $SkipLast=0, + # Readme file path + [string] $ReadmePath = 'README.md' ) - 'Setting package description from README.md' + "Setting package description from $ReadmePath" - $description = gc README.md -Encoding UTF8 + $description = gc $ReadmePath -Encoding UTF8 $endIdx = $description.Length - $SkipLast $description = $description | select -Index ($SkipFirst..$endIdx) | Out-String @@ -33,4 +35,4 @@ function Set-DescriptionFromReadme{ $xml_Description.AppendChild($cdata) | Out-Null $Package.SaveNuspec() -} \ No newline at end of file +} diff --git a/AU/Public/Update-AUPackages.ps1 b/AU/Public/Update-AUPackages.ps1 index ffad4017..6eee9088 100644 --- a/AU/Public/Update-AUPackages.ps1 +++ b/AU/Public/Update-AUPackages.ps1 @@ -47,6 +47,7 @@ function Update-AUPackages { UpdateTimeout - Timeout for background job in seconds, by default 1200 (20 minutes). Force - Force package update even if no new version is found. Push - Set to true to push updated packages to Chocolatey community repository. + PushAll - Set to true to push all updated packages and not only the most recent one per folder. WhatIf - Set to true to set WhatIf option for all packages. PluginPath - Additional path to look for user plugins. If not set only module integrated plugins will work @@ -128,6 +129,8 @@ function Update-AUPackages { } else { $pkg.Error = 'Job returned no object, Vector smash ?' } + } else { + $pkg = [AUPackage]::new($pkg) } $jobseconds = ($job.PSEndTime.TimeOfDay - $job.PSBeginTime.TimeOfDay).TotalSeconds @@ -215,7 +218,7 @@ function Update-AUPackages { if ( "$type" -ne 'AUPackage') { throw "'$using:package_name' update script didn't return AUPackage but: $type" } if ($pkg.Updated -and $Options.Push) { - $pkg.Result += $r = Push-Package + $pkg.Result += $r = Push-Package -All:$Options.PushAll if ($LastExitCode -eq 0) { $pkg.Pushed = $true } else { @@ -228,7 +231,7 @@ function Update-AUPackages { . $s $using:package_name $Options } - $pkg + $pkg.Serialize() } | Out-Null } $result = $result | sort Name diff --git a/AU/Public/Update-Package.ps1 b/AU/Public/Update-Package.ps1 index eacbce65..31c14200 100644 --- a/AU/Public/Update-Package.ps1 +++ b/AU/Public/Update-Package.ps1 @@ -82,7 +82,12 @@ function Update-Package { #Timeout for all web operations, by default 100 seconds. [int] $Timeout, + #Streams to process, either a string or an array. If ommitted, all streams are processed. + #Single stream required when Force is specified. + $IncludeStream, + #Force package update even if no new version is found. + #For multi streams packages, most recent stream is checked by default when Force is specified. [switch] $Force, #Do not show any Write-Host output. @@ -120,7 +125,7 @@ function Update-Package { mkdir -Force $pkg_path | Out-Null $Env:ChocolateyPackageName = "chocolatey\$($package.Name)" - $Env:ChocolateyPackageVersion = $global:Latest.Version + $Env:ChocolateyPackageVersion = $global:Latest.Version.ToString() $Env:ChocolateyAllowEmptyChecksums = 'true' foreach ($a in $arch) { $Env:chocolateyForceX86 = if ($a -eq '32') { 'true' } else { '' } @@ -194,40 +199,119 @@ function Update-Package { invoke_installer } + function process_stream() { + $package.Updated = $false + + if (!(is_version $package.NuspecVersion)) { + Write-Warning "Invalid nuspec file Version '$($package.NuspecVersion)' - using 0.0" + $global:Latest.NuspecVersion = $package.NuspecVersion = '0.0' + } + if (!(is_version $Latest.Version)) { throw "Invalid version: $($Latest.Version)" } + $package.RemoteVersion = $Latest.Version + + # For set_fix_version to work propertly, $Latest.Version's type must be assignable from string. + # If not, then cast its value to string. + if (!('1.0' -as $Latest.Version.GetType())) { + $Latest.Version = [string] $Latest.Version + } + + if (!$NoCheckUrl) { check_urls } + + "nuspec version: " + $package.NuspecVersion | result + "remote version: " + $package.RemoteVersion | result + + $script:is_forced = $false + if ([AUVersion] $Latest.Version -gt [AUVersion] $Latest.NuspecVersion) { + if (!($NoCheckChocoVersion -or $Force)) { + $choco_url = "https://chocolatey.org/packages/{0}/{1}" -f $global:Latest.PackageName, $package.RemoteVersion + try { + request $choco_url $Timeout | out-null + "New version is available but it already exists in the Chocolatey community feed (disable using `$NoCheckChocoVersion`):`n $choco_url" | result + return + } catch { } + } + } else { + if (!$Force) { + 'No new version found' | result + return + } + else { 'No new version found, but update is forced' | result; set_fix_version } + } + + 'New version is available' | result + + $match_url = ($Latest.Keys | ? { $_ -match '^URL*' } | select -First 1 | % { $Latest[$_] } | split-Path -Leaf) -match '(?<=\.)[^.]+$' + if ($match_url -and !$Latest.FileType) { $Latest.FileType = $Matches[0] } + + if ($ChecksumFor -ne 'none') { get_checksum } else { 'Automatic checksum skipped' | result } + + if ($WhatIf) { $package.Backup() } + try { + if (Test-Path Function:\au_BeforeUpdate) { 'Running au_BeforeUpdate' | result; au_BeforeUpdate $package | result } + if (!$NoReadme -and (Test-Path "$($package.Path)\README.md")) { Set-DescriptionFromReadme $package -SkipFirst 2 | result } + update_files + if (Test-Path Function:\au_AfterUpdate) { 'Running au_AfterUpdate' | result; au_AfterUpdate $package | result } + + choco pack --limit-output | result + if ($LastExitCode -ne 0) { throw "Choco pack failed with exit code $LastExitCode" } + } finally { + if ($WhatIf) { + $save_dir = $package.SaveAndRestore() + Write-Warning "Package restored and updates saved to: $save_dir" + } + } + + $package.Updated = $true + } + function set_fix_version() { $script:is_forced = $true if ($global:au_Version) { "Overriding version to: $global:au_Version" | result - $global:Latest.Version = $package.RemoteVersion = $global:au_Version - if (!(is_version $Latest.Version)) { throw "Invalid version: $($Latest.Version)" } + $package.RemoteVersion = $global:au_Version + if (!(is_version $global:au_Version)) { throw "Invalid version: $global:au_Version" } + $global:Latest.Version = $package.RemoteVersion $global:au_Version = $null return } $date_format = 'yyyyMMdd' $d = (get-date).ToString($date_format) - $v = [version]($package.NuspecVersion -replace '-.+') + $nuspecVersion = [AUVersion] $Latest.NuspecVersion + $v = $nuspecVersion.Version $rev = $v.Revision.ToString() try { $revdate = [DateTime]::ParseExact($rev, $date_format,[System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None) } catch {} if (($rev -ne -1) -and !$revdate) { return } $build = if ($v.Build -eq -1) {0} else {$v.Build} - $Latest.Version = $package.RemoteVersion = '{0}.{1}.{2}.{3}' -f $v.Major, $v.Minor, $build, $d + $v = [version] ('{0}.{1}.{2}.{3}' -f $v.Major, $v.Minor, $build, $d) + $package.RemoteVersion = $nuspecVersion.WithVersion($v).ToString() + $Latest.Version = $package.RemoteVersion -as $Latest.Version.GetType() + } + + function set_latest( [HashTable] $latest, [string] $version, $stream ) { + if (!$latest.NuspecVersion) { $latest.NuspecVersion = $version } + if ($stream -and !$latest.Stream) { $latest.Stream = $stream } + $package.NuspecVersion = $latest.NuspecVersion + + $global:Latest = $global:au_Latest + $latest.Keys | % { $global:Latest.Remove($_) } + $global:Latest += $latest } function update_files( [switch]$SkipNuspecFile ) { 'Updating files' | result - ' $Latest data:' | result; ($global:Latest.keys | sort | % { " {0,-15} ({1}) {2}" -f $_, $global:Latest[$_].GetType().Name, $global:Latest[$_] }) | result; '' | result + ' $Latest data:' | result; ($global:Latest.keys | sort | % { " {0,-25} {1,-12} {2}" -f $_, "($($global:Latest[$_].GetType().Name))", $global:Latest[$_] }) | result if (!$SkipNuspecFile) { " $(Split-Path $package.NuspecPath -Leaf)" | result - " setting id: $($global:Latest.PackageName)" | result + " setting id: $($global:Latest.PackageName)" | result $package.NuspecXml.package.metadata.id = $package.Name = $global:Latest.PackageName.ToString() - $msg ="updating version: {0} -> {1}" -f $package.NuspecVersion, $package.RemoteVersion + $msg = " updating version: {0} -> {1}" -f $package.NuspecVersion, $package.RemoteVersion if ($script:is_forced) { if ($package.RemoteVersion -eq $package.NuspecVersion) { $msg = " version not changed as it already uses 'revision': {0}" -f $package.NuspecVersion @@ -239,6 +323,9 @@ function Update-Package { $package.NuspecXml.package.metadata.version = $package.RemoteVersion.ToString() $package.SaveNuspec() + if ($global:Latest.Stream) { + $package.UpdateStream($global:Latest.Stream, $package.RemoteVersion) + } } $sr = au_SearchReplace @@ -250,7 +337,7 @@ function Update-Package { # is detected as ANSI $fileContent = gc $fileName -Encoding UTF8 $sr[ $fileName ].GetEnumerator() | % { - (' {0} = {1} ' -f $_.name, $_.value) | result + (' {0,-35} = {1}' -f $_.name, $_.value) | result if (!($fileContent -match $_.name)) { throw "Search pattern not found: '$($_.name)'" } $fileContent = $fileContent -replace $_.name, $_.value } @@ -262,20 +349,6 @@ function Update-Package { } } - function is_updated() { - $remote_l = $package.RemoteVersion -replace '-.+' - $nuspec_l = $package.NuspecVersion -replace '-.+' - $remote_r = $package.RemoteVersion.Replace($remote_l,'') - $nuspec_r = $package.NuspecVersion.Replace($nuspec_l,'') - - if ([version]$remote_l -eq [version] $nuspec_l) { - if (!$remote_r -and $nuspec_r) { return $true } - if ($remote_r -and !$nuspec_r) { return $false } - return ($remote_r -gt $nuspec_r) - } - [version]$remote_l -gt [version] $nuspec_l - } - function result() { if ($global:Silent) { return } @@ -306,17 +379,13 @@ function Update-Package { if ($Result) { sv -Scope Global -Name $Result -Value $package } $global:Latest = @{PackageName = $package.Name} - $global:Latest.NuspecVersion = $package.NuspecVersion - if (!(is_version $package.NuspecVersion)) { - Write-Warning "Invalid nuspec file Version '$($package.NuspecVersion)' - using 0.0" - $global:Latest.NuspecVersion = $package.NuspecVersion = '0.0' - } [System.Net.ServicePointManager]::SecurityProtocol = 'Ssl3,Tls,Tls11,Tls12' #https://github.com/chocolatey/chocolatey-coreteampackages/issues/366 $module = $MyInvocation.MyCommand.ScriptBlock.Module "{0} - checking updates using {1} version {2}" -f $package.Name, $module.Name, $module.Version | result try { $res = au_GetLatest | select -Last 1 + $global:au_Latest = $global:Latest if ($res -eq $null) { throw 'au_GetLatest returned nothing' } if ($res -eq 'ignore') { return $res } @@ -324,63 +393,82 @@ function Update-Package { $res_type = $res.GetType() if ($res_type -ne [HashTable]) { throw "au_GetLatest doesn't return a HashTable result but $res_type" } - $res.Keys | % { $global:Latest.Remove($_) } - $global:Latest += $res if ($global:au_Force) { $Force = $true } + if ($global:au_IncludeStream) { $IncludeStream = $global:au_IncludeStream } } catch { throw "au_GetLatest failed`n$_" } - if (!(is_version $Latest.Version)) { throw "Invalid version: $($Latest.Version)" } - $package.RemoteVersion = $Latest.Version - - if (!$NoCheckUrl) { check_urls } + if ($res.ContainsKey('Streams')) { + if (!$res.Streams) { throw "au_GetLatest's streams returned nothing" } + if ($res.Streams -isnot [System.Collections.Specialized.OrderedDictionary] -and $res.Streams -isnot [HashTable]) { + throw "au_GetLatest doesn't return an OrderedDictionary or HashTable result for streams but $($res.Streams.GetType())" + } - "nuspec version: " + $package.NuspecVersion | result - "remote version: " + $package.RemoteVersion | result + # Streams are expected to be sorted starting with the most recent one + $streams = @($res.Streams.Keys) + # In case of HashTable (i.e. not sorted), let's sort streams alphabetically descending + if ($res.Streams -is [HashTable]) { $streams = $streams | sort -Descending } - if (is_updated) { - if (!($NoCheckChocoVersion -or $Force)) { - $choco_url = "https://chocolatey.org/packages/{0}/{1}" -f $global:Latest.PackageName, $package.RemoteVersion - try { - request $choco_url $Timeout | out-null - "New version is available but it already exists in the Chocolatey community feed (disable using `$NoCheckChocoVersion`):`n $choco_url" | result - return $package - } catch { } - } - } else { - if (!$Force) { - 'No new version found' | result - return $package + if ($IncludeStream) { + if ($IncludeStream -isnot [string] -and $IncludeStream -isnot [double] -and $IncludeStream -isnot [Array]) { + throw "`$IncludeStream must be either a String, a Double or an Array but is $($IncludeStream.GetType())" + } + if ($IncludeStream -is [double]) { $IncludeStream = $IncludeStream -as [string] } + if ($IncludeStream -is [string]) { + # Forcing type in order to handle case when only one version is included + [Array] $IncludeStream = $IncludeStream -split ',' | % { $_.Trim() } + } + } elseif ($Force) { + # When forcing update, a single stream is expected + # By default, we take the first one (i.e. the most recent one) + $IncludeStream = @($streams | select -First 1) } - else { 'No new version found, but update is forced' | result; set_fix_version } - } + if ($Force -and (!$IncludeStream -or $IncludeStream.Length -ne 1)) { throw 'A single stream must be included when forcing package update' } - 'New version is available' | result + if ($IncludeStream) { $streams = @($streams | ? { $_ -in $IncludeStream }) } + # Let's reverse the order in order to process streams starting with the oldest one + [Array]::Reverse($streams) - $match_url = ($Latest.Keys | ? { $_ -match '^URL*' } | select -First 1 | % { $Latest[$_] } | split-Path -Leaf) -match '(?<=\.)[^.]+$' - if ($match_url -and !$Latest.FileType) { $Latest.FileType = $Matches[0] } + $res.Keys | ? { $_ -ne 'Streams' } | % { $global:au_Latest.Remove($_) } + $global:au_Latest += $res - if ($ChecksumFor -ne 'none') { get_checksum } else { 'Automatic checksum skipped' | result } + $allStreams = [ordered] @{} + $streams | % { + $stream = $res.Streams[$_] - if ($WhatIf) { $package.Backup() } - try { - if (Test-Path Function:\au_BeforeUpdate) { 'Running au_BeforeUpdate' | result; au_BeforeUpdate $package | result } - if (!$NoReadme -and (Test-Path "$($package.Path)\README.md")) { Set-DescriptionFromReadme $package -SkipFirst 2 | result } - update_files - if (Test-Path Function:\au_AfterUpdate) { 'Running au_AfterUpdate' | result; au_AfterUpdate $package | result } - - choco pack --limit-output | result - if ($LastExitCode -ne 0) { throw "Choco pack failed with exit code $LastExitCode" } - } finally { - if ($WhatIf) { - $save_dir = $package.SaveAndRestore() - Write-Warning "Package restored and updates saved to: $save_dir" + '' | result + "*** Stream: $_ ***" | result + + if ($stream -eq $null) { throw "au_GetLatest's $_ stream returned nothing" } + if ($stream -eq 'ignore') { return } + if ($stream -isnot [HashTable]) { throw "au_GetLatest's $_ stream doesn't return a HashTable result but $($stream.GetType())" } + + if ($package.Streams.$_.NuspecVersion -eq 'ignore') { + 'Ignored' | result + return + } + + set_latest $stream $package.Streams.$_.NuspecVersion $_ + process_stream + + if ($package.Streams.$_) { + $allStreams.$_ = $package.Streams.$_ + } else { + $allStreams.$_ = @{ NuspecVersion = $package.NuspecVersion } + } + $allStreams.$_ += $package.GetStreamDetails() } + $package.Updated = $false + $package.Streams = $allStreams + $package.Streams.Values | ? { $_.Updated } | % { $package.Updated = $true } + } else { + '' | result + set_latest $res $package.NuspecVersion + process_stream } - 'Package updated' | result - $package.Updated = $true + if ($package.Updated) { 'Package updated' | result } return $package } diff --git a/README.md b/README.md index 1cc26c48..a5392af7 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ To see AU in action see [video tutorial](https://www.youtube.com/watch?v=m2XpV2L ## Features - Use only PowerShell to create automatic update script for given package. +- Handles multiple streams with a single update script. - Automatically downloads installers and provides/verifies checksums for x32 and x64 versions. - Verifies URLs, nuspec versions, remote repository existence etc. - Automatically sets the Nuspec descriptions from a README.md files. @@ -62,6 +63,32 @@ function global:au_GetLatest { The returned version is later compared to the one in the nuspec file and if remote version is higher, the files will be updated. The returned keys of this HashTable are available via global variable `$global:Latest` (along with some keys that AU generates). You can put whatever data you need in the returned HashTable - this data can be used later in `au_SearchReplace`. +When multiple streams have to be handled, multiple HashTables can be put together in order to describe each supported stream. + +```powershell +function global:au_GetLatest { + # ... + $streams = @{} + $streams.Add('1.2', @{ Version = $version12; URL32 = $url12 }) + $streams.Add('1.3', @{ Version = $version13; URL32 = $url13 }) + return @{ Streams = $streams } +} +``` + +In order to help working with versions, function `Get-Version` can be called in order to parse [semver](http://semver.org/) versions in a flexible manner. It returns an `AUVersion` object with all the details about the version. Furthermore, this object can be compared and sorted. + +``` +PS C:\> Get-Version 'v1.3.2.7rc1' + +Version Prerelease BuildMetadata +------- ---------- ------------- +1.3.2.7 rc1 + + +PS C:\> $version = Get-Version '1.3.2-beta2+5' +PS C:\> $version.ToString(2) + ' => ' + $version.ToString() +1.3 => 1.3.2-beta2+5 +``` ### `au_SearchReplace` diff --git a/build.ps1 b/build.ps1 index 8db6f436..844199c7 100644 --- a/build.ps1 +++ b/build.ps1 @@ -33,7 +33,7 @@ $b = { $ErrorActionPreference = 'Stop' - Write-Host "`n==| Bulding $module_name $version`n" + Write-Host "`n==| Building $module_name $version`n" init $module_path = "$build_dir/$module_name" diff --git a/test.ps1 b/test.ps1 index f883b4ec..42580408 100644 --- a/test.ps1 +++ b/test.ps1 @@ -2,7 +2,8 @@ param( [switch]$Chocolatey, [switch]$Pester, - [string]$Tag + [string]$Tag, + [switch]$CodeCoverage ) if (!$Chocolatey -and !$Pester) { $Chocolatey = $Pester = $true } @@ -20,5 +21,10 @@ if ($Pester) { Write-Host "`n==| Running Pester tests" $testResultsFile = "$build_dir/TestResults.xml" - Invoke-Pester -Tag $Tag -OutputFormat NUnitXml -OutputFile $testResultsFile -PassThru + if ($CodeCoverage) { + $files = @(ls $PSScriptRoot/AU/* -Filter *.ps1 -Recurse | % FullName) + Invoke-Pester -Tag $Tag -OutputFormat NUnitXml -OutputFile $testResultsFile -PassThru -CodeCoverage $files + } else { + Invoke-Pester -Tag $Tag -OutputFormat NUnitXml -OutputFile $testResultsFile -PassThru + } } diff --git a/tests/AUPackage.Tests.ps1 b/tests/AUPackage.Tests.ps1 new file mode 100644 index 00000000..0c99c922 --- /dev/null +++ b/tests/AUPackage.Tests.ps1 @@ -0,0 +1,57 @@ +remove-module AU -ea ignore +import-module $PSScriptRoot\..\AU + +Describe 'AUPackage' -Tag aupackage { + InModuleScope AU { + It 'throws an error when intanciating without a path' { + { [AUPackage]::new('') } | Should Throw 'empty' + } + + It 'throws an error when intanciating without a hashtable' { + { [AUPackage]::new([hashtable] $null) } | Should Throw 'empty' + } + + It 'can serialize and deserialize' { + $expected = @{ + Path = 'path' + Name = 'name' + Updated = $true + Pushed = $true + RemoteVersion = '1.2.3' + NuspecVersion = '0.1.2' + Result = 'result1,result2,result3' -split ',' + Error = 'error' + NuspecPath = 'nuspecPath' + Ignored = $true + IgnoreMessage = 'ignoreMessage' + StreamsPath = 'streamsPath' + Streams = [PSCustomObject] @{ + '0.1' = @{ + NuspecVersion = '0.1.2' + Path = 'path' + Name = 'name' + Updated = $true + RemoteVersion = '1.2.3' + } + '0.2' = @{ + NuspecVersion = '0.2.2' + Path = 'path' + Name = 'name' + Updated = $true + RemoteVersion = '1.2.3' + } + } + } + + $package = [AUPackage]::new($expected) + $actual = $package.Serialize() + + $expected.Keys | ? { $_ -ne 'Streams' } | % { + $actual.$_ | Should Be $expected.$_ + } + $expected.Streams.psobject.Properties | % { + $actual.Streams.$_ | Should Be $expected.Streams.$_ + } + } + } +} diff --git a/tests/Get-Version.Tests.ps1 b/tests/Get-Version.Tests.ps1 new file mode 100644 index 00000000..39551622 --- /dev/null +++ b/tests/Get-Version.Tests.ps1 @@ -0,0 +1,111 @@ +remove-module AU -ea ignore +import-module $PSScriptRoot\..\AU + +Describe 'Get-Version' -Tag getversion { + InModuleScope AU { + It 'should convert a strict version' { + $expectedVersionStart = '1.2' + $expectedVersion = "$expectedVersionStart.3.4" + # for now, chocolatey does only support SemVer v1 (no dot separated identifiers in pre-release): + $expectedPrerelease = 'beta1' + $expectedBuildMetadata = 'xyz001' + # here is the SemVer v2 equivalent: + #$expectedPrerelease = 'beta.1' + #$expectedBuildMetadata = 'xyz.001' + $expected = "$expectedVersion-$expectedPrerelease+$expectedBuildMetadata" + $res = ConvertTo-AUVersion $expected + + $res | Should Not BeNullOrEmpty + $res.Version | Should Be ([version] $expectedVersion) + $res.Prerelease | Should BeExactly $expectedPrerelease + $res.BuildMetadata | Should BeExactly $expectedBuildMetadata + $res.ToString() | Should BeExactly $expected + $res.ToString(2) | Should BeExactly $expectedVersionStart + $res.ToString(-1) | Should BeExactly $expectedVersion + } + + It 'should not convert a non-strict version' { + { ConvertTo-AUVersion '1.2.3.4a' } | Should Throw + # for now, chocolatey does only support SemVer v1 (no dot separated identifiers in pre-release): + { ConvertTo-AUVersion 'v1.2.3.4-beta1+xyz001' } | Should Throw + # here is the SemVer v2 equivalent: + #{ ConvertTo-AUVersion 'v1.2.3.4-beta.1+xyz.001' } | Should Throw + } + + It 'should parse a non strict version' { + $expectedVersion = "1.2.3.4" + # for now, chocolatey does only support SemVer v1 (no dot separated identifiers in pre-release): + $expectedPrerelease = 'beta1' + $expectedBuildMetadata = 'xyz001' + # here is the SemVer v2 equivalent: + #$expectedPrerelease = 'beta.1' + #$expectedBuildMetadata = 'xyz.001' + $res = Get-Version "v$expectedVersion$expectedPrerelease+$expectedBuildMetadata" + + $res | Should Not BeNullOrEmpty + $res.Version | Should Be ([version] $expectedVersion) + $res.Prerelease | Should BeExactly $expectedPrerelease + $res.BuildMetadata | Should BeExactly $expectedBuildMetadata + } + + $testCases = @( + @{A = '1.9.0' ; B = '1.9.0' ; ExpectedResult = 0} + @{A = '1.9.0' ; B = '1.10.0' ; ExpectedResult = -1} + @{A = '1.10.0' ; B = '1.11.0' ; ExpectedResult = -1} + @{A = '1.0.0' ; B = '2.0.0' ; ExpectedResult = -1} + @{A = '2.0.0' ; B = '2.1.0' ; ExpectedResult = -1} + @{A = '2.1.0' ; B = '2.1.1' ; ExpectedResult = -1} + @{A = '1.0.0-alpha' ; B = '1.0.0-alpha' ; ExpectedResult = 0} + @{A = '1.0.0-alpha' ; B = '1.0.0' ; ExpectedResult = -1} + # for now, chocolatey does only support SemVer v1 (no dot separated identifiers in pre-release): + @{A = '1.0.0-alpha1' ; B = '1.0.0-alpha1' ; ExpectedResult = 0} + @{A = '1.0.0-alpha' ; B = '1.0.0-alpha1' ; ExpectedResult = -1} + @{A = '1.0.0-alpha1' ; B = '1.0.0-alphabeta' ; ExpectedResult = -1} + @{A = '1.0.0-alphabeta' ; B = '1.0.0-beta' ; ExpectedResult = -1} + @{A = '1.0.0-beta' ; B = '1.0.0-beta2' ; ExpectedResult = -1} + @{A = '1.0.0-beta2' ; B = '1.0.0-rc1' ; ExpectedResult = -1} + @{A = '1.0.0-rc1' ; B = '1.0.0' ; ExpectedResult = -1} + # here is the SemVer v2 equivalent: + #@{A = '1.0.0-alpha.1' ; B = '1.0.0-alpha.1' ; ExpectedResult = 0} + #@{A = '1.0.0-alpha.1' ; B = '1.0.0-alpha.01' ; ExpectedResult = 0} + #@{A = '1.0.0-alpha' ; B = '1.0.0-alpha.1' ; ExpectedResult = -1} + #@{A = '1.0.0-alpha.1' ; B = '1.0.0-alpha.beta'; ExpectedResult = -1} + #@{A = '1.0.0-alpha.beta'; B = '1.0.0-beta' ; ExpectedResult = -1} + #@{A = '1.0.0-beta' ; B = '1.0.0-beta.2' ; ExpectedResult = -1} + #@{A = '1.0.0-beta.2' ; B = '1.0.0-beta.11' ; ExpectedResult = -1} + #@{A = '1.0.0-beta.11' ; B = '1.0.0-rc.1' ; ExpectedResult = -1} + #@{A = '1.0.0-rc.1' ; B = '1.0.0' ; ExpectedResult = -1} + @{A = '1.0.0' ; B = '1.0.0+1' ; ExpectedResult = 0} + @{A = '1.0.0+1' ; B = '1.0.0+2' ; ExpectedResult = 0} + @{A = '1.0.0-alpha' ; B = '1.0.0-alpha+1' ; ExpectedResult = 0} + @{A = '1.0.0-alpha+1' ; B = '1.0.0-alpha+2' ; ExpectedResult = 0} + ) + + It 'should compare 2 versions successfully' -TestCases $testCases { param([string] $A, [string] $B, [int] $ExpectedResult) + $VersionA = ConvertTo-AUVersion $A + $VersionB = ConvertTo-AUVersion $B + if ($ExpectedResult -gt 0 ) { + $VersionA | Should BeGreaterThan $VersionB + } elseif ($ExpectedResult -lt 0 ) { + $VersionA | Should BeLessThan $VersionB + } else { + $VersionA | Should Be $VersionB + } + } + + $testCases = @( + @{Value = '1.2'} + @{Value = '1.2-beta+003'} + @{Value = [AUVersion] '1.2'} + @{Value = [AUVersion] '1.2-beta+003'} + @{Value = [version] '1.2'} + @{Value = [regex]::Match('1.2', '^(.+)$').Groups[1]} + @{Value = [regex]::Match('1.2-beta+003', '^(.+)$').Groups[1]} + ) + + It 'converts from any type of values' -TestCases $testCases { param($Value) + $version = [AUVersion] $Value + $version | Should Not BeNullOrEmpty + } + } +} diff --git a/tests/Update-AUPackages.Streams.Tests.ps1 b/tests/Update-AUPackages.Streams.Tests.ps1 new file mode 100644 index 00000000..d3ef1a34 --- /dev/null +++ b/tests/Update-AUPackages.Streams.Tests.ps1 @@ -0,0 +1,112 @@ +remove-module AU -ea ignore +import-module $PSScriptRoot\..\AU + +Describe 'Update-AUPackages using streams' -Tag updateallstreams { + $saved_pwd = $pwd + + function global:nuspec_file() { [xml](gc $PSScriptRoot/test_package_with_streams/test_package_with_streams.nuspec) } + $pkg_no = 2 + $streams_no = $pkg_no * 3 + + BeforeEach { + $global:au_Root = "TestDrive:\packages" + $global:au_NoPlugins = $true + + rm -Recurse $global:au_root -ea ignore + foreach ( $i in 1..$pkg_no ) { + $name = "test_package_with_streams_$i" + $path = "$au_root\$name" + + cp -Recurse -Force $PSScriptRoot\test_package_with_streams $path + $nu = nuspec_file + $nu.package.metadata.id = $name + rm "$path\*.nuspec" + $nu.OuterXml | sc "$path\$name.nuspec" + mv "$path\test_package_with_streams.json" "$path\$name.json" + + $module_path = Resolve-Path $PSScriptRoot\..\AU + "import-module '$module_path' -Force", (gc $path\update.ps1 -ea ignore) | sc $path\update.ps1 + } + + $Options = [ordered]@{} + } + + Context 'Plugins' { + + It 'should execute GitReleases plugin when there are updates' { + gc $global:au_Root\test_package_with_streams_1\update.ps1 | set content + $content -replace '@\{.+1\.3.+\}', "@{ Version = '1.3.2' }" | set content + $content -replace '@\{.+1\.2.+\}', "@{ Version = '1.2.4' }" | set content + $content | sc $global:au_Root\test_package_with_streams_1\update.ps1 + + $Options.GitReleases = @{ + ApiToken = 'apiToken' + ReleaseType = 'package' + Force = $true + } + + Mock Invoke-RestMethod { + return @{ + tag_name = 'test_package_with_streams_1-1.2.4' + assets = @( + @{ + url = 'https://api.github.com/test_package_with_streams_1.1.2.4.nupkg' + name = 'test_package_with_streams_1.1.2.4.nupkg' + } + ) + } + } -ModuleName AU + + updateall -NoPlugins:$false -Options $Options 6> $null + + Assert-MockCalled Invoke-RestMethod -Exactly 6 -ModuleName AU + } + } + + It 'should update package with checksum verification mode' { + + $choco_path = gcm choco.exe | % Source + $choco_hash = Get-FileHash $choco_path -Algorithm SHA256 | % Hash + gc $global:au_Root\test_package_with_streams_1\update.ps1 | set content + $content -replace '@\{.+1\.3.+\}', "@{ Version = '1.3.2'; ChecksumType32 = 'sha256'; Checksum32 = '$choco_hash'}" | set content + $content -replace 'update', "update -ChecksumFor 32" | set content + $content | sc $global:au_Root\test_package_with_streams_1\update.ps1 + + $res = updateall -Options $Options 6> $null + $res.Count | Should Be $pkg_no + $res[0].Updated | Should Be $true + } + + It 'should limit update time' { + gc $global:au_Root\test_package_with_streams_1\update.ps1 | set content + $content -replace 'update', "sleep 10; update" | set content + $content | sc $global:au_Root\test_package_with_streams_1\update.ps1 + $Options.UpdateTimeout = 5 + + $res = updateall -Options $Options 3>$null 6> $null + $res[0].Error -eq "Job termintated due to the 5s UpdateTimeout" | Should Be $true + } + + It 'should update all packages when forced' { + $Options.Force = $true + + $res = updateall -Options $Options 6> $null + + lsau | measure | % Count | Should Be $pkg_no + $res.Count | Should Be $pkg_no + ($res.Result -match 'update is forced').Count | Should Be $pkg_no + ($res | ? Updated).Count | Should Be $pkg_no + } + + It 'should update no packages when none is newer' { + $res = updateall 6> $null + + lsau | measure | % Count | Should Be $pkg_no + $res.Count | Should Be $pkg_no + ($res.Result -match 'No new version found').Count | Should Be $streams_no + ($res | ? Updated).Count | Should Be 0 + } + + $saved_pwd = $pwd +} + diff --git a/tests/Update-AUPackages.Tests.ps1 b/tests/Update-AUPackages.Tests.ps1 index a68a08ef..62e7b391 100644 --- a/tests/Update-AUPackages.Tests.ps1 +++ b/tests/Update-AUPackages.Tests.ps1 @@ -136,9 +136,71 @@ Describe 'Update-AUPackages' -Tag updateall { $info.plugin_results.RunInfo -match 'Test.MyPassword' | Should Be $true $info.Options.Test.MyPassword | Should Be '*****' } + + It 'should not execute GitReleases plugin when there are no updates' { + $Options.GitReleases = @{ + ApiToken = 'apiToken' + ReleaseType = 'package' + Force = $true + } + + Mock -ModuleName AU Invoke-RestMethod {} + + updateall -NoPlugins:$false -Options $Options 6> $null + + Assert-MockCalled -ModuleName AU Invoke-RestMethod -Exactly 0 -Scope It + } + + It 'should execute GitReleases plugin per package when there are updates' { + gc $global:au_Root\test_package_1\update.ps1 | set content + $content -replace '@\{.+\}', "@{ Version = '1.3' }" | set content + $content | sc $global:au_Root\test_package_1\update.ps1 + + $Options.GitReleases = @{ + ApiToken = 'apiToken' + ReleaseType = 'package' + Force = $true + } + + Mock -ModuleName AU Invoke-RestMethod { + return @{ + tag_name = 'test_package_1-1.3' + assets = @( + @{ + url = 'https://api.github.com/test_package_1.1.3.nupkg' + name = 'test_package_1.1.3.nupkg' + } + ) + } + } + + updateall -NoPlugins:$false -Options $Options 6> $null + + Assert-MockCalled -ModuleName AU Invoke-RestMethod -Exactly 3 -Scope It + } + + It 'should execute GitReleases plugin per date when there are updates' { + gc $global:au_Root\test_package_1\update.ps1 | set content + $content -replace '@\{.+\}', "@{ Version = '1.3' }" | set content + $content | sc $global:au_Root\test_package_1\update.ps1 + + $Options.GitReleases = @{ + ApiToken = 'apiToken' + ReleaseType = 'date' + Force = $true + } + + Mock -ModuleName AU Get-Date { return '2017-11-05' } -ParameterFilter { $UFormat -eq '{0:yyyy-MM-dd}' } + Mock -ModuleName AU Invoke-RestMethod { return @{ tag_name = '2017-11-05' } } + + updateall -NoPlugins:$false -Options $Options 6> $null + + Assert-MockCalled -ModuleName AU Get-Date -Exactly 1 -Scope It + Assert-MockCalled -ModuleName AU Invoke-RestMethod -Exactly 2 -Scope It + } } - It 'should update package with checsum verification mode' { + It 'should update package with checksum verification mode' { $choco_path = gcm choco.exe | % Source $choco_hash = Get-FileHash $choco_path -Algorithm SHA256 | % Hash diff --git a/tests/Update-Package.Streams.Tests.ps1 b/tests/Update-Package.Streams.Tests.ps1 new file mode 100644 index 00000000..66a92ad6 --- /dev/null +++ b/tests/Update-Package.Streams.Tests.ps1 @@ -0,0 +1,371 @@ +remove-module AU -ea ignore +import-module $PSScriptRoot\..\AU -force + +Describe 'Update-Package using streams' -Tag updatestreams { + $saved_pwd = $pwd + + function global:get_latest([string] $Version, [string] $URL32, [string] $Checksum32) { + $streams = @{ + '1.4' = @{ Version = '1.4-beta1'; URL32 = 'test.1.4-beta1' } + '1.3' = @{ Version = '1.3.1'; URL32 = 'test.1.3.1' } + '1.2' = @{ Version = '1.2.4'; URL32 = 'test.1.2.4' } + } + if ($Version) { + $stream = (ConvertTo-AUVersion $Version).ToString(2) + if (!$URL32) { + $URL32 = if ($streams.$stream) { $streams.$stream.URL32 } else { "test.$Version" } + } + $streams.Remove($stream) + $s = @{ Version = $Version; URL32 = $URL32 } + if ($Checksum32) { $s += @{ Checksum32 = $Checksum32 } } + $streams.Add($stream, $s) + } + $command = "function global:au_GetLatest { @{ Fake = 1; Streams = [ordered] @{`n" + foreach ($item in ($streams.Keys| sort { ConvertTo-AUVersion $_ } -Descending)) { + $command += "'$item' = @{Version = '$($streams.$item.Version)'; URL32 = '$($streams.$item.URL32)'" + if ($streams.$item.Checksum32) { $command += "; Checksum32 = '$($streams.$item.Checksum32)'" } + $command += "}`n" + } + $command += "} } }" + $command | iex + } + + function global:seach_replace() { + "function global:au_SearchReplace { @{} }" | iex + } + + function global:nuspec_file() { [xml](gc TestDrive:\test_package_with_streams\test_package_with_streams.nuspec) } + + function global:json_file() { (gc TestDrive:\test_package_with_streams\test_package_with_streams.json) | ConvertFrom-Json } + + BeforeEach { + cd $TestDrive + rm -Recurse -Force TestDrive:\test_package_with_streams -ea ignore + cp -Recurse -Force $PSScriptRoot\test_package_with_streams TestDrive:\test_package_with_streams + cd $TestDrive\test_package_with_streams + + $global:au_Timeout = 100 + $global:au_Force = $false + $global:au_IncludeStream = '' + $global:au_NoHostOutput = $true + $global:au_NoCheckUrl = $true + $global:au_NoCheckChocoVersion = $true + $global:au_ChecksumFor = 'none' + $global:au_WhatIf = $false + $global:au_NoReadme = $false + + rv -Scope global Latest -ea ignore + 'BeforeUpdate', 'AfterUpdate' | % { rm "Function:/au_$_" -ea ignore } + get_latest + seach_replace + } + + InModuleScope AU { + + Context 'Updating' { + + It 'can set description from README.md' { + $readme = 'dummy readme & test' + '','', $readme | Out-File $TestDrive\test_package_with_streams\README.md + $res = update + + $res.Result -match 'Setting package description from README.md' | Should Be $true + (nuspec_file).package.metadata.description.InnerText.Trim() | Should Be $readme + } + + It 'can set stream specific descriptions from README.md' { + get_latest -Version 1.4.0 + + $readme = 'dummy readme & test: ' + function au_BeforeUpdate { param([AUPackage] $package) + '','', ($readme + $package.RemoteVersion) | Out-File $TestDrive\test_package_with_streams\README.md + } + function au_AfterUpdate { param([AUPackage] $package) + $package.NuspecXml.package.metadata.description.InnerText.Trim() | Should Be ($readme + $package.RemoteVersion) + } + + $res = update + $res.Result -match 'Setting package description from README.md' | Should Not BeNullOrEmpty + } + + It 'does not set description from README.md with NoReadme parameter' { + $readme = 'dummy readme & test' + '','', $readme | Out-File $TestDrive\test_package_with_streams\README.md + $res = update -NoReadme + + $res.Result -match 'Setting package description from README.md' | Should BeNullOrEmpty + (nuspec_file).package.metadata.description | Should Be 'This is a test package with streams for Pester' + } + + It 'can backup and restore using WhatIf' { + get_latest -Version 1.2.3 + $global:au_Force = $true + $global:au_Version = '1.0' + $global:au_WhatIf = $true + $res = update -ChecksumFor 32 6> $null + + $res.Updated | Should Be $true + $res.RemoteVersion | Should Be '1.0' + (nuspec_file).package.metadata.version | Should Be 1.2.3 + (json_file).'1.2' | Should Be 1.2.3 + (json_file).'1.3' | Should Be 1.3.1 + (json_file).'1.4' | Should Be 1.4-beta1 + } + + It 'can let user override the version of the latest stream' { + get_latest -Version 1.2.3 + $global:au_Force = $true + $global:au_Version = '1.0' + + $res = update -ChecksumFor 32 6> $null + + $res.Updated | Should Be $true + $res.RemoteVersion | Should Be '1.0' + (json_file).'1.2' | Should Be 1.2.3 + (json_file).'1.3' | Should Be 1.3.1 + (json_file).'1.4' | Should Be 1.0 + } + + It 'can let user override the version of a specific stream' { + get_latest -Version 1.2.3 + $global:au_Force = $true + $global:au_IncludeStream = '1.2' + $global:au_Version = '1.0' + + $res = update -ChecksumFor 32 6> $null + + $res.Updated | Should Be $true + $res.RemoteVersion | Should Be '1.0' + (json_file).'1.2' | Should Be 1.0 + (json_file).'1.3' | Should Be 1.3.1 + (json_file).'1.4' | Should Be 1.4-beta1 + } + + It 'automatically verifies the checksum' { + $choco_path = gcm choco.exe | % Source + $choco_hash = Get-FileHash $choco_path -Algorithm SHA256 | % Hash + + get_latest -Version 1.2.4 -URL32 $choco_path -Checksum32 $choco_hash + + $res = update -ChecksumFor 32 6> $null + $res.Result -match 'hash checked for 32 bit version' | Should Be $true + } + + It 'automatically calculates the checksum' { + update -ChecksumFor 32 -IncludeStream 1.2 6> $null + + $global:Latest.Checksum32 | Should Not BeNullOrEmpty + $global:Latest.ChecksumType32 | Should Be 'sha256' + $global:Latest.Checksum64 | Should BeNullOrEmpty + $global:Latest.ChecksumType64 | Should BeNullOrEmpty + } + + It 'updates package when remote version is higher' { + $res = update + + $res.Updated | Should Be $true + $res.Streams.'1.2'.RemoteVersion | Should Be 1.2.4 + $res.Streams.'1.3'.RemoteVersion | Should Be 1.3.1 + $res.Streams.'1.4'.RemoteVersion | Should Be 1.4-beta1 + $res.Result[-1] | Should Be 'Package updated' + (nuspec_file).package.metadata.version | Should Be 1.2.4 + (json_file).'1.2' | Should Be 1.2.4 + (json_file).'1.3' | Should Be 1.3.1 + (json_file).'1.4' | Should Be 1.4-beta1 + } + + It 'updates package when multiple remote versions are higher' { + get_latest -Version 1.4.0 + + $res = update + + $res.Updated | Should Be $true + $res.Streams.'1.2'.RemoteVersion | Should Be 1.2.4 + $res.Streams.'1.3'.RemoteVersion | Should Be 1.3.1 + $res.Streams.'1.4'.RemoteVersion | Should Be 1.4.0 + $res.Result[-1] | Should Be 'Package updated' + (json_file).'1.2' | Should Be 1.2.4 + (json_file).'1.3' | Should Be 1.3.1 + (json_file).'1.4' | Should Be 1.4.0 + } + + It "does not update the package when remote version is not higher" { + get_latest -Version 1.2.3 + + $res = update + + $res.Updated | Should Be $false + $res.Streams.'1.2'.RemoteVersion | Should Be 1.2.3 + $res.Streams.'1.3'.RemoteVersion | Should Be 1.3.1 + $res.Streams.'1.4'.RemoteVersion | Should Be 1.4-beta1 + (nuspec_file).package.metadata.version | Should Be 1.2.3 + (json_file).'1.2' | Should Be 1.2.3 + (json_file).'1.3' | Should Be 1.3.1 + (json_file).'1.4' | Should Be 1.4-beta1 + } + + It "throws an error when forcing update whithout specifying a stream" { + get_latest -Version 1.2.3 + { update -Force -IncludeStream 1.2,1.4 } | Should Throw 'A single stream must be included when forcing package update' + } + + It "updates the package when forced using choco fix notation" { + get_latest -Version 1.2.3 + + $res = update -Force -IncludeStream 1.2 + + $d = (get-date).ToString('yyyyMMdd') + $res.Updated | Should Be $true + $res.Result[-1] | Should Be 'Package updated' + $res.Result -match 'No new version found, but update is forced' | Should Not BeNullOrEmpty + (nuspec_file).package.metadata.version | Should Be "1.2.3.$d" + (json_file).'1.2' | Should Be "1.2.3.$d" + (json_file).'1.3' | Should Be 1.3.1 + (json_file).'1.4' | Should Be 1.4-beta1 + } + + It "does not use choco fix notation if the package remote version is higher" { + $res = update -Force -IncludeStream 1.2 + + $res.Updated | Should Be $true + $res.Streams.'1.2'.RemoteVersion | Should Be 1.2.4 + (nuspec_file).package.metadata.version | Should Be 1.2.4 + (json_file).'1.2' | Should Be 1.2.4 + (json_file).'1.3' | Should Be 1.3.1 + (json_file).'1.4' | Should Be 1.4-beta1 + } + + It "searches and replaces given file lines when updating" { + function global:au_SearchReplace { + @{ + 'test_package_with_streams.nuspec' = @{ + '()(.*)()' = "`$1test_package_with_streams.$($Latest.Version)`$3" + } + } + } + + update + + $nu = (nuspec_file).package.metadata + $nu.releaseNotes | Should Be 'test_package_with_streams.1.2.4' + $nu.id | Should Be 'test_package_with_streams' + $nu.version | Should Be 1.2.4 + } + } + + Context 'Json file' { + + It 'loads a json file from the package directory' { + { update } | Should Not Throw + } + + It "uses version 0.0 if it can't find the json file in the current directory" { + rm *.json + update *> $null + $global:Latest.NuspecVersion | Should Be '0.0' + } + + It "uses version 0.0 on invalid json version" { + $streams = json_file + $streams.'1.2' = '{{PackageVersion}}' + $streams | ConvertTo-Json | Set-Content "$TestDrive\test_package_with_streams\test_package_with_streams.json" -Encoding UTF8 + + update -IncludeStream 1.2 *> $null + + $global:Latest.NuspecVersion | Should Be '0.0' + } + + It "uses version 0.0 when a new stream is available" { + get_latest -Version 1.5.0 + update *> $null + $global:Latest.NuspecVersion | Should Be '0.0' + } + + It "does not update the package when stream is ignored in json file" { + $streams = json_file + $streams.'1.2' = 'ignore' + $streams | ConvertTo-Json | Set-Content "$TestDrive\test_package_with_streams\test_package_with_streams.json" -Encoding UTF8 + + $res = update + + $res.Updated | Should Be $false + } + } + + Context 'au_GetLatest' { + + It "throws if au_GetLatest doesn't return OrderedDictionary or HashTable for streams" { + $return_value = @(1) + function global:au_GetLatest { @{ Streams = $return_value } } + { update } | Should Throw "doesn't return an OrderedDictionary or HashTable" + $return_value = @() + { update } | Should Throw "returned nothing" + } + + It "supports properties defined outside streams" { + get_latest -Version 1.4.0 + function au_BeforeUpdate { $global:Latest.Fake | Should Be 1 } + update + } + + It 'supports alphabetical streams' { + $return_value = @{ + dev = @{ Version = '1.4.0' } + beta = @{ Version = '1.3.1' } + stable = @{ Version = '1.2.4' } + } + function global:au_GetLatest { @{ Streams = $return_value } } + + $res = update + + $res.Updated | Should Be $true + $res.Result[-1] | Should Be 'Package updated' + (json_file).stable | Should Be 1.2.4 + (json_file).beta | Should Be 1.3.1 + (json_file).dev | Should Be 1.4.0 + } + } + + Context 'Before and after update' { + It 'calls au_BeforeUpdate if package is updated' { + function au_BeforeUpdate { $global:Latest.test = 1 } + update -IncludeStream 1.2 + $global:Latest.test | Should Be 1 + } + + It 'calls au_AfterUpdate if package is updated' { + function au_AfterUpdate { $global:Latest.test = 1 } + update -IncludeStream 1.2 + $global:Latest.test | Should Be 1 + } + + It 'doesnt call au_BeforeUpdate if package is not updated' { + get_latest -Version 1.2.3 + function au_BeforeUpdate { $global:Latest.test = 1 } + update -IncludeStream 1.2 + $global:Latest.test | Should BeNullOrEmpty + } + + It 'does not change type of $Latest.Version when calling au_BeforeUpdate and au_AfterUpdate' { + $return_value = @{ + '1.4' = @{ Version = ConvertTo-AUVersion '1.4-beta1' } + '1.2' = @{ Version = '1.2.4' } + '1.3' = @{ Version = [version] '1.3.1' } + } + function global:au_GetLatest { @{ Streams = $return_value } } + function checkLatest { + $return_latest = $return_value[$global:Latest.Stream] + $return_latest.Keys | % { + $global:Latest[$_] | Should BeOfType $return_latest[$_].GetType() + $global:Latest[$_] | Should BeExactly $return_latest[$_] + } + } + function au_BeforeUpdate { checkLatest } + function au_BeforeUpdate { checkLatest } + update + } + } + } + + cd $saved_pwd +} diff --git a/tests/Update-Package.Tests.ps1 b/tests/Update-Package.Tests.ps1 index 5e70aa9d..a9fb048f 100644 --- a/tests/Update-Package.Tests.ps1 +++ b/tests/Update-Package.Tests.ps1 @@ -48,7 +48,7 @@ Describe 'Update-Package' -Tag update { (nuspec_file).package.metadata.description.InnerText.Trim() | Should Be $readme } - It 'deesnt set description from README.md with NoReadme parameter' { + It 'does not set description from README.md with NoReadme parameter' { $readme = 'dummy readme & test' '','', $readme | Out-File $TestDrive\test_package\README.md $res = update -NoReadme @@ -254,7 +254,7 @@ Describe 'Update-Package' -Tag update { update *> $null - $global:Latest.NuspecVersion | Should Be 0.0 + $global:Latest.NuspecVersion | Should Be '0.0' } } @@ -277,6 +277,51 @@ Describe 'Update-Package' -Tag update { function global:au_GetLatest { throw 'test' } { update } | Should Throw "test" } + + It 'checks values in $Latest when entering au_GetLatest' { + function global:au_GetLatest { + $Latest.Count | Should Be 1 + $Latest.PackageName | Should Be 'test_package' + @{ Version = '1.2' } + } + update + } + + It 'supports returning custom values' { + function global:au_GetLatest { @{ Version = '1.2'; NewValue = 1 } } + update + $global:Latest.NewValue | Should Be 1 + } + + It 'supports adding values to $global:Latest' { + function global:au_GetLatest { $global:Latest += @{ NewValue = 1 }; @{ Version = '1.2' } } + update + $global:Latest.NewValue | Should Be 1 + } + + It 'supports adding values to $Latest' { + function global:au_GetLatest { $Latest.NewValue = 1; @{ Version = '1.2' } } + update + $global:Latest.NewValue | Should Be 1 + } + + $testCases = @( + @{ Version = '1.2'; Type = [string] } + @{ Version = [AUVersion] '1.2'; Type = [AUVersion] } + @{ Version = [version] '1.2'; Type = [version] } + @{ Version = [regex]::Match('1.2', '^(.+)$').Groups[1]; Type = [string] } + ) + + It 'supports various Version types' -TestCases $testCases { param($Version) + function global:au_GetLatest { @{ Version = $Version } } + { update } | Should Not Throw + } + + It 'supports various Version types when forcing update' -TestCases $testCases { param($Version, $Type) + function global:au_GetLatest { @{ Version = $Version } } + function global:au_BeforeUpdate { $Latest.Version | Should BeOfType $Type } + { update -Force } | Should Not Throw + } } Context 'Before and after update' { diff --git a/tests/test_package_with_streams/test_package_with_streams.json b/tests/test_package_with_streams/test_package_with_streams.json new file mode 100644 index 00000000..b5ac88e4 --- /dev/null +++ b/tests/test_package_with_streams/test_package_with_streams.json @@ -0,0 +1,8 @@ +{ + "1.2": "1.2.3", + "1.3": "1.3.1", + "1.4": "1.4-beta1", + "stable": "1.2.3", + "beta": "1.3.1", + "dev": "1.4-beta1" +} diff --git a/tests/test_package_with_streams/test_package_with_streams.nuspec b/tests/test_package_with_streams/test_package_with_streams.nuspec new file mode 100644 index 00000000..87eb8634 --- /dev/null +++ b/tests/test_package_with_streams/test_package_with_streams.nuspec @@ -0,0 +1,23 @@ + + + + test_package_with_streams + 1.2.3 + Test Package with Streams + Miodrag Milić + Miodrag Milić + http://www.gnu.org/copyleft/gpl.html + https://github.com/majkinetor/au + false + This is a test package with streams for Pester + This is a test package with streams for Pester + test streams pester + + + + None + + + + + diff --git a/tests/test_package_with_streams/tools/chocolateyInstall.ps1 b/tests/test_package_with_streams/tools/chocolateyInstall.ps1 new file mode 100644 index 00000000..f80c3115 --- /dev/null +++ b/tests/test_package_with_streams/tools/chocolateyInstall.ps1 @@ -0,0 +1,12 @@ +$ErrorActionPreference = 'Stop' + +$packageName = 'test_package_with_streams' +$url32 = gcm choco.exe | % Source +$checksum32 = '' + +$params = @{ + packageName = $packageName + fileFullPath = "$PSScriptRoot\choco.exe" + Url = "file:///$url32" +} +Get-ChocolateyWebFile @params diff --git a/tests/test_package_with_streams/update.ps1 b/tests/test_package_with_streams/update.ps1 new file mode 100644 index 00000000..225892a5 --- /dev/null +++ b/tests/test_package_with_streams/update.ps1 @@ -0,0 +1,13 @@ +function global:au_SearchReplace() { + @{} +} + +function global:au_GetLatest() { + @{ Streams = [ordered] @{ + '1.4' = @{ Version = '1.4-beta1' } + '1.3' = @{ Version = '1.3.1' } + '1.2' = @{ Version = '1.2.3' } + } } +} + +update