Think of a deployment as a box of app files. This pipeline writes a checklist with a digital fingerprint for every file. Before IIS (the web server) opens the new box, the pipeline checks that the files still match.
1. Make a fingerprint checklist
Run this after dotnet publish has put the finished app in a clean folder. SHA-256 reads a file and makes a long fingerprint from its bytes. If even one byte changes, the fingerprint should change too.
# This is the folder containing the finished app files.
$publishRoot = $env:BUILD_ARTIFACTSTAGINGDIRECTORY
$manifestPath = Join-Path $publishRoot 'deploy-manifest.json'
# Look through every file in the finished app folder and its subfolders.
$files = @(
Get-ChildItem -LiteralPath $publishRoot -Force -File -Recurse |
ForEach-Object {
# Save a short path like Views/Home/Index.cshtml, not the whole computer path.
$relativePath = $_.FullName.Substring($publishRoot.TrimEnd('\').Length).TrimStart('\').Replace('\', '/')
[pscustomobject]@{
path = $relativePath
# Ask PowerShell to read this file and calculate its SHA-256 fingerprint.
sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash
}
}
)
# Label the checklist so we know which build and code version made it.
$manifest = [pscustomobject]@{
buildId = $env:BUILD_BUILDID
sourceVersion = $env:BUILD_SOURCEVERSION
files = $files
}
# Turn the checklist into JSON text and save it beside the app files.
# It is saved after the fingerprints are made, so the checklist does not fingerprint itself.
$manifest | ConvertTo-Json -Depth 5 |
Set-Content -LiteralPath $manifestPath -Encoding UTF8
2. Check a folder against the checklist
The pipeline runs this check on the downloaded zip contents, then runs it again after copying the files onto the server. If a check fails, throw stops the pipeline so the new version cannot be switched on.
function Test-Manifest {
param([string]$Root) # The folder we want to check.
# Open the checklist and read its JSON text as PowerShell data.
$manifestPath = Join-Path $Root 'deploy-manifest.json'
$manifest = Get-Content -LiteralPath $manifestPath -Raw | ConvertFrom-Json
# Make sure this checklist came from this exact build and code version.
if ($manifest.buildId -ne $env:BUILD_BUILDID -or
$manifest.sourceVersion -ne $env:BUILD_SOURCEVERSION) {
throw 'Manifest belongs to a different build or commit.'
}
# Make a lookup table: each expected file name points to its saved fingerprint.
$expected = @{}
foreach ($entry in $manifest.files) {
$expected[$entry.path.Replace('/', '\')] = $entry.sha256
}
# Find every real file in the folder. Do not count the checklist itself.
$actualFiles = @(
Get-ChildItem -LiteralPath $Root -Force -File -Recurse |
Where-Object { $_.FullName -ne $manifestPath }
)
# A missing or extra file means this folder does not match the checklist.
if ($actualFiles.Count -ne $expected.Count) {
throw 'Manifest and folder contain different numbers of files.'
}
foreach ($file in $actualFiles) {
# Work out the file's name inside the folder.
$relativePath = $file.FullName.Substring($Root.TrimEnd('\').Length).TrimStart('\')
# Make a new fingerprint from the file that arrived here.
$actualHash = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash
# Stop if the file was not on the list or its fingerprint is different.
if (-not $expected.ContainsKey($relativePath) -or
$actualHash -ne $expected[$relativePath]) {
throw "File verification failed: $relativePath"
}
}
# Reaching this line means all the files matched.
Write-Host "Verified $($actualFiles.Count) files in $Root"
}
3. Copy, check, then switch the website
# Check the downloaded files before copying them.
Test-Manifest -Root $artifactFolder
# Copy them into a new folder named for this release. Keep the live folder as it is.
robocopy $artifactFolder $releasePath /E /R:2 /W:2
if ($LASTEXITCODE -ge 8) { throw "Copy failed: $LASTEXITCODE" }
$global:LASTEXITCODE = 0 # Robocopy uses small numbers for successful copies.
# Check the server copy too. Do not switch IIS if it does not match.
Test-Manifest -Root $releasePath
# Remember where IIS points now, in case we need to undo the switch.
$previousPath = (Get-Website -Name $siteName).physicalPath
Stop-Website -Name $siteName # Pause the website while changing its folder.
Stop-WebAppPool -Name $appPool
# Tell IIS to use the folder we just checked.
Set-ItemProperty -Path "IIS:\Sites\$siteName" -Name physicalPath -Value $releasePath
try {
# Turn the new website back on.
Start-WebAppPool -Name $appPool
Start-Website -Name $siteName
} catch {
# If starting it fails, point IIS back to the old folder and try again.
Stop-Website -Name $siteName -ErrorAction SilentlyContinue
Stop-WebAppPool -Name $appPool -ErrorAction SilentlyContinue
Set-ItemProperty -Path "IIS:\Sites\$siteName" -Name physicalPath -Value $previousPath
Start-WebAppPool -Name $appPool
Start-Website -Name $siteName
throw
}
What the check tells us
A matching fingerprint means the file that arrived is the same as the file on the checklist. A missing file, an extra file, or changed contents stops the release before IIS switches folders.
This check cannot tell whether the app itself has every feature it needs. The build still needs its normal checks, and a smoke test should open important pages after deployment. Also, the manifest is a checklist, not a secret stamp: someone who could replace both the app files and the checklist could make them match. For a signed guarantee, the checklist would need a separate protected signature.
Some ASP.NET pages are packed inside the app’s compiled file when the app is built, instead of being separate files. See Microsoft’s Razor view compilation guide.