diff --git a/AGENTS.md b/AGENTS.md index 01221404..505c110a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -298,9 +298,10 @@ last tag, or when a package it depends on is released. Documentation description, keywords, urls, classifiers, readme, authors or maintainers does not count either, which `ci/release-plan-filter.ps1` works out with Python. The examples packages are tested but never published. common-ci's publish -carries one version, which here is only a label, and the release plan in -`ci/packages.ps1` decides what is built, published and tagged. Never push a -plain `4.5.x` tag, because nothing here reads one. +carries one version, which here is only a label, and the release plan +decides what is built, published and tagged. The release plan is common-ci's +`steps/release-plan.ps1`, which `ci/packages.ps1` loads, and common-ci tests +it. Never push a plain `4.5.x` tag, because nothing here reads one. `ci/packages.json` is the list of packages and what each depends on. Add a package there, with a `FirstVersion` if it has never been released, or it is diff --git a/ci/dispatch.Tests.ps1 b/ci/dispatch.Tests.ps1 index 3d75e0a2..352e667b 100644 --- a/ci/dispatch.Tests.ps1 +++ b/ci/dispatch.Tests.ps1 @@ -2,8 +2,8 @@ # # Each test builds a throwaway repository with two products, 'alpha' and # 'beta', whose ci/ scripts are written by the test, and runs the dispatcher -# from a folder laid out like a common-ci checkout, with common-ci's real -# steps/run-script.ps1, because that is how common-ci calls it. +# from a folder laid out like a common-ci checkout, with a copy of common-ci's +# real steps folder, because that is how common-ci calls it. BeforeAll { . (Join-Path $PSScriptRoot 'dispatch.ps1') @@ -17,6 +17,10 @@ BeforeAll { if ((Test-Path $script:RunScript) -eq $false) { throw "common-ci not found at '$commonCi'; set COMMON_CI to a checkout of it" } + # The whole steps folder is copied, because ci/packages.ps1 loads the + # release plan from the common-ci checkout that holds this repository's + # folder, and the release plan may load other steps beside it. + $script:Steps = Join-Path $commonCi 'steps' # A repository with packages.json naming one package in each product. function New-ProductRepo { @@ -45,8 +49,8 @@ BeforeAll { # A folder laid out like a common-ci checkout, which the dispatcher runs in. function New-CommonCi { $path = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString()) - $steps = New-Item -ItemType Directory -Force (Join-Path $path 'steps') - Copy-Item $script:RunScript $steps + $null = New-Item -ItemType Directory -Force $path + Copy-Item -Recurse $script:Steps (Join-Path $path 'steps') return $path } } diff --git a/ci/packages.Tests.ps1 b/ci/packages.Tests.ps1 index 243d87a6..63066049 100644 --- a/ci/packages.Tests.ps1 +++ b/ci/packages.Tests.ps1 @@ -1,9 +1,11 @@ -# Tests for ci/packages.ps1. +# Tests for ci/packages.ps1 and ci/release-plan-filter.ps1. # # The selection rules are tested against a small package list written here, so -# a test states exactly which graph it relies on. The release plan reads tags -# and diffs, so those tests build a throwaway git repository. One test reads the -# real packages.json, to catch a typo in a dependency name or a cycle. +# a test states exactly which graph it relies on. The release plan, which +# packages.ps1 loads from common-ci, is tested there. The tests of this +# repository's filter and of the build plan read tags and diffs, so they build +# a throwaway git repository. Some tests read the real packages.json, to catch +# a typo in a dependency name or a cycle, and to check its Ignore globs. BeforeAll { . (Join-Path $PSScriptRoot 'packages.ps1') @@ -75,40 +77,6 @@ AfterEach { $script:Repo = $null } -Describe 'Get-DependentClosure' { - It 'adds every package that depends on a changed one, through others' { - $closure = Get-DependentClosure $script:Graph @('core') - $closure | Sort-Object | Should -Be @( - 'core', 'dd', 'engines', 'examples', 'ipi', 'location') - } - - It 'adds nothing for a package nothing depends on' { - Get-DependentClosure $script:Graph @('did') | Should -Be @('did') - } - - It 'is empty when given nothing' { - @(Get-DependentClosure $script:Graph @()).Count | Should -Be 0 - } -} - -Describe 'Sort-ByDependency' { - It 'puts every package after the packages it depends on' { - $order = Sort-ByDependency $script:Graph @('dd', 'engines', 'core') - $order | Should -Be @('core', 'engines', 'dd') - } - - It 'ignores dependencies that are not being sorted' { - Sort-ByDependency $script:Graph @('dd') | Should -Be @('dd') - } - - It 'refuses a cycle rather than looping' { - $cycle = @( - New-Package 'a' 'p' 'p/a' @('b') - New-Package 'b' 'p' 'p/b' @('a')) - { Sort-ByDependency $cycle @('a', 'b') } | Should -Throw '*cycle*' - } -} - Describe 'Resolve-AffectedProducts' { It 'tests only the product of a package nothing depends on' { Resolve-AffectedProducts $script:Graph @('device/dd/a.py') | @@ -147,168 +115,6 @@ Describe 'Resolve-AffectedProducts' { } } -Describe 'Get-ReleasePlan' { - It 'releases nothing but first releases when nothing changed since the tags' { - $script:Repo = New-Repo $script:AllFiles $script:AllTags - Add-Commit $script:Repo @() - $plan = @(Get-ReleasePlan $script:Repo $script:Graph) - $plan.Name | Should -Be @('ipi') - $plan[0].Version | Should -Be '4.5.0' - $plan[0].Reason | Should -Be 'first release' - } - - It 'releases a changed package at the next patch, and its dependents' { - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @('pipeline/engines/a.py') - $plan = @(Get-ReleasePlan $script:Repo $script:Graph) - $plan.Name | Should -Be @('engines', 'dd', 'ipi') - ($plan | Where-Object Name -eq 'engines').Version | Should -Be '4.5.39' - ($plan | Where-Object Name -eq 'dd').Version | Should -Be '4.5.97' - ($plan | Where-Object Name -eq 'dd').Reason | - Should -Be 'a package it depends on is released' - ($plan | Where-Object Name -eq 'ipi').Version | Should -Be '4.5.1' - } - - It 'releases a vendoring package when the vendored folder changes' { - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @('pipeline/owid/a.py') - @(Get-ReleasePlan $script:Repo $script:Graph).Name | Should -Be @('did') - } - - It 'releases nothing for a change to an unpublished package' { - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @('device/examples/a.py') - @(Get-ReleasePlan $script:Repo $script:Graph).Count | Should -Be 0 - } - - It 'releases nothing for a change outside every package' { - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @('ci/build.ps1', 'device/docs/a.md') - @(Get-ReleasePlan $script:Repo $script:Graph).Count | Should -Be 0 - } - - It 'releases nothing a second time from a commit already released' { - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @('device/dd/a.py') - git -C $script:Repo tag 'dd/4.5.97' - @(Get-ReleasePlan $script:Repo $script:Graph).Count | Should -Be 0 - } - - It 'continues from the highest tag, not the latest one made' { - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + @( - 'ipi/4.5.0', 'dd/4.5.9')) - Add-Commit $script:Repo @('device/dd/a.py') - (@(Get-ReleasePlan $script:Repo $script:Graph) | - Where-Object Name -eq 'dd').Version | Should -Be '4.5.97' - } - - It 'does not read another package''s tags as its own' { - # 'ipi' must not see tags of a package whose name starts with 'ipi'. - $graph = $script:Graph + (New-Package 'ipi_cloud' 'ipi' 'ipi/cloud' -FirstVersion '4.5.0') - $script:Repo = New-Repo ($script:AllFiles + 'ipi/cloud/a.py') ( - $script:AllTags + 'ipi_cloud/4.5.7') - $plan = @(Get-ReleasePlan $script:Repo $graph) - ($plan | Where-Object Name -eq 'ipi').Version | Should -Be '4.5.0' - } - - It 'plans a changed package for a caller that stops on native errors' { - # publish-package.ps1 stops on a failing native command; 'git diff - # --quiet' exiting 1 for a changed package once failed the publish. - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @('device/dd/a.py') - $plan = & { - $ErrorActionPreference = 'Stop' - $PSNativeCommandUseErrorActionPreference = $true - @(Get-ReleasePlan $script:Repo $script:Graph) - } - $plan.Name | Should -Be @('dd') - } - - It 'reads the tags whatever folder it runs from' { - # PowerShell expands a wildcard in a native command's argument into - # matching files, so a tag pattern run from a folder that holds a - # folder of the same name, as the repository root holds 'location', - # once found no tags. - $script:Repo = New-Repo $script:AllFiles $script:AllTags - $here = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString()) - $null = New-Item -ItemType Directory -Force (Join-Path $here 'location') - $null = New-Item -ItemType File (Join-Path $here 'location' 'setup.py') - Push-Location $here - try { - (Get-ReleasedVersions $script:Repo 'location')[-1] | Should -Be ([version]'4.5.4') - } finally { - Pop-Location - Remove-Item -Recurse -Force $here - } - } - - It 'refuses a never-released package with no FirstVersion' { - $graph = @(New-Package 'new' 'p' 'p/new') - $script:Repo = New-Repo @('p/new/a.py') @() - { Get-ReleasePlan $script:Repo $graph } | Should -Throw '*FirstVersion*' - } - - It 'says how many files counted' { - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @('device/dd/a.py', 'device/dd/b.py') - (@(Get-ReleasePlan $script:Repo $script:Graph) | - Where-Object Name -eq 'dd').Reason | - Should -Be 'changed since 4.5.96 (2 files)' - } - - It 'releases nothing for a change only to documentation, tests or examples' { - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @( - 'device/dd/readme.md', 'device/dd/docs/guide.txt', - 'device/dd/tests/test_a.py', 'device/dd/test/a.py', - 'device/dd/examples/a.py', 'device/dd/LICENSE', - 'pipeline/core/sub/notes.md') - @(Get-ReleasePlan $script:Repo $script:Graph).Count | Should -Be 0 - } - - It 'ignores documentation, tests and examples whatever their case' { - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @( - 'device/dd/README.MD', 'device/dd/Tests/a.py', - 'device/dd/Examples/a.py') - @(Get-ReleasePlan $script:Repo $script:Graph).Count | Should -Be 0 - } - - It 'releases for the code changed beside the documentation' { - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @('device/dd/readme.md', 'device/dd/a.py') - $plan = @(Get-ReleasePlan $script:Repo $script:Graph) - $plan.Name | Should -Be @('dd') - $plan[0].Reason | Should -Be 'changed since 4.5.96 (1 file)' - } - - It 'leaves out the files a package''s Ignore globs match' { - $graph = @($script:Graph | ForEach-Object { $_.PSObject.Copy() }) - ($graph | Where-Object Name -eq 'dd') | - Add-Member Ignore @('device/dd/tox.ini') - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @('device/dd/tox.ini') - @(Get-ReleasePlan $script:Repo $graph).Count | Should -Be 0 - } - - It 'counts documentation for a package that turns the defaults off' { - $graph = @($script:Graph | ForEach-Object { $_.PSObject.Copy() }) - ($graph | Where-Object Name -eq 'dd') | Add-Member IgnoreDefaults $false - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @('device/dd/readme.md') - @(Get-ReleasePlan $script:Repo $graph).Name | Should -Be @('dd') - } - - It 'refuses IgnoreDefaults given as text' { - $graph = @($script:Graph | ForEach-Object { $_.PSObject.Copy() }) - ($graph | Where-Object Name -eq 'dd') | Add-Member IgnoreDefaults 'false' - $script:Repo = New-Repo $script:AllFiles ($script:AllTags + 'ipi/4.5.0') - Add-Commit $script:Repo @('device/dd/readme.md') - { Get-ReleasePlan $script:Repo $graph } | - Should -Throw '*IgnoreDefaults*true or false*' - } -} - Describe 'release-plan-filter.ps1' { BeforeAll { # A setup.py, setup.cfg and pyproject.toml like the ones here, with diff --git a/ci/packages.ps1 b/ci/packages.ps1 index ec4bb89b..17f73ca2 100644 --- a/ci/packages.ps1 +++ b/ci/packages.ps1 @@ -1,117 +1,26 @@ -# Shared by the scripts in this folder, which dot-source it. Reads the -# packages this repository publishes from packages.json, works out which -# products a pull request touches, and which packages a publish releases at -# which version. +# Shared by the scripts in this folder, which dot-source it. Works out which +# products a pull request touches, what a build of HEAD makes, and checks each +# package's tox.ini. # -# Each package has its own version, tagged '/'. A package is -# released when a file that counts under its folders changed since its last -# tag, or when a package it depends on is being released, because its -# requirement on that package moves. This is the scheme in 51Degrees/rust#205, -# which the per language repositories follow. -# -# Every file counts except those matched by the package's Ignore globs in -# packages.json and, unless its IgnoreDefaults is false, by -# $ReleasePlanDefaultIgnore, so a change only to documentation, tests or -# examples releases nothing. release-plan-filter.ps1 then leaves out a -# manifest whose change is only to metadata such as links. These are the -# fields and the filter of common-ci's steps/release-plan.ps1. -# -# A submodule in Path or ExtraPaths changes as one path, its own, so any move -# of it counts. Neither the globs nor the filter see inside it. - -# Reads packages.json. Kept as a function so the tests can give their own list. -function Get-Packages([string]$RepoPath) { - return Get-Content -Raw (Join-Path $RepoPath 'ci/packages.json') | - ConvertFrom-Json -} - -# The folders whose changes count as a change to the package: its own folder, -# and any folder it takes files from at build time, such as a submodule it -# vendors. -function Get-PackagePaths($Package) { - $paths = @($Package.Path) - if ($Package.ExtraPaths) { - $paths += $Package.ExtraPaths - } - return $paths -} - -function Test-PathUnder([string]$Path, [string]$Folder) { - return $Path -eq $Folder -or $Path.StartsWith("$Folder/") -} - -# The files whose changes do not release a package, being documentation, -# tests, examples and LICENSE files wherever they sit and whatever their case, -# unless the package sets IgnoreDefaults to false. These are git glob -# pathspecs from the repository root, where '**/' matches any number of -# folders, none included. -$ReleasePlanDefaultIgnore = @( - '**/*.md' - '**/docs/**' - '**/tests/**' - '**/test/**' - '**/examples/**' - '**/LICENSE*' -) - -# The pathspecs that leave out of a package's changes the files its Ignore -# globs match and, unless IgnoreDefaults is false, the default set. -# PowerShell on Linux and macOS replaces a native command's argument that -# holds a '*' with the files it matches, if any. Each of these starts with -# ':(', which no file in a checkout starts with, so git receives it as written. -function Get-PackageExcludes($Package) { - $defaults = $Package.IgnoreDefaults - if ($null -ne $defaults -and $defaults -isnot [bool]) { - throw "$($Package.Name) gives IgnoreDefaults as '$defaults', where " + - "packages.json must give true or false." - } - $excludes = @($Package.Ignore | Where-Object { $_ } | - ForEach-Object { ":(exclude,glob)$_" }) - if ($defaults -ne $false) { - # The defaults ignore case, as one language names a folder Tests or - # Examples where another names it tests or examples. - $excludes = @($ReleasePlanDefaultIgnore | - ForEach-Object { ":(exclude,glob,icase)$_" }) + $excludes - } - return $excludes -} - -# The files under the package's folders that changed since the given tag, as -# paths from the repository root, apart from those it ignores. -function Get-ChangedFiles([string]$RepoPath, $Package, [string]$From) { - # A failure is reported below, naming the package, whatever the caller's - # preference. Set for this function only. - $PSNativeCommandUseErrorActionPreference = $false - $pathspecs = @(Get-PackagePaths $Package) + @(Get-PackageExcludes $Package) - $files = @(& git -C $RepoPath diff --name-only $From HEAD -- $pathspecs) - if ($LASTEXITCODE -ne 0) { - throw "git could not list the changes to $($Package.Name) since $From." - } - return $files -} - -# The files that count among a package's changed files. The repository's -# ci/release-plan-filter.ps1 decides when there is one, and otherwise every -# file counts. A path the filter returns that it was not given stops the -# plan, because a filter that rewrote a path would otherwise release a -# package on a file that never changed. -function Select-CountedFiles([string]$RepoPath, $Package, [string]$From, - [string[]]$Files) { - $filter = Join-Path $RepoPath 'ci/release-plan-filter.ps1' - if ($Files.Count -eq 0 -or (Test-Path $filter -PathType Leaf) -eq $false) { - return $Files - } - $filter = (Resolve-Path $filter).ProviderPath - $kept = @(& $filter -RepoPath $RepoPath -Package $Package -From $From ` - -Files $Files | Where-Object { $_ }) - foreach ($file in $kept) { - if ($Files -cnotcontains $file) { - throw "ci/release-plan-filter.ps1 returned '$file' for " + - "$($Package.Name), which is not one of the files it was given." - } - } - return $kept +# Which packages a publish releases, and at which version, is worked out by +# common-ci's steps/release-plan.ps1 from ci/packages.json. common-ci runs +# these scripts from its own root, with this repository checked out inside +# it. A checkout of common-ci beside this repository is found too, or one +# elsewhere can be named with COMMON_CI. +$releasePlan = @( + $env:COMMON_CI, + (Join-Path $PSScriptRoot '..' '..'), + (Join-Path $PSScriptRoot '..' '..' 'common-ci')) | + Where-Object { $_ } | + ForEach-Object { Join-Path $_ 'steps' 'release-plan.ps1' } | + Where-Object { Test-Path $_ } | + Select-Object -First 1 +if ($null -eq $releasePlan) { + throw "common-ci's steps/release-plan.ps1 was not found. Set COMMON_CI " + + "to a checkout of common-ci." } +. $releasePlan +Remove-Variable releasePlan # Each tox.ini that does not install every package its package depends on, # directly or through another, from this checkout, as a line naming what is @@ -158,56 +67,6 @@ function Get-ToxGaps([string]$RepoPath, $Packages = $null) { return $gaps } -# The names of the given packages and of every package that depends on one of -# them, directly or through another. -function Get-DependentClosure($Packages, [string[]]$Names) { - $closure = [Collections.Generic.HashSet[string]]::new() - foreach ($name in @($Names | Where-Object { $_ })) { - $null = $closure.Add($name) - } - $grew = $true - while ($grew -eq $true) { - $grew = $false - foreach ($package in $Packages) { - if ($closure.Contains($package.Name) -eq $true) { - continue - } - foreach ($dependency in $package.DependsOn) { - if ($closure.Contains($dependency) -eq $true) { - $null = $closure.Add($package.Name) - $grew = $true - break - } - } - } - } - return @($closure) -} - -# The named packages, each after every package it depends on, so a release -# never publishes a package before the version it requires exists. -function Sort-ByDependency($Packages, [string[]]$Names) { - $remaining = [Collections.Generic.List[string]]::new([string[]]@($Names)) - $sorted = [Collections.Generic.List[string]]::new() - while ($remaining.Count -gt 0) { - $ready = @($remaining | Where-Object { - $name = $_ - $package = $Packages | Where-Object { $_.Name -eq $name } - @($package.DependsOn | Where-Object { - $remaining.Contains($_) -eq $true - }).Count -eq 0 - } | Sort-Object) - if ($ready.Count -eq 0) { - throw "packages.json has a dependency cycle among: $($remaining -join ', ')" - } - foreach ($name in $ready) { - $sorted.Add($name) - $null = $remaining.Remove($name) - } - } - return @($sorted) -} - # The products a set of changed files touches. A file in a package's folders # touches that package's product and the product of every package depending on # it, because a change to pipeline can break device-detection. @@ -251,10 +110,7 @@ function Resolve-AffectedProducts($Packages, [string[]]$Changed) { } # The products a pull request changes, or every product when the checkout is -# not a pull request (a publish, or a build of the branch itself), as -# 51degrees-cxx does. A pull request is checked out with the branch it merges -# into merged in, so the merge base with that branch leaves only the pull -# request's own changes. +# not a pull request (a publish, or a run on main), as 51degrees-cxx does. function Get-AffectedProducts([string]$RepoPath, [string]$Branch = 'main') { # merge-base fails where there is no such branch, which is an answer here. # Set for this function only, so a caller that stops on a failing native @@ -271,117 +127,6 @@ function Get-AffectedProducts([string]$RepoPath, [string]$Branch = 'main') { return Resolve-AffectedProducts $packages $changed } -# The parts of a version such as 4.5.38, or $null for anything else. -function ConvertTo-VersionParts([string]$Text) { - $match = [regex]::Match($Text, '^(\d+)\.(\d+)\.(\d+)$') - if ($match.Success -eq $false) { - return $null - } - return [version]::new([int]$match.Groups[1].Value, - [int]$match.Groups[2].Value, [int]$match.Groups[3].Value) -} - -# The package's released versions, read from its '/' tags, -# highest last. The '/' after the name keeps fiftyone_ipintelligence from -# reading the tags of fiftyone_ipintelligence_cloud. -function Get-ReleasedVersions([string]$RepoPath, [string]$Name, - [switch]$AtHead) { - # Tags are found by prefix, not with a 'name/*' pattern: PowerShell on - # Linux and macOS expands a '*' in an argument to a native command into - # matching files, so from a folder holding a 'location' folder git would - # be handed file names and find no tags. - $arguments = @('-C', $RepoPath, 'for-each-ref', - '--format=%(refname:strip=2)', "refs/tags/$Name/") - if ($AtHead -eq $true) { - $arguments += @('--points-at', 'HEAD') - } - $versions = foreach ($tag in (& git @arguments)) { - ConvertTo-VersionParts $tag.Substring($Name.Length + 1) - } - return @($versions | Where-Object { $null -ne $_ } | Sort-Object) -} - -# What a publish of HEAD releases: one entry per package, in the order to -# publish them, each with the version it is released at and why. -# -# A package already tagged at HEAD was released from this commit, so it is -# left out, which makes a second publish of the same commit release nothing. -# A package with no tag has never been released and starts at its FirstVersion -# from packages.json. Packages that are not published, the examples, are never -# in the plan; a change to them is tested but releases nothing. -function Get-ReleasePlan([string]$RepoPath, $Packages = $null) { - if ($null -eq $Packages) { - $Packages = Get-Packages $RepoPath - } - $published = @($Packages | Where-Object { $_.Publish -eq $true }) - - $previous = @{} - $reasons = @{} - foreach ($package in $published) { - $atHead = Get-ReleasedVersions $RepoPath $package.Name -AtHead - if ($atHead.Count -gt 0) { - continue - } - $released = Get-ReleasedVersions $RepoPath $package.Name - if ($released.Count -eq 0) { - if ([string]::IsNullOrEmpty($package.FirstVersion) -eq $true) { - throw "$($package.Name) has never been released and " + - "packages.json gives it no FirstVersion." - } - $previous[$package.Name] = $null - $reasons[$package.Name] = 'first release' - continue - } - $last = $released[-1] - $previous[$package.Name] = $last - $tag = "$($package.Name)/$last" - $files = @(Get-ChangedFiles $RepoPath $package $tag) - $counted = @(Select-CountedFiles $RepoPath $package $tag $files) - if ($counted.Count -gt 0) { - $noun = 'files' - if ($counted.Count -eq 1) { - $noun = 'file' - } - $reasons[$package.Name] = - "changed since $last ($($counted.Count) $noun)" - } - # A filter may leave an exit code that was an answer, which must not - # read as a failure to the script that called this. - $global:LASTEXITCODE = 0 - } - - # A dependent is released with its dependency, as long as it was not - # already released from this commit. - $candidates = @($published.Name | Where-Object { - $previous.ContainsKey($_) -eq $true - }) - $changed = @($reasons.Keys) - $toRelease = @(Get-DependentClosure $published $changed | Where-Object { - $candidates -contains $_ - }) - - foreach ($name in (Sort-ByDependency $published $toRelease)) { - $package = $published | Where-Object { $_.Name -eq $name } - $last = $previous[$name] - if ($null -eq $last) { - $version = $package.FirstVersion - } else { - $version = "$($last.Major).$($last.Minor).$($last.Build + 1)" - } - $reason = $reasons[$name] - if ($null -eq $reason) { - $reason = 'a package it depends on is released' - } - [pscustomobject]@{ - Name = $name - Path = $package.Path - Version = $version - Previous = $last - Reason = $reason - } - } -} - # What a build of HEAD makes: the release plan, or, when nothing changed since # the last release, every published package at its released version. Nothing # is published then, because package-update-required.ps1 finds the release