diff --git a/AU/Plugins/Gist.ps1 b/AU/Plugins/Gist.ps1 index e53fb5a0..ceb35e0c 100644 --- a/AU/Plugins/Gist.ps1 +++ b/AU/Plugins/Gist.ps1 @@ -2,12 +2,10 @@ # Last Change: 10-Nov-2016. <# .SYNOPSIS - Create update history as markdown report using git commit log. + Upload files to Github gist platform. .DESCRIPTION - Shows one date per line and all of the packages pushed to the Chocolatey community repository during that day. - First letter of the package name links to report (produced by the Report plugin), the rest links to actuall - commit (produced by the Git plugin). + Plugin uploads one or more local files to the gist with the given id #> param( $Info, @@ -40,6 +38,10 @@ ls $Path | % { } # request + +#https://github.com/majkinetor/au/issues/142 +[System.Net.ServicePointManager]::SecurityProtocol = 3072 -bor 768 -bor [System.Net.SecurityProtocolType]::Tls -bor [System.Net.SecurityProtocolType]::Ssl3 + $uri = 'https://api.github.com/gists' $params = @{ ContentType = 'application/json' @@ -48,11 +50,15 @@ $params = @{ Body = $gist | ConvertTo-Json UseBasicparsing = $true } + +#$params | ConvertTo-Json -Depth 100 | Write-Host + if ($ApiKey) { $params.Headers = @{ Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($ApiKey)) } } + $res = iwr @params #https://api.github.com/gists/a700c70b8847b29ebb1c918d47ee4eb1/211bac4dbb707c75445533361ad12b904c593491 diff --git a/AU/Plugins/Git.ps1 b/AU/Plugins/Git.ps1 index e18b2417..67e6c7d4 100644 --- a/AU/Plugins/Git.ps1 +++ b/AU/Plugins/Git.ps1 @@ -12,8 +12,18 @@ param( # Git password. You can use Github Token here if you omit username. [string] $Password, - #Force git commit when package is updated but not pushed. - [switch] $Force + # Force git commit when package is updated but not pushed. + [switch] $Force, + + # Commit strategy: + # single - 1 commit with all packages + # atomic - 1 commit per package + # atomictag - 1 commit and tag per package + [ValidateSet('single', 'atomic', 'atomictag')] + [string]$commitStrategy = 'atomic', + + # Branch name + [string]$Branch = 'master' ) [array]$packages = if ($Force) { $Info.result.updated } else { $Info.result.pushed } @@ -39,38 +49,44 @@ if ($User -and $Password) { } Write-Host "Executing git pull" -git checkout -q master -git pull -q origin master - -Write-Host "Adding updated packages to git repository: $( $packages | % Name)" -$packages | % { git add -u $_.Path } -git status - -Write-Host "Commiting" -$message = "AU: $($packages.Length) updated - $($packages | % Name)" -$gist_url = $Info.plugin_results.Gist -split '\n' | select -Last 1 -git commit -m "$message`n[skip ci] $gist_url" --allow-empty - -Write-Host "Pushing Master" -git push -q - -Write-Host "Merging master -> dev" -git checkout -q dev -git pull -q origin dev -git merge -Xtheirs -q master -git status - -Write-Host "Pushing Dev" -git push -q - -Write-Host "Merging dev -> staging" -git checkout -q staging -git pull -q origin staging -git merge -Xtheirs -q dev -git status - -Write-Host "Pushing Staging" -git push -q +git checkout -q $Branch +git pull -q origin $Branch + + +if ($commitStrategy -like 'atomic*') { + $packages | % { + Write-Host "Adding update package to git repository: $($_.Name)" + git add -u $_.Path + git status + + Write-Host "Commiting $($_.Name)" + $message = "AU: $($_.Name) upgraded from $($_.NuspecVersion) to $($_.RemoteVersion)" + $gist_url = $Info.plugin_results.Gist -split '\n' | select -Last 1 + $snippet_url = $Info.plugin_results.Snippet -split '\n' | select -Last 1 + git commit -m "$message`n[skip ci] $gist_url $snippet_url" --allow-empty + + if ($commitStrategy -eq 'atomictag') { + $tagcmd = "git tag -a $($_.Name)-$($_.RemoteVersion) -m '$($_.Name)-$($_.RemoteVersion)'" + Invoke-Expression $tagcmd + } + } +} +else { + Write-Host "Adding updated packages to git repository: $( $packages | % Name)" + $packages | % { git add -u $_.Path } + git status + Write-Host "Commiting" + $message = "AU: $($packages.Length) updated - $($packages | % Name)" + $gist_url = $Info.plugin_results.Gist -split '\n' | select -Last 1 + $snippet_url = $Info.plugin_results.Snippet -split '\n' | select -Last 1 + git commit -m "$message`n[skip ci] $gist_url $snippet_url" --allow-empty +} +Write-Host "Pushing changes" +git push -q +if ($commitStrategy -eq 'atomictag') { + write-host 'Atomic Tag Push' + git push -q --tags +} popd diff --git a/AU/Plugins/GitReleases.ps1 b/AU/Plugins/GitReleases.ps1 new file mode 100644 index 00000000..fd76babf --- /dev/null +++ b/AU/Plugins/GitReleases.ps1 @@ -0,0 +1,163 @@ +# Author: Kim Nordmo +# Last Change: 29-Oct-2017. + +<# +.SYNOPSIS + Creates Github release for updated packages +#> +param( + $Info, + + # Github API token to use when creating/checking releases and uploading artifacts + [string]$ApiToken, + + # What kind of release should be created, either 1 release per date, or 1 release per package and version is supported. + [ValidateSet('date', 'package')] + [string]$releaseType, + + # The text that should be used in the header of the release. + [string]$releaseHeader = $null, + + # The text that should be used in the description of the release. + [string]$releaseDescription = $null, + + # The formatting to use when replacing in release header/description and on date based releases. + [string]$dateFormat = '{0:yyyy-MM-dd}', + + # Force creating a release when a package have been updated and not just been pushed. + [switch]$Force +) + +function GetOrCreateRelease() { + param( + [string]$tagName, + [string]$releaseName, + [string]$releaseDescription, + [string]$repository, + $headers) + + try { + Write-Verbose "Checking for a release using the tag: $tagName..." + $response = Invoke-RestMethod -UseBasicParsing -Uri "https://api.github.com/repos/$repository/releases/tags/$tagName" -Headers $headers | ? tag_name -eq $tagName + if ($response) { + return $response + } + } + catch { + } + + $json = @{ + "tag_name" = $tagName + "target_commitish" = "master" + "name" = $releaseName + "body" = $releaseDescription + "draft" = $false + "prerelease" = $false + } | ConvertTo-Json -Compress + + 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\/([^\/]+\/[^\/\.]+)")) { + Write-Warning "Unable to parse the repository information, skipping..." + return; +} +$repository = $Matches[1] + +$headers = @{ + Authorization = "token $ApiToken" +} + +if ($releaseType -eq 'date' -and !$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 ' +} + +$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 + } +} + +$uploadHeaders = $headers.Clone() +$uploadHeaders['Content-Type'] = 'application/zip' + +$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 + } + + $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/Plugins/Gitter.ps1 b/AU/Plugins/Gitter.ps1 new file mode 100644 index 00000000..3387b34d --- /dev/null +++ b/AU/Plugins/Gitter.ps1 @@ -0,0 +1,44 @@ +# Author: Kim Nordmo +# Last Change: 2018-06-13 +<# +.SYNOPSIS + Publishes the package update status to gitter. + +.PARAMETER WebHookUrl + This is the cusotm webhook url created through gitter integrations. + +.PARAMETER MessageFormat + The format of the message that is meant to be published on gitter. + {0} = The total number of automated packages. + {1} = The number of updated packages, + {2} = The number of published packages. + {3} = The number of failed packages. + {4} = The url to the github gist. +#> +param( + $Info, + [string]$WebHookUrl, + [string]$MessageFormat = "[Update Status:{0} packages.`n {1} updated, {2} Published, {3} Failed]({4})" +) + +if (!$WebHookUrl) { return } # If we don't have a webhookurl we can't push status messages, so ignore. + +$updatedPackages = @($Info.result.updated).Count +$publishedPackages = @($Info.result.pushed).Count +$failedPackages = $Info.error_count.total +$gistUrl = $Info.plugin_results.Gist -split '\n' | select -Last 1 +$packageCount = $Info.result.all.Length + +$gitterMessage = ($MessageFormat -f $packageCount, $updatedPackages, $publishedPackages, $failedPackages, $gistUrl) + +$arguments = @{ + Body = if ($failedPackages -gt 0) { "message=$gitterMessage&level=error" } else { "message=$gitterMessage" } + UseBasicParsing = $true + Uri = $WebHookUrl + ContentType = 'application/x-www-form-urlencoded' + Method = 'Post' +} + +"Submitting message to gitter" +Invoke-RestMethod @arguments +"Message submitted to gitter" \ No newline at end of file diff --git a/AU/Plugins/History.ps1 b/AU/Plugins/History.ps1 index 86d788b9..0404dc5d 100644 --- a/AU/Plugins/History.ps1 +++ b/AU/Plugins/History.ps1 @@ -25,7 +25,7 @@ param( Write-Host "Saving history to $Path" -$res=[ordered]@{} +$res=[System.Collections.Specialized.OrderedDictionary]@{} $log = git --no-pager log -q --grep '^AU: ' --date iso | Out-String $all_commits = $log | sls 'commit(.|\n)+?(?=\ncommit )' -AllMatches foreach ($commit in $all_commits.Matches.Value) { @@ -50,7 +50,7 @@ foreach ($commit in $all_commits.Matches.Value) { $res.$date += $packages_md } -$res = $res.Keys | select -First $Lines | % { $r=[ordered]@{} } { $r[$_] = $res[$_] } {$r} +$res = $res.Keys | select -First $Lines | % { $r=[System.Collections.Specialized.OrderedDictionary]@{} } { $r[$_] = $res[$_] } {$r} $history = @" # Update History diff --git a/AU/Plugins/Mail.ps1 b/AU/Plugins/Mail.ps1 index 727faca7..506d86b6 100644 --- a/AU/Plugins/Mail.ps1 +++ b/AU/Plugins/Mail.ps1 @@ -4,6 +4,7 @@ param( $Info, [string] $To, + [string] $From, [string] $Server, [string] $UserName, [string] $Password, @@ -20,10 +21,11 @@ if (($Info.error_count.total -eq 0) -and !$SendAlways) { return } -$errors_word = if ($Info.error_count.total -eq 1) {'error'} else {'errors' } +$errors_word = if ($Info.error_count.total -eq 1) { 'error' } else { 'errors' } # Create mail message -$from = "Update-AUPackages@{0}.{1}" -f $Env:UserName, $Env:ComputerName + +if (!$From) { $From = "Update-AUPackages@{0}.{1}" -f $Env:UserName, $Env:ComputerName } $msg = New-Object System.Net.Mail.MailMessage $from, $To $msg.IsBodyHTML = $true diff --git a/AU/Plugins/Report/markdown.ps1 b/AU/Plugins/Report/markdown.ps1 index 5d552f40..eb7d4b2d 100644 --- a/AU/Plugins/Report/markdown.ps1 +++ b/AU/Plugins/Report/markdown.ps1 @@ -64,7 +64,7 @@ if ($Info.error_count.total) { if ($Info.result.ignored) { md_title Ignored - md_table $Info.result.ignored -Columns 'Icon', 'Updated', 'Name', 'NuspecVersion', 'IgnoreMessage' + md_table $Info.result.ignored -Columns 'Icon', 'Name', 'NuspecVersion', 'IgnoreMessage' } if ($Info.result.ok) { diff --git a/AU/Plugins/Report/markdown_funcs.ps1 b/AU/Plugins/Report/markdown_funcs.ps1 index 2fa81294..a0be5350 100644 --- a/AU/Plugins/Report/markdown_funcs.ps1 +++ b/AU/Plugins/Report/markdown_funcs.ps1 @@ -31,6 +31,8 @@ function md_table($result, $Columns, $MaxErrorLength=150) { E={ $r = "[{0}](#{1})" -f $_.Updated, $_.Name.Replace('.','').ToLower() $r += if ($_.Updated) { ' 🔸' } + $r += if ($_.Streams) { ' 🕄' } + $r += if (ls $_.Path -Recurse -Include VERIFICATION.txt) { ' 📥' } $r } }, diff --git a/AU/Plugins/Report/text.ps1 b/AU/Plugins/Report/text.ps1 index c6ed671e..9009d4f3 100644 --- a/AU/Plugins/Report/text.ps1 +++ b/AU/Plugins/Report/text.ps1 @@ -3,7 +3,7 @@ $Title = if ($Params.Title) { $Params.Title } else { 'Update-AUPackages' #============================================================================== -function title($txt) { "r`n{0}`r`n{1}`r`n" -f $txt,('-'*$txt.Length) } +function title($txt) { "`r`n{0}`r`n{1}`r`n" -f $txt,('-'*$txt.Length) } function indent($txt, $level=4) { $txt -split "`n" | % { ' '*$level + $_ } } $now = $Info.startTime.ToUniversalTime().ToString('yyyy-MM-dd HH:mm') @@ -31,7 +31,7 @@ if ($Info.pushed) { $Info.result.pushed | select 'Name', 'Updated', 'Pushed', 'RemoteVersion', 'NuspecVersion' | ft | Out-String | set r indent $r 2 - $ok | % { $_.Name; indent $_.Result; "" } + $Info.result.pushed | % { $_.Name; indent $_.Result; "" } } if ($Info.error_count.total) { @@ -39,7 +39,7 @@ if ($Info.error_count.total) { $Info.result.errors | select 'Name', 'NuspecVersion', 'Error' | ft | Out-String | set r indent $r 2 - $Info.result.errors | % { $_.Name; ident $_.Error; "" } + $Info.result.errors | % { $_.Name; indent $_.Error; "" } } diff --git a/AU/Plugins/Snippet.ps1 b/AU/Plugins/Snippet.ps1 new file mode 100644 index 00000000..cbbd184b --- /dev/null +++ b/AU/Plugins/Snippet.ps1 @@ -0,0 +1,48 @@ +# Author: dimqua +# Last Change: 11-Oct-2018. +<# +.SYNOPSIS + Upload update history report to Gitlab snippet. + +.DESCRIPTION + Plugin uploads update history report (created by Report plugin) to the snippet with the given id and filename. You can use gitlab.com instance (default) or self-hosted one. +#> +param( + $Info, + + # Snippet id + [string] $Id, + + # Gitlab API Token, create in User Settings -> Access Tokens -> Create personal access token + # Make sure token has 'api' scope. + [string] $ApiToken, + + # File paths to attach to snippet + [string[]] $Path, + + # Snippet file name + [string] $FileName = 'Update-AUPackages.md', + + # GitLab instance's (sub)domain name + [string] $Domain = 'gitlab.com' + +) + +# Create snippet +ls $Path | % { + $file_name = Split-Path $_ -Leaf + $content = gc $_ -Raw + $snippet = '{"content": "' + $content + '"}' + } + +$params = @{ + ContentType = 'application/json' + Method = "PUT" + Uri = "https://$Domain/api/v4/snippets/$Id" + Body = ($snippet | ConvertTo-Json).replace('"{\"content\": \"','{"content": "').replace('\"}"','"') + ', "file_name": "' + $FileName + '"}' + Headers = @{ 'PRIVATE-TOKEN'=$ApiToken } +} + +# Request +$res = Invoke-WebRequest @params +"https://$Domain/snippets/$Id" diff --git a/AU/Private/AUPackage.ps1 b/AU/Private/AUPackage.ps1 index c9c20ac4..7da24181 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 = [System.Collections.Specialized.OrderedDictionary] @{} + $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 = [System.Collections.Specialized.OrderedDictionary] @{} } + if (!$this.Streams.Contains($s)) { $this.Streams.$s = @{} } + if ($this.Streams.$s -ne 'ignore') { $this.Streams.$s.NuspecVersion = $v } + $versions = [System.Collections.Specialized.OrderedDictionary] @{} + $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 = [System.Collections.Specialized.OrderedDictionary] @{} + $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..ac240c09 --- /dev/null +++ b/AU/Private/AUVersion.ps1 @@ -0,0 +1,120 @@ +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 } + $pr = $Matches['prerelease'] + $bm = $Matches['buildMetadata'] + if ($pr -and !$strict) { $pr = $pr.Replace(' ', '.') } + if ($bm -and !$strict) { $bm = $bm.Replace(' ', '.') } + # for now, chocolatey does only support SemVer v1 (no dot separated identifiers in pre-release): + if ($pr -and $strict -and $pr -like '*.*') { return $false } + if ($bm -and $strict -and $bm -like '*.*') { return $false } + if ($pr) { $pr = $pr.Replace('.', '') } + if ($bm) { $bm = $bm.Replace('.', '') } + # + $result.Value = [AUVersion]::new($reference.Value, $pr, $bm) + return $true + } + + hidden static [string] GetPattern([bool] $strict) { + $versionPattern = '(?\d+(?:\.\d+){1,3})' + if ($strict) { + $identifierPattern = "[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*" + return "^$versionPattern(?:-(?$identifierPattern))?(?:\+(?$identifierPattern))?`$" + } else { + $identifierPattern = "[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+| \d+)*" + 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/check_url.ps1 b/AU/Private/check_url.ps1 index 81dd868d..5f42a2f4 100644 --- a/AU/Private/check_url.ps1 +++ b/AU/Private/check_url.ps1 @@ -1,10 +1,10 @@ # Returns nothing if url is valid, error otherwise -function check_url( [string] $Url, [int]$Timeout, $ExcludeType='text/html' ) { +function check_url( [string] $Url, [int]$Timeout, $ExcludeType='text/html', $Options ) { if (!(is_url $Url)) { return "URL syntax is invalid" } try { - $response = request $url $Timeout + $response = request $url $Timeout -Options $Options if ($response.ContentType -like "*${ExcludeType}*") { return "Bad content type '$ExcludeType'" } } catch { 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/Private/request.ps1 b/AU/Private/request.ps1 index 9fab8f09..898b0ead 100644 --- a/AU/Private/request.ps1 +++ b/AU/Private/request.ps1 @@ -1,8 +1,20 @@ -function request( [string]$Url, [int]$Timeout ) { +function request( [string]$Url, [int]$Timeout, $Options ) { if ([string]::IsNullOrWhiteSpace($url)) {throw 'The URL is empty'} $request = [System.Net.WebRequest]::Create($Url) if ($Timeout) { $request.Timeout = $Timeout*1000 } - + + if ($Options.Headers) { + $Options.Headers.Keys | % { + if ([System.Net.WebHeaderCollection]::IsRestricted($_)) { + $key = $_.Replace('-','') + $request.$key = $Options.Headers[$_] + } + else { + $request.Headers.add($_, $Options.Headers[$_]) + } + } + } + $response = $request.GetResponse() $response.Close() $response diff --git a/AU/Public/Get-RemoteChecksum.ps1 b/AU/Public/Get-RemoteChecksum.ps1 index d65f771d..c57aef9d 100644 --- a/AU/Public/Get-RemoteChecksum.ps1 +++ b/AU/Public/Get-RemoteChecksum.ps1 @@ -6,9 +6,9 @@ Download file from internet and calculate its checksum #> -function Get-RemoteChecksum( [string] $Url, $Algorithm='sha256' ) { +function Get-RemoteChecksum( [string] $Url, $Algorithm='sha256', $Headers ) { $fn = [System.IO.Path]::GetTempFileName() - Invoke-WebRequest $Url -OutFile $fn -UseBasicParsing + Invoke-WebRequest $Url -OutFile $fn -UseBasicParsing -Headers $Headers $res = Get-FileHash $fn -Algorithm $Algorithm | % Hash rm $fn -ea ignore return $res.ToLower() diff --git a/AU/Public/Get-RemoteFiles.ps1 b/AU/Public/Get-RemoteFiles.ps1 index dfe5973a..b29884a9 100644 --- a/AU/Public/Get-RemoteFiles.ps1 +++ b/AU/Public/Get-RemoteFiles.ps1 @@ -3,10 +3,10 @@ <# .SYNOPSIS - Get Latest URL32 and/or URL64 into tools directxory. + Get Latest URL32 and/or URL64 into tools directory. .DESCRIPTION - This function will download the binaries pointed to by $Latest.URL32 and $Latest.URL34. + This function will download the binaries pointed to by $Latest.URL32 and $Latest.URL64. The function is used to embed binaries into the Chocolatey package. The function will keep original remote file name but it will add suffix _x32 or _x64. @@ -41,7 +41,7 @@ function Get-RemoteFiles { function name4url($url) { if ($FileNameBase) { return $FileNameBase } $res = $url -split '/' | select -Last 1 -Skip $FileNameSkip - $res -replace '\.[a-zA-Z]+$' + $res -replace '\.[^.]+$' } function ext() { @@ -84,7 +84,7 @@ function Get-RemoteFiles { Write-Host "Downloading to $file_name -" $Latest.Url64 $client.DownloadFile($Latest.URL64, $file_path) $global:Latest.Checksum64 = Get-FileHash $file_path -Algorithm $Algorithm | % Hash - $global:Latest.ChecksumType32 = $Algorithm + $global:Latest.ChecksumType64 = $Algorithm $global:Latest.FileName64 = $file_name } } catch{ throw $_ } finally { $client.Dispose() } diff --git a/AU/Public/Get-Version.ps1 b/AU/Public/Get-Version.ps1 new file mode 100644 index 00000000..1171dd73 --- /dev/null +++ b/AU/Public/Get-Version.ps1 @@ -0,0 +1,55 @@ +# Author: Thomas Démoulins + +<# +.SYNOPSIS + Parses a semver-like object from a string in a flexible manner. + +.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 version can be in the middle of a url or sentence + - first version found is returned + - there can be no hyphen between the version and the pre-release + - extra spaces are ignored + - optional delimiters can be provided to help parsing the string + +.EXAMPLE + Get-Version 'Last version: 1.2.3 beta 3.' + + Returns 1.2.3-beta3 + +.EXAMPLE + Get-Version 'https://github.com/atom/atom/releases/download/v1.24.0-beta2/AtomSetup.exe' + + Return 1.24.0-beta2 + +.EXAMPLE + Get-Version 'http://mirrors.kodi.tv/releases/windows/win32/kodi-17.6-Krypton-x86.exe' -Delimiter '-' + + Return 17.6 +#> +function Get-Version { + [CmdletBinding()] + param( + # Version string to parse. + [Parameter(Mandatory=$true)] + [string] $Version, + # Optional delimiter(s) to help locate the version in the string: the version must start and end with one of these chars. + [char[]] $Delimiter + ) + if ($Delimiter) { + $delimiters = $Delimiter -join '' + @('\', ']', '^', '-') | % { $delimiters = $delimiters.Replace($_, "\$_") } + $regex = $Version | Select-String -Pattern "[$delimiters](\d+\.\d+[^$delimiters]*)[$delimiters]" -AllMatches + foreach ($match in $regex.Matches) { + $reference = [ref] $null + if ([AUVersion]::TryParse($match.Groups[1], $reference, $false)) { + return $reference.Value + } + } + } + return [AUVersion]::Parse($Version, $false) +} diff --git a/AU/Public/Push-Package.ps1 b/AU/Public/Push-Package.ps1 index 20ea16e7..aa6c0f05 100644 --- a/AU/Public/Push-Package.ps1 +++ b/AU/Public/Push-Package.ps1 @@ -3,22 +3,29 @@ <# .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'} + $push_url = if ($Env:au_PushUrl) { $Env:au_PushUrl } + else { 'https://push.chocolatey.org' } + + $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 $push_url } } else { - cpush $package.Name --source https://push.chocolatey.org + $packages | % { cpush $_.Name --source $push_url } } } diff --git a/AU/Public/Set-DescriptionFromReadme.ps1 b/AU/Public/Set-DescriptionFromReadme.ps1 new file mode 100644 index 00000000..eedfdd45 --- /dev/null +++ b/AU/Public/Set-DescriptionFromReadme.ps1 @@ -0,0 +1,38 @@ +<# +.SYNOPSIS + Updates nuspec file description from README.md + +.DESCRIPTION + This script should be called in au_AfterUpdate to put the text in the README.md + into description tag of the Nuspec file. The current description will be replaced. + + You need to call this function manually only if you want to pass it custom parameters. + In that case use NoReadme parameter of the Update-Package. + +.EXAMPLE + function global:au_AfterUpdate { Set-DescriptionFromReadme -Package $args[0] -SkipLast 2 -SkipFirst 2 } +#> +function Set-DescriptionFromReadme{ + param( + [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, + # Readme file path + [string] $ReadmePath = 'README.md' + ) + + "Setting package description from $ReadmePath" + + $description = gc $ReadmePath -Encoding UTF8 + $endIdx = $description.Length - $SkipLast + $description = $description | select -Index ($SkipFirst..$endIdx) | Out-String + + $cdata = $Package.NuspecXml.CreateCDataSection($description) + $xml_Description = $Package.NuspecXml.GetElementsByTagName('description')[0] + $xml_Description.RemoveAll() + $xml_Description.AppendChild($cdata) | Out-Null + + $Package.SaveNuspec() +} diff --git a/AU/Public/Update-AUPackages.ps1 b/AU/Public/Update-AUPackages.ps1 index c5a649bf..7d8c391c 100644 --- a/AU/Public/Update-AUPackages.ps1 +++ b/AU/Public/Update-AUPackages.ps1 @@ -1,5 +1,6 @@ -# Author: Miodrag Milic -# Last Change: 02-Dec-2016. + + # Author: Miodrag Milic + # Last Change: 08-May-2018 <# .SYNOPSIS @@ -47,8 +48,10 @@ 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 + NoCheckChocoVersion - Set to true to set NoCheckChocoVersion option for all packages. Plugin - Any HashTable key will be treated as plugin with the same name as the option name. A script with that name will be searched for in the AU module path and user specified path. @@ -81,6 +84,7 @@ function Update-AUPackages { if (!$Options.Force) { $Options.Force = $false } if (!$Options.Push) { $Options.Push = $false } if (!$Options.PluginPath) { $Options.PluginPath = '' } + if (!$Options.NoCheckChocoVersion){ $Options.NoCheckChocoVersion = $false } Remove-Job * -force #remove any previously run jobs @@ -91,6 +95,7 @@ function Update-AUPackages { $aup = Get-AUPackages $Name Write-Host 'Updating' $aup.Length 'automatic packages at' $($startTime.ToString("s") -replace 'T',' ') $(if ($Options.Force) { "(forced)" } else {}) Write-Host 'Push is' $( if ($Options.Push) { 'enabled' } else { 'disabled' } ) + Write-Host 'NoCheckChocoVersion is' $( if ($Options.NoCheckChocoVersion) { 'enabled' } else { 'disabled' } ) if ($Options.Force) { Write-Host 'FORCE IS ENABLED. All packages will be updated' } $script_err = 0 @@ -124,22 +129,25 @@ function Update-AUPackages { $pkg.Ignored = $true $pkg.IgnoreMessage = $pkg.Result[-1] } elseif ($job.State -eq 'Stopped') { - $pkg.Error = "Job termintated due to the $($Options.UpdateTimeout)s UpdateTimeout" + $pkg.Error = "Job terminated due to the $($Options.UpdateTimeout)s UpdateTimeout" } else { $pkg.Error = 'Job returned no object, Vector smash ?' } + } else { + $pkg = [AUPackage]::new($pkg) } - - $message = $pkg.Name + ' ' + $jobseconds = ($job.PSEndTime.TimeOfDay - $job.PSBeginTime.TimeOfDay).TotalSeconds + $message = "[$($p)/$($aup.length)] " + $pkg.Name + ' ' $message += if ($pkg.Updated) { 'is updated to ' + $pkg.RemoteVersion } else { 'has no updates' } if ($pkg.Updated -and $Options.Push) { $message += if (!$pkg.Pushed) { ' but push failed!' } else { ' and pushed'} } if ($pkg.Error) { - $message = "$($pkg.Name) ERROR: " + $message = "[$($p)/$($aup.length)] $($pkg.Name) ERROR: " $message += $pkg.Error.ToString() -split "`n" | % { "`n" + ' '*5 + $_ } } + $message+= " ({0:N2}s)" -f $jobseconds Write-Host ' ' $message $result += $pkg @@ -162,6 +170,38 @@ function Update-AUPackages { $package_name = Split-Path $package_path -Leaf Write-Verbose "Starting $package_name" Start-Job -Name $package_name { #TODO: fix laxxed variables in job for BE and AE + function repeat_ignore([ScriptBlock] $Action) { # requires $Options + $run_no = 0 + $run_max = if ($Options.RepeatOn) { if (!$Options.RepeatCount) { 2 } else { $Options.RepeatCount+1 } } else {1} + + :main while ($run_no -lt $run_max) { + $run_no++ + try { + $res = & $Action 6> $out + break main + } catch { + if ($run_no -ne $run_max) { + foreach ($msg in $Options.RepeatOn) { + if ($_.Exception -notlike "*${msg}*") { continue } + Write-Warning "Repeating $using:package_name ($run_no): $($_.Exception)" + if ($Options.RepeatSleep) { Write-Warning "Sleeping $($Options.RepeatSleep) seconds before repeating"; sleep $Options.RepeatSleep } + continue main + } + } + foreach ($msg in $Options.IgnoreOn) { + if ($_.Exception -notlike "*${msg}*") { continue } + Write-Warning "Ignoring $using:package_name ($run_no): $($_.Exception)" + "AU ignored on: $($_.Exception)" | Out-File -Append $out + $res = 'ignore' + break main + } + $type = if ($res) { $res.GetType() } + if ( "$type" -eq 'AUPackage') { $res.Error = $_ } else { return $_ } + } + } + $res + } + $Options = $using:Options cd $using:package_path @@ -171,42 +211,15 @@ function Update-AUPackages { $global:au_Force = $Options.Force $global:au_WhatIf = $Options.WhatIf $global:au_Result = 'pkg' + $global:au_NoCheckChocoVersion = $Options.NoCheckChocoVersion if ($Options.BeforeEach) { $s = [Scriptblock]::Create( $Options.BeforeEach ) . $s $using:package_name $Options } - $run_no = 0 - $run_max = $Options.RepeatCount - $run_max = if ($Options.RepeatOn) { if (!$Options.RepeatCount) { 2 } else { $Options.RepeatCount+1 } } else {1} - - :main while ($run_no -lt $run_max) { - $run_no++ - $pkg = $null #test double report when it fails - try { - $pkg = ./update.ps1 6> $out - break main - } catch { - if ($run_no -ne $run_max) { - foreach ($msg in $Options.RepeatOn) { - if ($_.Exception -notlike "*${msg}*") { continue } - Write-Warning "Repeating $using:package_name ($run_no): $($_.Exception)" - if ($Options.RepeatSleep) { Write-Warning "Sleeping $($Options.RepeatSleep) seconds before repeating"; sleep $Options.RepeatSleep } - continue main - } - } - foreach ($msg in $Options.IgnoreOn) { - if ($_.Exception -notlike "*${msg}*") { continue } - "AU ignored on: $($_.Exception)" | Out-File -Append $out - $pkg = 'ignore' - break main - } - if ($pkg) { $pkg.Error = $_ } - } - } + $pkg = repeat_ignore { ./update.ps1 } if (!$pkg) { throw "'$using:package_name' update script returned nothing" } - if (($pkg -eq 'ignore') -or ($pkg[-1] -eq 'ignore')) { return 'ignore' } $pkg = $pkg[-1] @@ -214,20 +227,26 @@ 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 - if ($LastExitCode -eq 0) { - $pkg.Pushed = $true - } else { - $pkg.Error = "Push ERROR`n" + ($r | select -skip 1) + $res = repeat_ignore { + $r = Push-Package -All:$Options.PushAll + if ($LastExitCode -eq 0) { return $r } else { throw $r } } - } + if (($res -eq 'ignore') -or ($res[-1] -eq 'ignore')) { return 'ignore' } + if ($res -is [System.Management.Automation.ErrorRecord]) { + $pkg.Error = "Push ERROR`n" + $res + } else { + $pkg.Pushed = $true + $pkg.Result += $res + } + } + if ($Options.AfterEach) { $s = [Scriptblock]::Create( $Options.AfterEach ) . $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 7125cdd4..7fc19e99 100644 --- a/AU/Public/Update-Package.ps1 +++ b/AU/Public/Update-Package.ps1 @@ -45,7 +45,7 @@ } function global:au_GetLatest { - $download_page = Invoke-WebRequest -Uri https://github.com/hluk/CopyQ/releases + $download_page = Invoke-WebRequest https://github.com/hluk/CopyQ/releases -UseBasicParsing $re = "copyq-.*-setup.exe" $url = $download_page.links | ? href -match $re | select -First 1 -expand href @@ -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. @@ -91,15 +96,18 @@ function Update-Package { #Output variable. [string] $Result, - #Backup and restore package - [switch] $WhatIf + #Backup and restore package. + [switch] $WhatIf, + + #Disable automatic update of nuspec description from README.md files with first 2 lines skipped. + [switch] $NoReadme ) function check_urls() { "URL check" | result $Latest.Keys | ? {$_ -like 'url*' } | % { $url = $Latest[ $_ ] - if ($res = check_url $url) { throw "${res}:$url" } else { " $url" | result } + if ($res = check_url $url -Options $Latest.Options) { throw "${res}:$url" } else { " $url" | result } } } @@ -117,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 { '' } @@ -191,40 +199,120 @@ 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)) { + if ( !$au_GalleryUrl ) { $au_GalleryUrl = 'https://chocolatey.org' } + $choco_url = "$au_GalleryUrl/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 | % { $v=$global:Latest[$_]; " {0,-25} {1,-12} {2}" -f $_, "($( if ($v) { $v.GetType().Name } ))", $v }) | 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 @@ -236,6 +324,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 @@ -247,7 +338,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 } @@ -259,20 +350,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 } @@ -303,17 +380,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 } @@ -321,62 +394,83 @@ 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 = [System.Collections.Specialized.OrderedDictionary] @{} + $streams | % { + $stream = $res.Streams[$_] - if ($WhatIf) { $package.Backup() } - try { - if (Test-Path Function:\au_BeforeUpdate) { 'Running au_BeforeUpdate' | result; au_BeforeUpdate | result } - update_files - if (Test-Path Function:\au_AfterUpdate) { 'Running au_AfterUpdate' | result; au_AfterUpdate | 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 + + $allStreams.$_ = if ($package.Streams.$_) { $package.Streams.$_.Clone() } else { @{} } + $allStreams.$_.NuspecVersion = $package.NuspecVersion + $allStreams.$_ += $package.GetStreamDetails() + } + $package.Updated = $false + $package.Streams = $allStreams + $package.Streams.Values | ? { $_.Updated } | % { + $package.NuspecVersion = $_.NuspecVersion + $package.RemoteVersion = $_.RemoteVersion + $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/CHANGELOG.md b/CHANGELOG.md index 28a1a696..30fb6598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,46 @@ ## Next +- `Get-RemoteChecksum`: New parameter `Headers` +- Plugins: + - New plugin: Gitter + - New plugin: Snippet + +### Bugfixes + + - Fixed header handling during request ([#164](https://github.com/majkinetor/au/issues/164)) + - Push errors are not repeated or ignored on ([#175](https://github.com/majkinetor/au/issues/175)) + +## 2018.5.18 + +- `Update-Package`: + - Now you can pass HTTP/HTTPS headers to `$Latest.Options.Headers` to avoid `Unauthorized` errors while checking URLs. + - Package Gallery URL is no longer hard-coded but taken from the `$au_GalleryUrl` if it exists ([#95](https://github.com/majkinetor/au/issues/95)) +- `Update-AUPackages`: Added `NoCheckChocoVersion` option. +- Plugins: + - `Git`: Added `Branch` parameter to specify a branch name + - `Mail`: Added `From` parameter to be used with mail servers that do not allow non-existent email addresses. + +### Bugfixes + +- `Gist` plugin: Security protocol is set according to updated [Github requirements](https://githubengineering.com/crypto-removal-notice). +- `Get-RemoteFiles`: Fixed wrong checksum type being set on 64bit url + +## 2018.1.11 +- `Update-AuPackage` + - New feature [streams](https://github.com/majkinetor/au#streams) that extends `au_GetLatest` with option to return multiple HashTables (one for each stream). + - New parameter `IncludeStream` to force update of specific stream. +- `au_BeforeUpdate` and `au_AfterUpdate` now have parameter `Package` of type `[AUPackage]` which you can use among other things to modify the Nuspec data. +- Added new function `Set-DescriptionFromReadme` that is called automatically when README.md is present in the package folder ([#85](https://github.com/majkinetor/au/issues/85)). See [documentation](README.md#automatic-package-description-from-readmemd). +- Plugins: + - New plugin: [GitReleases](https://github.com/majkinetor/au/blob/master/AU/Plugins/GitReleases.ps1) creates Github release on successifully pushed packages. + - Git: new parameter `Strategy` with options on how to commit repository changes + - Report: symbols in markdown report to mark embedded and stream packages + +## 2017.8.30 + - `Update-AUPackages` - - New options to handle update.ps1 errors: `IgnoreOn`, `RepeatOn`,`RepeatCount`,`RepeatSleep`. See [documentation](https://github.com/majkinetor/au#handling-update-errors). ([#76](https://github.com/majkinetor/au/issues/76)) + - New options to handle update.ps1 errors: `IgnoreOn`, `RepeatOn`,`RepeatCount`,`RepeatSleep`. See [documentation](https://github.com/majkinetor/au#handling-update-errors). ([#76](https://github.com/majkinetor/au/issues/76)). - New option `WhatIf` option that will trigger WhatIf on all packages. - New AUPackage properties: `Ignored` (boolean) and `IgnoreMessage`. - Report plugin: `IgnoreMessage` is added in the ignore section. diff --git a/DEVEL.md b/DEVEL.md index 543eda01..149ff146 100644 --- a/DEVEL.md +++ b/DEVEL.md @@ -45,4 +45,6 @@ $v = ./build.ps1 #create a new version Before publishing, edit the `NEXT` header in the `CHANGELOG.md` file to set the release notes. The publish script will take content of the second level header named after version as the release notes. The publishing will fail if release notes are not found. If that happens, don't forget to edit the file **and commit/push it to repository** in order for next tag to include it. +Chocolatey package description is automatically taken from the README.md section "## Features". + Publishing procedure depends on number of environment variables. Rename `vars_default.ps1` to `vars.ps1` and set variables there to get them included. diff --git a/Plugins.md b/Plugins.md index 154efce3..8a0cc718 100644 --- a/Plugins.md +++ b/Plugins.md @@ -22,6 +22,29 @@ To set up plugin to create gist under your user name you need to give it your gi * To use it locally, just ensure `git push` doesn't require credentials and dont set any environment variables. * To use on build server such as [[AppVeyor]], specify `$Env:username` and `$Env:password`. If you host git repository on Github its preferable to use personal access token. You can use the same token as with gist as long as _**public repo**_ scope is activated. +## [GitReleases](https://github.com/majkinetor/au/blob/master/AU/Plugins/GitReleases.ps1) + +**Creates Github release for updated packages**. + +* It is recommended to add the following line `skip_tags: true` in the `appveyor.yml` file to prevent tags from being built. While it may not be necessary, this is used to prevent packages from being submitted again when `[AU]` or `[PUSH]` is being used in the commit header message. + +## [Gitter](https://github.com/majkinetor/au/blob/master/AU/Plugins/Gitter.ps1) + +**Setup project to submit gitter status** + +* First of all, navigate to the gitter channel you wish to have the status listed (you'll need to have permission to add integrations, and need to do it through the webpage). + 1. Click on the icon for room settings, then select `Integrations`. + 2. Select a `Custom` Integration + 3. Copy the unique webhook url listed in the dialog. + 4. Update your appveyor environment variable with your unique webhook, and set the name to `gitter_webhook`. + 5. Navigate to the `update_all.ps1` file in your repository, and update the `$Options` hashtable with the following + ```powershell + Gitter = @{ + WebHookUrl = $env:gitter_webhook + } + ``` + 6. Enjoy your status updates, or frown on failures. + ## [History](https://github.com/majkinetor/au/blob/master/AU/Plugins/History.ps1) **Create update history as markdown report using git log**. @@ -48,4 +71,15 @@ The plugin saves state of all packages in a file that can be used locally or upl **Save run info to the file and exclude sensitive information**. Run this plugin as the last one to save all other info produced during the run in such way that it can be recreated as object. -To load it for inspection use `$info = Import-CliXml update_info.xml`. \ No newline at end of file +To load it for inspection use `$info = Import-CliXml update_info.xml`. + +## [Snippet](https://github.com/majkinetor/au/blob/master/AU/Plugins/Snippet.ps1) + +**Upload update history report to GitLab snippet**. + +To set up plugin to create snippet under your user name you need to give it your snippet id and authentication: + +* Log into https://gitlab.com/users/sign_in with the user you want to use. +* [Create a snippet](https://gitlab.com/snippets/new) (private or not) with a title and some random content. Grab the id at the end of it - `https://gitlab.com/snippets/{id}`. Set it as `$Env:snippet_id` environment variable. +* Create [GitLab personal access token](https://gitlab.com/profile/personal_access_tokens) and **make sure token has _api_ scope selected**. Authenticating with username and password isn't supported for security reasons. Set it as `$Env:gitlab_api_token` environment variable. + diff --git a/README.md b/README.md index c19ac265..fca7dffc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ -[![build](https://ci.appveyor.com/api/projects/status/github/majkinetor/au?svg=true)](https://ci.appveyor.com/project/majkinetor/au)   [![chat](https://img.shields.io/badge/gitter-join_chat-1dce73.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIwIiB5PSI1IiBmaWxsPSIjZmZmIiB3aWR0aD0iMSIgaGVpZ2h0PSI1Ii8%2BPHJlY3QgeD0iMiIgeT0iNiIgZmlsbD0iI2ZmZiIgd2lkdGg9IjEiIGhlaWdodD0iNyIvPjxyZWN0IHg9IjQiIHk9IjYiIGZpbGw9IiNmZmYiIHdpZHRoPSIxIiBoZWlnaHQ9IjciLz48cmVjdCB4PSI2IiB5PSI2IiBmaWxsPSIjZmZmIiB3aWR0aD0iMSIgaGVpZ2h0PSI0Ii8%2BPC9zdmc%2B&logoWidth=8)](https://gitter.im/chocolatey_au/Lobby)   [![license](https://img.shields.io/badge/license-GPL2-blue.svg)](https://raw.githubusercontent.com/majkinetor/au/master/license.txt) +[![build](https://ci.appveyor.com/api/projects/status/github/majkinetor/au?svg=true)](https://ci.appveyor.com/project/majkinetor/au) [![chat](https://img.shields.io/badge/gitter-join_chat-1dce73.svg?logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIwIiB5PSI1IiBmaWxsPSIjZmZmIiB3aWR0aD0iMSIgaGVpZ2h0PSI1Ii8%2BPHJlY3QgeD0iMiIgeT0iNiIgZmlsbD0iI2ZmZiIgd2lkdGg9IjEiIGhlaWdodD0iNyIvPjxyZWN0IHg9IjQiIHk9IjYiIGZpbGw9IiNmZmYiIHdpZHRoPSIxIiBoZWlnaHQ9IjciLz48cmVjdCB4PSI2IiB5PSI2IiBmaWxsPSIjZmZmIiB3aWR0aD0iMSIgaGVpZ2h0PSI0Ii8%2BPC9zdmc%2B&logoWidth=8)](https://gitter.im/chocolatey_au/Lobby) [![license](https://img.shields.io/badge/license-GPL2-blue.svg)](https://raw.githubusercontent.com/majkinetor/au/master/license.txt) +[![](http://transparent-favicon.info/favicon.ico)](#) +[![](http://transparent-favicon.info/favicon.ico)](#) +[![](http://transparent-favicon.info/favicon.ico)](#) +[![](http://transparent-favicon.info/favicon.ico)](#) +[![](http://transparent-favicon.info/favicon.ico)](#) +[![](http://transparent-favicon.info/favicon.ico)](#) +[![](https://img.shields.io/badge/donate-patreon-blue.svg?longCache=true&style=for-the-badge)](https://www.patreon.com/majkinetor) --- @@ -12,13 +19,17 @@ 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. -- Can use global variables to change functionality. -- Sugar functions for Chocolatey package maintainers. +- Automatically sets the nuspec descriptions from a README.md files. - Update single package or any subset of previously created AU packages with a single command. - Multithread support when updating multiple packages. +- Repeat or ignore specific failures when updating multiple packages. - Plugin system when updating everything, with few integrated plugins to send email notifications, save results to gist and push updated packages to git repository. +- Use of global variables to change functionality. +- Sugar functions for Chocolatey package maintainers. +- Great performance - hundreds of packages can be checked and updated in several minutes. ## Installation @@ -49,7 +60,7 @@ As an example, the following function uses [Invoke-WebRequest](https://technet.m ```powershell function global:au_GetLatest { - $download_page = Invoke-WebRequest -Uri $releases #1 + $download_page = Invoke-WebRequest -Uri $releases -UseBasicParsing #1 $regex = '.exe$' $url = $download_page.links | ? href -match $regex | select -First 1 -expand href #2 $version = $url -split '-|.exe' | select -Last 1 -Skip 2 #3 @@ -59,7 +70,6 @@ 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`. - ### `au_SearchReplace` Function returns HashTable containing search and replace data for any package file in the form: @@ -137,6 +147,33 @@ Package updated This is best understood via the example - take a look at the real life package [installer script](https://github.com/majkinetor/au-packages/blob/master/dngrep/tools/chocolateyInstall.ps1) and its [AU updater](https://github.com/majkinetor/au-packages/blob/master/dngrep/update.ps1). +### Automatic package description from README.md + +If a package directory contains the `README.md` file, its content will be automatically set as description of the package with first 2 lines omitted - you can put on those lines custom content that will not be visible in the package description. + +To disable this option use `-NoReadme` switch with the `Update-Package` function. You can still call it manually from within `au_AfterUpdate`, which you may want to do in order to pass custom parameters to it: + +```powershell +function global:au_AfterUpdate ($Package) { + Set-DescriptionFromReadme $Package -SkipLast 2 -SkipFirst 5 +} +``` + +To extract descriptions from existing packages into README.md files the following script can be used: + +```powershell +ls | ? PSIsContainer | ? { !(Test-Path $_\README.md) } | % { + [xml] $package = gc $_\*.nuspec -ea 0 -Encoding UTF8 + if (!$package) { return } + + $meta = $package.package.metadata + $readme = ('# [{0}](https://chocolatey.org/packages/{0})' -f $meta.id, $meta.iconUrl), '' + $readme += $meta.description -split "`n" | % { $_.Trim() } + $readme -join "`n" | Out-File -Encoding UTF8 $_\README.md + $meta.id +} +``` + ### Checks The `update` function does the following checks: @@ -158,6 +195,9 @@ For some packages, you may want to disable some of the checks by specifying addi ### Automatic checksums +**NOTE**: This feature works by invoking `chocolateyInstall.ps1` of the respective package with a [monkey-patched version of the `Get-ChocolateyWebFile` helper function](https://github.com/majkinetor/au/blob/a8d31244997f08685cc894da4faa1012c60b34f1/AU/Public/Update-Package.ps1#L172). The install script is supposed to either call this function explicitly or indirectly (e.g. `Install-ChocolateyInstallPackage $url`, which calls the former one). +In any case, upon execution of `Get-ChocolateyWebFile`, the install script will be **terminated**. Any actions in your script occurring after the call to `Get-ChocolateyWebFile` will **not** be run. This is due to the nature of [how the function gets monkey-patched](https://github.com/majkinetor/au/blob/a8d31244997f08685cc894da4faa1012c60b34f1/AU/Public/Update-Package.ps1#L172), which might be improved in the future. + When new version is available, the `update` function will by default download both x32 and x64 versions of the installer and calculate the desired checksum. It will inject this info in the `$global:Latest` HashTable variable so you can use it via `au_SearchReplace` function to update hashes. The parameter `ChecksumFor` can contain words `all`, `none`, `32` or `64` to further control the behavior. You can disable this feature by calling update like this: @@ -174,20 +214,17 @@ If the checksum is actually obtained from the vendor's site, you can provide it If the `ChecksumXX` hash key is present, the AU will change to checksum verification mode - it will download the installer and verify that its checksum matches the one provided. If the key is not present, the AU will calculate hash with the given `ChecksumTypeXX` algorithm. -**NOTE**: This feature works by monkey patching the `Get-ChocolateyWebFile` helper function and invoking the `chocolateyInstall.ps1` afterwards for the package in question. This means that it downloads the files using whatever method is specified in the package installation script. - - ### Manual checksums Sometimes invoking `chocolateyInstall.ps1` during the automatic checksum could be problematic so you need to disable it using update option `ChecksumFor none` and get the checksum some other way. Function `Get-RemoteChecksum` can be used to simplify that: ```powershell - function au_BeforeUpdate() { + function global:au_BeforeUpdate() { $Latest.Checksum32 = Get-RemoteChecksum $Latest.Url32 } - function au_GetLatest() { - download_page = Invoke-WebRequest $releases -UseBasicParsing + function global:au_GetLatest() { + $download_page = Invoke-WebRequest $releases -UseBasicParsing $url = $download_page.links | ? href -match '\.exe$' | select -First 1 -expand href $version = $url -split '/' | select -Last 1 -Skip 1 @{ @@ -217,7 +254,7 @@ Updating files ... ``` -Force option changes how package version is used. Without force, the `NuspecVersion` determines what is going on. Normally, if `NuspecVersion` is lower or equal then the `RemoteVersion` update happens. With `Force` this changes: +Force option changes how package version is used. Without force, the `NuspecVersion` determines what is going on. Normally, if `NuspecVersion` is lower then the `RemoteVersion` update happens. With `Force` this changes: 1. If `NuspecVersion` is lower then `RemoteVersion`, Force is ignored and update happens as it would normally 2. If `NuspecVersion` is the same as the `RemoteVersion`, the version will change to chocolatey fix notation. @@ -247,6 +284,8 @@ This is the same as if you added the parameters to `update` function inside the however, its way easier to setup global variable with manual intervention on multiple packages. +There is also a special variable `$au_GalleryUrl` using which you can change the URL that is used to check if package is already pushed. It defaults to https://chocolatey.org and you can change it if you need to this option for 3rd party or internal package repositories. + ### Reusing the AU updater with metapackages Metapackages can reuse an AU updater of its dependency by the following way: @@ -270,7 +309,7 @@ AU function `Get-RemoteFiles` can download files and save them in the package's The following example downloads files inside `au_BeforeUpdate` function which is called before the package files are updated with the latest data (function is not called if no update is available): ```powershell -function au_BeforeUpdate() { +function global:au_BeforeUpdate() { #Download $Latest.URL32 / $Latest.URL64 in tools directory and remove any older installers. Get-RemoteFiles -Purge } @@ -280,6 +319,46 @@ This function will also set the appropriate `$Latest.ChecksumXX`. **NOTE**: There is no need to use automatic checksum when embedding because `Get-RemoteFiles` will do it, so always use parameter `-ChecksumFor none`. +### Streams + +The software vendor may maintain _multiple latest versions_, of specific releases because of the need for long time support. `au_GetLatest` provides an option to return multiple HashTables in order for its user to monitor each supported software _stream_. Prior to AU streams, each software stream was typically treated as a separate package and maintained independently. Using AU streams allows a single package updater to update multiple version streams in a single run: + +```powershell +function global:au_GetLatest { + # ... + @{ + Streams = [ordered] @{ + '1.3' = @{ Version = $version13; URL32 = $url13 } # $version13 = '1.3.9' + '1.2' = @{ Version = $version12; URL32 = $url12 } # $version12 = '1.2.3.1' + } + } +} +``` + +Though a `Hashtable` can be returned for streams, it is recommended to return an `OrderedDictionary` (see above example) that contains streams from the most recent to the oldest one. This ensures that when forcing an update, the most recent stream available will be considered by default (i.e. when no `-IncludeStream` is specified). + +Latest stream versions are kept in the `.json` file in the package directory. For real life example take a look at the [Python3](https://github.com/chocolatey/chocolatey-coreteampackages/blob/master/automatic/python3/update.ps1) package updater which automatically finds available python 3 streams and keeps them [up to date](https://gist.github.com/a14b1e5bfaf70839b338eb1ab7f8226f/78cdc99c2d7433d26c65bc721c26c1cc60ccca3d#python3). + +Streams can be also used to manage multiple related packages as a single package. [LibreOffice](https://github.com/chocolatey/chocolatey-coreteampackages/blob/master/automatic/libreoffice/update.ps1) package updater uses streams to manage [two different](https://gist.github.com/choco-bot/a14b1e5bfaf70839b338eb1ab7f8226f/78cdc99c2d7433d26c65bc721c26c1cc60ccca3d#libreoffice) variants of the software (prior to streams this was handled via 2 packages.) + +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. + +**NOTES**: +- By default only the first updated stream is pushed per run of `updateall`. In order to push all of them add among its options `PushAll = $true`. +- To force the update of the single stream using `IncludeStream` parameter. To do so on via commit message use `[AU package\stream]` syntax. + +```powershell +PS> Get-Version 'v1.3.2.7rc1' + +Version Prerelease BuildMetadata +------- ---------- ------------- +1.3.2.7 rc1 + +PS> $version = Get-Version '1.3.2-beta2+5' +PS> $version.ToString(2) + ' => ' + $version.ToString() +1.3 => 1.3.2-beta2+5 +``` + ### WhatIf If you don't like the fact that AU changes the package inline, or just want to preview changes you can use `$WhatIf` parameter or `$au_WhatIf` global variable: @@ -303,7 +382,7 @@ WARNING: Package restored and updates saved to: C:\Users\majkinetor\AppData\Loca You can update all packages and optionally push them to the Chocolatey repository with a single command. Function `Update-AUPackages` (alias `updateall`) will iterate over `update.ps1` scripts and execute each in a separate thread. If it detects that a package is updated it will optionally try to push it to the Chocolatey repository and may also run configured plugins. -For the push to work, specify your Choocolatey API key in the file `api_key` in the script's directory (or its parent directory) or set the environment variable `$Env:api_key`. If none provided cached nuget key will be used. +For the push to work, specify your Chocolatey API key in the file `api_key` in the script's directory (or its parent directory) or set the environment variable `$Env:api_key`. If none provided cached NuGet key will be used. The function will search for packages in the current directory. To override that, use global variable `$au_Root`: @@ -446,10 +525,20 @@ RepeatCount = 2 #How many times to repeat on - If the same error is both in `RepeatOn` and `IgnoreOn` list, the package will first be repeated and if the error persists, it will be ignored. - The last line returned by the package prior to the word 'ignore' is used as `IgnoreMessage` for that package and shown in reports. +#### Can't validate URL error + +If you encounter `Can't validate URL` error like + +```bash +Can't validate URL +Exception calling "GetResponse" with "0" argument(s): "The remote server returned an error: (401) Unauthorized.": +``` + +you need to pass HTTP/HTTPS headers used for retrieving `url`/`url64bit` to `$Latest.Options.Headers` as `Hashtable`, where key is header name, and value are header itself. This may be `Authorization` or `Referer` header or any others. ## Other functions -Apart from the functions used in the updating process, there are few suggars for regular maintenance of the package: +Apart from the functions used in the updating process, there are few sugars for regular maintenance of the package: - Test-Package Quickly test install and/or uninstall of the package from the current directory with optional parameters. This function can be used to start testing in [chocolatey-test-environment](https://github.com/majkinetor/chocolatey-test-environment) via `Vagrant` parameter. @@ -459,3 +548,10 @@ Push the latest package using your API key. - Get-AuPackages (alias `gau` or `lsau`) Returns the list of the packages which have `update.ps1` script in its directory and which name doesn't start with '_'. + +## Community + +- [Wormies AU Helpers](https://github.com/WormieCorp/Wormies-AU-Helpers) +Helper scripts to make maintaining packages using AU even easier +- [Chocolatey Core Community Maintainers Team Packages](https://github.com/chocolatey/chocolatey-coreteampackages) +The [largest](https://gist.github.com/choco-bot/a14b1e5bfaf70839b338eb1ab7f8226f) repository of AU packages by far 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/publish.ps1 b/publish.ps1 index cad9aee4..356f6e46 100644 --- a/publish.ps1 +++ b/publish.ps1 @@ -54,6 +54,9 @@ function get_release_notes() { } function Publish-Github() { + + [System.Net.ServicePointManager]::SecurityProtocol = 3072 -bor 768 -bor [System.Net.SecurityProtocolType]::Tls -bor [System.Net.SecurityProtocolType]::Ssl3 + if (!$Github) { Write-Host "Github publish disabled."; return } Write-Host 'Publishing to Github' 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..487d64ff --- /dev/null +++ b/tests/Get-Version.Tests.ps1 @@ -0,0 +1,144 @@ +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 + { 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 + } + + $testCases = @( + @{Value = '1.2-beta.3'} + @{Value = '1.2+xyz.4'} + @{Value = '1.2-beta.3+xyz.4'} + ) + + It 'does not convert semver v2' -TestCases $testCases { param($Value, $ExpectedResult) + { [AUVersion] $Value } | Should Throw 'Invalid version' + } + + $testCases = @( + @{ExpectedResult = '5.4.9' ; Delimiter = '-' ; Value = 'http://dl.airserver.com/pc32/AirServer-5.4.9-x86.msi'} + @{ExpectedResult = '1.24.0-beta2' ; Value = 'https://github.com/atom/atom/releases/download/v1.24.0-beta2/AtomSetup.exe'} + @{ExpectedResult = '2.4.0.24-beta' ; Value = 'https://github.com/gurnec/HashCheck/releases/download/v2.4.0.24-beta/HashCheckSetup-v2.4.0.24-beta.exe'} + @{ExpectedResult = '2.0.9' ; Value = 'http://www.ltr-data.se/files/imdiskinst_2.0.9.exe'} + @{ExpectedResult = '17.6' ; Delimiter = '-' ; Value = 'http://mirrors.kodi.tv/releases/windows/win32/kodi-17.6-Krypton-x86.exe'} + @{ExpectedResult = '0.70.2' ; Value = 'https://github.com/Nevcairiel/LAVFilters/releases/download/0.70.2/LAVFilters-0.70.2-Installer.exe'} + @{ExpectedResult = '2.2.0-1' ; Value = 'https://files.kde.org/marble/downloads/windows/Marble-setup_2.2.0-1_x64.exe'} + @{ExpectedResult = '2.3.2' ; Value = 'https://github.com/sabnzbd/sabnzbd/releases/download/2.3.2/SABnzbd-2.3.2-win-setup.exe'} + @{ExpectedResult = '1.9' ; Delimiter = '-' ; Value = 'http://download.serviio.org/releases/serviio-1.9-win-setup.exe'} + @{ExpectedResult = '0.17.0' ; Value = 'https://github.com/Stellarium/stellarium/releases/download/v0.17.0/stellarium-0.17.0-win32.exe'} + @{ExpectedResult = '5.24.3.1' ; Value = 'http://strawberryperl.com/download/5.24.3.1/strawberry-perl-5.24.3.1-32bit.msi'} + @{ExpectedResult = '3.5.4' ; Value = 'https://github.com/SubtitleEdit/subtitleedit/releases/download/3.5.4/SubtitleEdit-3.5.4-Setup.zip'} + # for now, chocolatey does only support SemVer v1 (no dot separated identifiers in pre-release): + @{ExpectedResult = '1.2.3-beta4' ; Value = 'v 1.2.3 beta 4'} + @{ExpectedResult = '1.2.3-beta3' ; Value = 'Last version: 1.2.3 beta 3.'} + # here is the SemVer v2 equivalent: + #@{ExpectedResult = '1.2.3-beta.4' ; Value = 'v 1.2.3 beta 4'} + #@{ExpectedResult = '1.2.3-beta.3' ; Value = 'Last version: 1.2.3 beta 3.'} + ) + + It 'should parse any non strict version' -TestCases $testCases { param($Value, $Delimiter, $ExpectedResult) + $version = Get-Version $Value -Delimiter $Delimiter + $version | Should Be ([AUVersion] $ExpectedResult) + } + } +} diff --git a/tests/Update-AUPackages.Streams.Tests.ps1 b/tests/Update-AUPackages.Streams.Tests.ps1 new file mode 100644 index 00000000..dd901f83 --- /dev/null +++ b/tests/Update-AUPackages.Streams.Tests.ps1 @@ -0,0 +1,210 @@ +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 text Report plugin' { + 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.Report = @{ + Type = 'text' + Path = "$global:au_Root\report.txt" + } + + $res = updateall -NoPlugins:$false -Options $Options 6> $null + + $pattern = "\bFinished $pkg_no packages\b[\S\s]*" + $pattern += '\b1 updated\b[\S\s]*' + $pattern += '\b0 errors\b[\S\s]*' + $pattern += '\btest_package_with_streams_1 +True +1\.3\.2 +1\.3\.1\b[\S\s]*' + $pattern += "\btest_package_with_streams_2 +False +1\.4-beta1 +1\.4-beta1\b[\S\s]*" + $pattern += '\btest_package_with_streams_1\b[\S\s]*' + $pattern += '\bStream: 1\.2\b[\S\s]*' + $pattern += '\bnuspec version: 1\.2\.3\b[\S\s]*' + $pattern += '\bremote version: 1\.2\.4\b[\S\s]*' + $pattern += '\bNew version is available\b[\S\s]*' + $pattern += '\bStream: 1\.3\b[\S\s]*' + $pattern += '\bnuspec version: 1\.3\.1\b[\S\s]*' + $pattern += '\bremote version: 1\.3\.2\b[\S\s]*' + $pattern += '\bNew version is available\b[\S\s]*' + $pattern += '\bStream: 1\.4\b[\S\s]*' + $pattern += '\bnuspec version: 1\.4-beta1\b[\S\s]*' + $pattern += '\bremote version: 1\.4-beta1\b[\S\s]*' + $pattern += '\bNo new version found\b[\S\s]*' + $pattern += '\bPackage updated\b[\S\s]*' + $pattern += '\btest_package_with_streams_2\b[\S\s]*' + $pattern += '\bStream: 1\.2\b[\S\s]*' + $pattern += '\bnuspec version: 1\.2\.3\b[\S\s]*' + $pattern += '\bremote version: 1\.2\.3\b[\S\s]*' + $pattern += '\bNo new version found\b[\S\s]*' + $pattern += '\bStream: 1\.3\b[\S\s]*' + $pattern += '\bnuspec version: 1\.3\.1\b[\S\s]*' + $pattern += '\bremote version: 1\.3\.1\b[\S\s]*' + $pattern += '\bNo new version found\b[\S\s]*' + $pattern += '\bStream: 1\.4\b[\S\s]*' + $pattern += '\bnuspec version: 1\.4-beta1\b[\S\s]*' + $pattern += '\bremote version: 1\.4-beta1\b[\S\s]*' + $pattern += '\bNo new version found\b[\S\s]*' + $Options.Report.Path | Should Exist + $Options.Report.Path | Should FileContentMatchMultiline $pattern + } + + It 'should execute markdown Report plugin' { + 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.Report = @{ + Type = 'markdown' + Path = "$global:au_Root\report.md" + Params = @{ Github_UserRepo = 'majkinetor/chocolatey' } + } + + $res = updateall -NoPlugins:$false -Options $Options 6> $null + + $pattern = "\bFinished $pkg_no packages\b[\S\s]*" + $pattern += '\b1 updated\b[\S\s]*' + $pattern += '\b0 errors\b[\S\s]*' + $pattern += '\btest_package_with_streams_1\b.*\bTrue\b.*\bFalse\b.*\b1\.3\.2\b.*\b1\.3\.1\b[\S\s]*' + $pattern += "\btest_package_with_streams_2\b.*\bFalse\b.*\bFalse\b.*\b1\.4-beta1\b.*\b1\.4-beta1\b[\S\s]*" + $pattern += '\btest_package_with_streams_1\b[\S\s]*' + $pattern += '\bStream: 1\.2\b[\S\s]*' + $pattern += '\bnuspec version: 1\.2\.3\b[\S\s]*' + $pattern += '\bremote version: 1\.2\.4\b[\S\s]*' + $pattern += '\bNew version is available\b[\S\s]*' + $pattern += '\bStream: 1\.3\b[\S\s]*' + $pattern += '\bnuspec version: 1\.3\.1\b[\S\s]*' + $pattern += '\bremote version: 1\.3\.2\b[\S\s]*' + $pattern += '\bNew version is available\b[\S\s]*' + $pattern += '\bStream: 1\.4\b[\S\s]*' + $pattern += '\bnuspec version: 1\.4-beta1\b[\S\s]*' + $pattern += '\bremote version: 1\.4-beta1\b[\S\s]*' + $pattern += '\bNo new version found\b[\S\s]*' + $pattern += '\bPackage updated\b[\S\s]*' + $pattern += '\btest_package_with_streams_2\b[\S\s]*' + $pattern += '\bStream: 1\.2\b[\S\s]*' + $pattern += '\bnuspec version: 1\.2\.3\b[\S\s]*' + $pattern += '\bremote version: 1\.2\.3\b[\S\s]*' + $pattern += '\bNo new version found\b[\S\s]*' + $pattern += '\bStream: 1\.3\b[\S\s]*' + $pattern += '\bnuspec version: 1\.3\.1\b[\S\s]*' + $pattern += '\bremote version: 1\.3\.1\b[\S\s]*' + $pattern += '\bNo new version found\b[\S\s]*' + $pattern += '\bStream: 1\.4\b[\S\s]*' + $pattern += '\bnuspec version: 1\.4-beta1\b[\S\s]*' + $pattern += '\bremote version: 1\.4-beta1\b[\S\s]*' + $pattern += '\bNo new version found\b[\S\s]*' + $Options.Report.Path | Should Exist + $Options.Report.Path | Should FileContentMatchMultiline $pattern + } + + 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 terminated 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..6bb6d4ff 100644 --- a/tests/Update-AUPackages.Tests.ps1 +++ b/tests/Update-AUPackages.Tests.ps1 @@ -104,7 +104,45 @@ Describe 'Update-AUPackages' -Tag updateall { (gc $TestDrive\tmp_test).Count | Should be 3 } - It 'should execute Report plugin' { + It 'should execute text Report plugin' { + 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.Report = @{ + Type = 'text' + Path = "$global:au_Root\report.txt" + } + + $res = updateall -NoPlugins:$false -Options $Options 6> $null + + $pattern = "\bFinished $pkg_no packages\b[\S\s]*" + $pattern += '\b1 updated\b[\S\s]*' + $pattern += '\b0 errors\b[\S\s]*' + $pattern += '\btest_package_1 +True +1\.3 +1\.2\.3\b[\S\s]*' + foreach ( $i in 2..$pkg_no ) { + $pattern += "\btest_package_$i +False +1\.2\.3 +1\.2\.3\b[\S\s]*" + } + $pattern += '\btest_package_1\b[\S\s]*' + $pattern += '\bnuspec version: 1\.2\.3\b[\S\s]*' + $pattern += '\bremote version: 1\.3\b[\S\s]*' + $pattern += '\bNew version is available\b[\S\s]*' + $pattern += '\bPackage updated\b[\S\s]*' + foreach ( $i in 2..$pkg_no ) { + $pattern += "\btest_package_$i\b[\S\s]*" + $pattern += '\bnuspec version: 1\.2\.3\b[\S\s]*' + $pattern += '\bremote version: 1\.2\.3\b[\S\s]*' + $pattern += '\bNo new version found\b[\S\s]*' + } + $Options.Report.Path | Should Exist + $Options.Report.Path | Should FileContentMatchMultiline $pattern + } + + It 'should execute markdown Report plugin' { + 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.Report = @{ Type = 'markdown' Path = "$global:au_Root\report.md" @@ -113,10 +151,26 @@ Describe 'Update-AUPackages' -Tag updateall { $res = updateall -NoPlugins:$false -Options $Options 6> $null - Test-Path $Options.Report.Path | Should Be $true - - $report = gc $Options.Report.Path - ($report -match "test_package_[1-3]").Count | Should Be 9 + $pattern = "\bFinished $pkg_no packages\b[\S\s]*" + $pattern += '\b1 updated\b[\S\s]*' + $pattern += '\b0 errors\b[\S\s]*' + $pattern += '\btest_package_1\b.*\bTrue\b.*\bFalse\b.*\b1\.3\b.*\b1\.2\.3\b[\S\s]*' + foreach ( $i in 2..$pkg_no ) { + $pattern += "\btest_package_$i\b.*\bFalse\b.*\bFalse\b.*\b1\.2\.3\b.*\b1\.2\.3\b[\S\s]*" + } + $pattern += '\btest_package_1\b[\S\s]*' + $pattern += '\bnuspec version: 1\.2\.3\b[\S\s]*' + $pattern += '\bremote version: 1\.3\b[\S\s]*' + $pattern += '\bNew version is available\b[\S\s]*' + $pattern += '\bPackage updated\b[\S\s]*' + foreach ( $i in 2..$pkg_no ) { + $pattern += "\btest_package_$i\b[\S\s]*" + $pattern += '\bnuspec version: 1\.2\.3\b[\S\s]*' + $pattern += '\bremote version: 1\.2\.3\b[\S\s]*' + $pattern += '\bNo new version found\b[\S\s]*' + } + $Options.Report.Path | Should Exist + $Options.Report.Path | Should FileContentMatchMultiline $pattern } It 'should execute RunInfo plugin' { @@ -136,9 +190,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 @@ -159,7 +275,7 @@ Describe 'Update-AUPackages' -Tag updateall { $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 + $res[0].Error -eq "Job terminated due to the 5s UpdateTimeout" | Should Be $true } It 'should update all packages when forced' { diff --git a/tests/Update-Package.Streams.Tests.ps1 b/tests/Update-Package.Streams.Tests.ps1 new file mode 100644 index 00000000..cc6658ef --- /dev/null +++ b/tests/Update-Package.Streams.Tests.ps1 @@ -0,0 +1,381 @@ +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.NuspecVersion | Should Be 1.2.3 + $res.RemoteVersion | Should Be 1.2.4 + $res.Streams.'1.2'.NuspecVersion | Should Be 1.2.3 + $res.Streams.'1.2'.RemoteVersion | Should Be 1.2.4 + $res.Streams.'1.3'.NuspecVersion | Should Be 1.3.1 + $res.Streams.'1.3'.RemoteVersion | Should Be 1.3.1 + $res.Streams.'1.4'.NuspecVersion | Should Be 1.4-beta1 + $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.3.2 + + $res = update + + $res.Updated | Should Be $true + $res.NuspecVersion | Should Be 1.3.1 + $res.RemoteVersion | Should Be 1.3.2 + $res.Streams.'1.2'.NuspecVersion | Should Be 1.2.3 + $res.Streams.'1.2'.RemoteVersion | Should Be 1.2.4 + $res.Streams.'1.3'.NuspecVersion | Should Be 1.3.1 + $res.Streams.'1.3'.RemoteVersion | Should Be 1.3.2 + $res.Streams.'1.4'.NuspecVersion | Should Be 1.4-beta1 + $res.Streams.'1.4'.RemoteVersion | Should Be 1.4-beta1 + $res.Result[-1] | Should Be 'Package updated' + (json_file).'1.2' | Should Be 1.2.4 + (json_file).'1.3' | Should Be 1.3.2 + (json_file).'1.4' | Should Be 1.4-beta1 + } + + 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 dbca9563..556ed797 100644 --- a/tests/Update-Package.Tests.ps1 +++ b/tests/Update-Package.Tests.ps1 @@ -27,6 +27,7 @@ Describe 'Update-Package' -Tag update { $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 } @@ -37,7 +38,26 @@ Describe 'Update-Package' -Tag update { InModuleScope AU { Context 'Updating' { - It 'can backup and restore using WhatIf' { + + It 'can set description from README.md' { + $readme = 'dummy readme & test' + '','', $readme | Out-File $TestDrive\test_package\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 '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 + + $res.Result -match 'Setting package description from README.md' | Should BeNullOrEmpty + (nuspec_file).package.metadata.description | Should Be 'This is a test package 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 @@ -193,24 +213,24 @@ Describe 'Update-Package' -Tag update { It 'sets Force parameter from global variable au_Force if it is not bound' { $global:au_Force = $true - $msg = "Parameter Force set from global variable au_Force: $au_Force" + $filter_msg = "Parameter Force set from global variable au_Force: $au_Force" update -Verbose - Assert-MockCalled Write-Verbose -ParameterFilter { $Message -eq $msg } + Assert-MockCalled Write-Verbose -ParameterFilter { $Message -eq $filter_msg } } It "doesn't set Force parameter from global variable au_Force if it is bound" { $global:au_Force = $true - $msg = "Parameter Force set from global variable au_Force: $au_Force" + $filter_msg = "Parameter Force set from global variable au_Force: $au_Force" update -Verbose -Force:$false - Assert-MockCalled Write-Verbose -ParameterFilter { $Message -ne $msg } + Assert-MockCalled Write-Verbose -ParameterFilter { $Message -ne $filter_msg } } It 'sets Timeout parameter from global variable au_Timeout if it is not bound' { $global:au_Timeout = 50 - $msg = "Parameter Timeout set from global variable au_Timeout: $au_Timeout" + $filter_msg = "Parameter Timeout set from global variable au_Timeout: $au_Timeout" update -Verbose - Assert-MockCalled Write-Verbose -ParameterFilter { $Message -eq $msg } + Assert-MockCalled Write-Verbose -ParameterFilter { $Message -eq $filter_msg } } } @@ -234,7 +254,7 @@ Describe 'Update-Package' -Tag update { update *> $null - $global:Latest.NuspecVersion | Should Be 0.0 + $global:Latest.NuspecVersion | Should Be '0.0' } } @@ -257,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