Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1597838
Use common-ci's release plan in place of this repository's copy
jwrosewell Nov 5, 2026
db78491
Release a package only for a change that counts, leaving out document…
jwrosewell Nov 7, 2026
0713add
Refuse a filter's path it was not given, and IgnoreDefaults as text, …
jwrosewell Nov 7, 2026
3232edb
Count a manifest that is not UTF-8 rather than stopping the plan
jwrosewell Nov 7, 2026
d3ce8df
Test that a manifest that is not UTF-8 counts
jwrosewell Nov 7, 2026
8637ebd
Release no package for a change only to its tox.ini
jwrosewell Nov 9, 2026
3668b67
Ignore tox.ini in every package, including those without one yet
jwrosewell Nov 9, 2026
1ccc3cd
Merge rc into feature/release-plan-ignore
jwrosewell Nov 13, 2026
e2220a1
Test that a change only to the maintainers releases nothing
jwrosewell Nov 13, 2026
766fe11
Leave the maintainers out of the manifest metadata, as the authors are
jwrosewell Nov 13, 2026
c8175fc
Merge feature/release-plan-ignore, which holds rc, into feature/share…
jwrosewell Nov 13, 2026
642d492
Say that the release plan comes from common-ci, and that packages.ps1…
jwrosewell Nov 13, 2026
d292d72
Give the dispatch tests' common-ci folder the release plan, as a real…
jwrosewell Nov 13, 2026
ec8b2fd
Name the maintainers among the fields the filter leaves out, in the h…
jwrosewell Nov 13, 2026
e7c4796
Merge feature/release-plan-ignore into feature/shared-release-plan
jwrosewell Nov 13, 2026
742f256
Copy common-ci's whole steps folder into the dispatch tests' stand-in…
jwrosewell Nov 13, 2026
bb56b0e
Merge rc into feature/shared-release-plan
jwrosewell Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions ci/dispatch.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
}
Expand Down
206 changes: 6 additions & 200 deletions ci/packages.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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') |
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading