zscripts-token-savers/tests/DeployTag.Tests.ps1
KellyMichels 99a794ed9b chore(mirror): mirror tagOnDeploy and the zstart gitPull fix, and stop publishing current product names
The mirror carries the tagOnDeploy feature, its tests, and the zstart
gitPull fix from the private tree. Twelve published references to
current product names and internal issue numbers are reworded
generically, the denylist learns the current spellings (a dot or hyphen
broke the word, an underscore hid the boundary), and planted cases prove
the suite now sees them. CHECKSUMS.txt regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 13:28:45 -05:00

198 lines
9.0 KiB
PowerShell

# zdeploy lays the release tag it already knows the number for.
#
# Invoke-Pester .\tests
#
# A versioning scheme that asks every release to lay an annotated tag needs
# something to enforce it. For a project whose build number lives OUTSIDE git -
# in a database, say - nothing did, and one project reached thirty-eight builds
# with four tags. Those builds were not recoverable: the number only ever
# existed in the database.
#
# These tests drive REAL git against a real bare remote in a temp directory,
# the way StartGitPull.Tests.ps1 does. A stand-in that faked git would prove
# nothing about the two properties that matter most here: that the tag is
# annotated and pushed, and that NOTHING this function does can fail a deploy
# which already reached production.
BeforeAll {
. (Join-Path (Split-Path -Parent $PSScriptRoot) "ZHelpers.ps1")
$script:tmpRoots = New-Object System.Collections.ArrayList
function New-TempDir {
param([string]$Tag)
$dir = Join-Path ([IO.Path]::GetTempPath()) ("zdeploy-tag-$Tag-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $dir -Force | Out-Null
[void]$script:tmpRoots.Add($dir)
return $dir
}
function Invoke-Git {
# Test plumbing only. The thing under test does its own git handling
# and must not go through here.
param([string]$In, [string[]]$GitArgs)
Push-Location -LiteralPath $In
try {
$ErrorActionPreference = 'Continue'
$quoted = $GitArgs | ForEach-Object { if ($_ -match '\s') { '"' + $_ + '"' } else { $_ } }
$out = & cmd /c ("git " + ($quoted -join ' ') + " 2>&1")
if ($LASTEXITCODE -ne 0) { throw "test plumbing: git $($GitArgs -join ' ') failed in $In`n$out" }
return $out
}
finally { Pop-Location }
}
function New-RepoWithRemote {
param([string]$Tag)
$remote = New-TempDir "$Tag-remote"
Invoke-Git -In $remote -GitArgs @('init', '--bare', '-q') | Out-Null
$work = New-TempDir "$Tag-work"
Invoke-Git -In $work -GitArgs @('init', '-q', '-b', 'main') | Out-Null
Invoke-Git -In $work -GitArgs @('config', 'user.email', 'test@example.com') | Out-Null
Invoke-Git -In $work -GitArgs @('config', 'user.name', 'Test') | Out-Null
Set-Content -LiteralPath (Join-Path $work 'app.txt') -Value 'v1' -Encoding utf8
Invoke-Git -In $work -GitArgs @('add', '-A') | Out-Null
Invoke-Git -In $work -GitArgs @('commit', '-q', '-m', 'first') | Out-Null
Invoke-Git -In $work -GitArgs @('remote', 'add', 'origin', $remote) | Out-Null
Invoke-Git -In $work -GitArgs @('push', '-q', '-u', 'origin', 'main') | Out-Null
return @{ Work = $work; Remote = $remote }
}
function New-Proj {
param([string]$Root, $TagOnDeploy = $true)
$deploy = if ($null -eq $TagOnDeploy) {
[pscustomobject]@{ zipName = 'X.zip' }
} else {
[pscustomobject]@{ zipName = 'X.zip'; tagOnDeploy = $TagOnDeploy }
}
return [pscustomobject]@{ localRoot = $Root; deploy = $deploy }
}
function Get-Tags {
param([string]$In)
$out = Invoke-Git -In $In -GitArgs @('tag', '--list')
return @($out | Where-Object { $_ -and $_.ToString().Trim() } |
ForEach-Object { $_.ToString().Trim() })
}
}
AfterAll {
foreach ($d in $script:tmpRoots) {
Remove-Item -LiteralPath $d -Recurse -Force -ErrorAction SilentlyContinue
}
}
Describe 'New-DeployTag' {
It 'tags the deployed commit and pushes it' {
$r = New-RepoWithRemote 'happy'
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.39' -Note 'Build deployed'
Get-Tags -In $r.Work | Should -Contain 'v0.0.1.0.39'
# On the remote too: a tag only in the local checkout is not a record.
$remoteTags = Invoke-Git -In $r.Work -GitArgs @('ls-remote', '--tags', 'origin')
($remoteTags -join "`n") | Should -Match 'refs/tags/v0\.0\.1\.0\.39'
}
It 'writes an ANNOTATED tag carrying the version and the note' {
$r = New-RepoWithRemote 'annotated'
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v1.2.3.4.5' -Note 'Ask AI sources'
# cat-file says "tag" for an annotated object and "commit" for a
# lightweight one. The scheme asks for annotated.
$type = Invoke-Git -In $r.Work -GitArgs @('cat-file', '-t', 'v1.2.3.4.5')
($type -join '').Trim() | Should -Be 'tag'
$msg = Invoke-Git -In $r.Work -GitArgs @('tag', '-l', 'v1.2.3.4.5', '--format=%(contents)')
($msg -join ' ') | Should -Match 'v1\.2\.3\.4\.5'
($msg -join ' ') | Should -Match 'Ask AI sources'
}
It 'points the tag at the commit that was deployed' {
$r = New-RepoWithRemote 'sha'
$head = (Invoke-Git -In $r.Work -GitArgs @('rev-parse', 'HEAD') -join '').Trim()
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v9.9.9.9.9'
$tagged = (Invoke-Git -In $r.Work -GitArgs @('rev-list', '-n', '1', 'v9.9.9.9.9') -join '').Trim()
$tagged | Should -Be $head
}
It 'does nothing at all unless the project opts in' {
$r = New-RepoWithRemote 'optout'
New-DeployTag -Proj (New-Proj $r.Work -TagOnDeploy $false) -Version 'v0.0.0.0.1'
Get-Tags -In $r.Work | Should -BeNullOrEmpty
# And when the key is absent entirely, which is every existing project.
New-DeployTag -Proj (New-Proj $r.Work -TagOnDeploy $null) -Version 'v0.0.0.0.2'
Get-Tags -In $r.Work | Should -BeNullOrEmpty
}
It 'leaves an existing tag alone rather than failing a redeploy' {
$r = New-RepoWithRemote 'exists'
New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.40' -Note 'first'
{ New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.40' -Note 'second' } |
Should -Not -Throw
$msg = Invoke-Git -In $r.Work -GitArgs @('tag', '-l', 'v0.0.1.0.40', '--format=%(contents)')
($msg -join ' ') | Should -Match 'first'
($msg -join ' ') | Should -Not -Match 'second'
}
It 'never throws when the directory is not a git repo' {
$plain = New-TempDir 'notrepo'
{ New-DeployTag -Proj (New-Proj $plain) -Version 'v0.0.0.0.3' } | Should -Not -Throw
}
It 'never throws when the push fails, and still writes the tag locally' {
# A deploy that reached production must not be reported as failed
# because a tag could not leave the machine.
$r = New-RepoWithRemote 'badremote'
Invoke-Git -In $r.Work -GitArgs @('remote', 'set-url', 'origin',
(Join-Path ([IO.Path]::GetTempPath()) 'no-such-remote-zz')) | Out-Null
{ New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.41' } | Should -Not -Throw
Get-Tags -In $r.Work | Should -Contain 'v0.0.1.0.41'
}
It 'survives the deploy-wide ErrorActionPreference of Stop' {
# The trap this whole file documents: under 'Stop', PS 5.1 turns any
# native stderr line into a terminating error. git push writes its
# ordinary progress to stderr, so a tag that works interactively can
# still kill a deploy.
$r = New-RepoWithRemote 'stoppref'
$ErrorActionPreference = 'Stop'
{ New-DeployTag -Proj (New-Proj $r.Work) -Version 'v0.0.1.0.42' } | Should -Not -Throw
Get-Tags -In $r.Work | Should -Contain 'v0.0.1.0.42'
}
}
Describe 'Get-DeployStampFiles' {
It 'covers every changelog name the fleet actually writes' {
# The old inline copy knew CHANGELOG.md and not build_changelog.md,
# a name a project's own changelog tool may write - so that stamp
# counted as unreviewed source in the branch guard.
$stamps = Get-DeployStampFiles
$stamps | Should -Contain 'build-version.json'
$stamps | Should -Contain 'CHANGELOG.md'
$stamps | Should -Contain 'build_changelog.md'
}
It 'does not count a stamp the deploy just wrote as an uncommitted change' {
$r = New-RepoWithRemote 'stamps'
Set-Content -LiteralPath (Join-Path $r.Work 'build_changelog.md') -Value 'x' -Encoding utf8
Invoke-Git -In $r.Work -GitArgs @('add', '-A') | Out-Null
Invoke-Git -In $r.Work -GitArgs @('commit', '-q', '-m', 'add changelog') | Out-Null
Set-Content -LiteralPath (Join-Path $r.Work 'build_changelog.md') -Value 'stamped by the deploy' -Encoding utf8
Push-Location -LiteralPath $r.Work
try { $changes = Get-TrackedChangesExcludingStamps } finally { Pop-Location }
$changes.Count | Should -Be 0
}
It 'still reports real source changes' {
$r = New-RepoWithRemote 'realchange'
Set-Content -LiteralPath (Join-Path $r.Work 'app.txt') -Value 'edited' -Encoding utf8
Push-Location -LiteralPath $r.Work
try { $changes = Get-TrackedChangesExcludingStamps } finally { Pop-Location }
$changes.Count | Should -Be 1
($changes -join ' ') | Should -Match 'app\.txt'
}
}