Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 41 additions & 0 deletions .github/actions/ci-versioning/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,47 @@ runs:
}
Write-Host "::notice title=Versioning::Found versioning datasets for: $($versions.Name -join ', ')"

# Precondition, not a policy choice. The runner is handed
# --subject-assemblies "<workspace>\Build" and restricts attribution to the
# namespaces the assemblies there declare. If that directory is absent it falls
# back to attributing every failure across the whole dependency closure, which
# reports other repositories' defects against this one: measured at 1,056 of 1,056
# on a real pull request. The runner does warn, but on stderr, and a warning does
# not stop the check producing a verdict it has no basis for.
#
# Asserted here rather than left to the runner because this is a property of the
# build, and the build is this action's job. Same shape as the dataset guard above.
#
# Not the vacuous-green question. "Nothing to check" is a legitimate green;
# "the thing to check was never built" is a broken precondition. This is the second.
#
# Two causes when it fires, and they need different fixes. Either the projects declare
# <OutputPath>..\Build\ only inside PropertyGroups conditioned on Debug or Test, so a
# Release build does not apply it; or they declare no <OutputPath> at all and take the
# SDK default. Both send output to bin\<config>\<tfm>\ and leave Build\ absent.
#
# Neither is a fault in those repositories. Nothing ever verified this directory: the
# convention was enforced by a linter that only rewrote <OutputPath> lines already
# present, and no check read Build\ until this one.
- name: Validate subject build output
if: steps.changed.outputs.count != '0'
shell: pwsh
run: |
$subjectDir = "${{ github.workspace }}\Build"

if (-not (Test-Path $subjectDir)) {
Write-Host "::error title=Versioning::Subject build output missing at $subjectDir. This repository's own assemblies were not built to the directory the check attributes against, so no failure could be attributed to it. Two usual causes: the projects declare <OutputPath>..\Build\ only under Debug or Test conditions, which a Release build does not apply; or they declare no <OutputPath> at all and take the SDK default. Either way the output went to bin\Release\<tfm>\ instead."
exit 1
}

$dlls = @(Get-ChildItem $subjectDir -Filter *.dll -Recurse -ErrorAction SilentlyContinue)
if ($dlls.Count -eq 0) {
Write-Host "::error title=Versioning::Subject build output at $subjectDir contains no assemblies. The directory exists but nothing was built into it, so no failure could be attributed to this repository."
exit 1
}

Write-Host "::notice title=Versioning::Subject build output: $($dlls.Count) assembl(ies) in $subjectDir."

- name: Prepare VersioningRunner
id: runner
if: steps.changed.outputs.count != '0'
Expand Down
64 changes: 64 additions & 0 deletions .github/scripts/tests/ci-versioning-action.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# ci-versioning-action.Tests.ps1 — structural assertions over ci-versioning's action.yml.
#
# Same shape as the sibling files for ci-serialisation and ci-compliance: properties of the
# action as a whole that a reader cannot check from any one place in it.
#
# The property here is that the check asserts its own preconditions. The runner is handed a
# subject build directory and narrows attribution to the assemblies in it; if that directory is
# missing it silently widens to the whole dependency closure and reports other repositories'
# defects against this one. The runner warns, but on stderr, and a warning does not stop a
# verdict being produced on no basis.
#
# Run locally: pwsh -Command "Invoke-Pester .github/scripts/tests -Output Detailed"
# Run in CI: lint-workflows.yml, the powershell-tests job.

BeforeAll {
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../..')).Path
$script:actionPath = Join-Path $repoRoot '.github/actions/ci-versioning/action.yml'
$script:lines = Get-Content $actionPath
$script:text = $lines -join "`n"
}

Describe 'ci-versioning action.yml' {

Context 'preconditions are asserted before the runner is invoked' {

# The datasets guard already existed and is the pattern being followed. Asserted so
# that if it is ever removed, the reasoning below stops resting on something absent.
It 'still guards the datasets directory' {
$text | Should -Match 'Datasets directory missing at'
}

It 'guards the subject build output the same way' {
$text | Should -Match 'Subject build output missing at' -Because 'an absent subject directory makes the check attribute every failure across the whole closure'
}

It 'also rejects a subject directory that exists but is empty' {
$text | Should -Match 'contains no assemblies' -Because 'a present but empty directory yields an empty subject set, which reports nothing and passes'
}

It 'fails rather than warns, because the check cannot do its job without it' {
$guard = $text -split '- name: Validate subject build output' | Select-Object -Last 1
$guard = ($guard -split '- name: ')[0]
$guard | Should -Match 'exit 1'
$guard | Should -Not -Match '::warning'
}

# Ordering matters and is not obvious from either step alone: the guard is worthless
# after the thing it protects has already run.
It 'runs before the versioning tests, not after' {
$guardIdx = ($lines | Select-String -Pattern '- name: Validate subject build output' | Select-Object -First 1).LineNumber
$runnerIdx = ($lines | Select-String -Pattern '- name: Run versioning tests' | Select-Object -First 1).LineNumber
$guardIdx | Should -BeLessThan $runnerIdx
}

# Substring rather than regex: the path contains backslashes and braces, and a guard
# that checked a different directory from the one the runner reads would pass a
# loosely-written pattern while protecting nothing.
It 'checks the same directory the runner is pointed at' {
$subjectPath = '"${{ github.workspace }}\Build"'
$text.Contains('$subjectDir = ' + $subjectPath) | Should -BeTrue -Because 'the guard must read the path the runner is given'
$text.Contains('--subject-assemblies ' + $subjectPath) | Should -BeTrue -Because 'if the runner argument changes, this guard stops protecting it'
}
}
}
Loading