Imported from BillHertzing/ATAP.Utilities (
.github/skills/build-deploy-module/SKILL.md). Install upstream withnpx skills add BillHertzing/ATAP.Utilities --skill build-deploy-module. Copyright stays with the author.
Build Deploy Module
Release an ATAP PowerShell module from a sprint worktree through BuildMaster
into powershellget-stable, test the promoted package, and install that exact
Production version on the workstation with -Scope AllUsers.
Required input
ModuleName- the exact module folder name under a repositorysrc/folder.
Non-negotiable gates
- Do not guess the sprint worktree. If zero or multiple sprint worktrees contain the module, stop and ask the user which path to use.
- Treat
version.json.versionas the release source, not always the final package version. Recompute the resolved package version after the release-prep commit because NBGV/git height can affect the built artifact version. - A stable deployment is not done when BuildMaster accepts or queues work. It is
done only when the exact resolved version appears in
powershellget-stable, promoted-module tests pass, the exact version is installed to AllUsers, and a fresh shell imports the expected version from the expected module path. - Do not publish directly to ProGet or the PowerShell Gallery. BuildMaster owns build, pack, promote, and release progression.
- Do not stage or commit files outside the selected module root.
Deterministic helpers to prefer
Use the installed/source ATAP.Utilities.BuildTooling.PowerShell helpers when
available instead of reimplementing repository conventions.
| Helper | Use |
|---|---|
Resolve-BuildMasterPackageProjectPath |
Resolve the module project path after a sprint repository root is selected when this helper is actually exported in the current BuildTooling build. |
Get-BuildContext |
Compute ResolvedPackageVersion, CeilingTier, and BuildMaster tier metadata. In the current contract this requires both -Application and -ProjectPath. |
Get-PSModuleVersionFromNBGV |
Cross-check the NBGV-derived PowerShell module/package version from the module root. In the current contract this uses -ModuleRoot. |
Get-TierFromNBGVLabel |
Explain why a prerelease label maps to a non-stable tier when CeilingTier is not Production. |
Start-BuildMasterModulePipelineBatch |
Start the reviewed BuildMaster module pipeline. This queues/starts work; it does not prove stable promotion by itself. |
Invoke-PromotedModuleTests |
Run tier-appropriate tests against the promoted package from the feed, not against the source tree alone. |
Do not use private helpers as the primary contract for this skill. In
particular, Import-ATAPModuleFromProGet can be useful background knowledge but
does not install an exact RequiredVersion, so it is not the deployment gate
for this workflow.
1. Resolve the sprint worktree
Search sibling worktrees under C:/Dropbox/whertzing/GitHub for sprint
directories that contain src/<ModuleName>/version.json.
- If no sprint worktree contains the module, stop and ask the user what repo or path to use.
- If more than one sprint worktree contains the module, list the matching paths
and ask the user which one to use. Do not guess between
-work-items,-release-build, or other parallel sprint worktrees. - Once selected, treat that sprint worktree as the repository root.
After selecting the root, import ATAP.Utilities.BuildTooling.PowerShell and
attempt to resolve the module root with:
$moduleRoot = Resolve-BuildMasterPackageProjectPath -ModuleName <ModuleName> -RepositoryRoot <repoRoot>
If that helper is unavailable or not exported in the current BuildTooling
version, report that fact and fall back to <repoRoot>/src/<ModuleName>.
2. Read and validate version.json
Read <moduleRoot>/version.json and require a non-empty top-level version
property.
- If
version.jsonis missing or malformed, stop and report the exact path and parse error. - Preserve every property except
versionwhen a bump is required. - Use structured JSON parsing/writing. Do not use ad hoc regex replacement unless the file shape is simple enough to preserve safely and you say so in the final report.
3. Compute the current package context
From the selected sprint worktree root, first resolve the BuildMaster
application through the reviewed BuildMasterApplicationByModule mapping or an
explicit ApplicationByModule override. Then compute context with the current
helper signature:
$branch = git branch --show-current
$application = '<ResolvedApplication>'
$context = Get-BuildContext -Application $application -ProjectPath $moduleRoot -Branch $branch
Cross-check the NBGV result with the current helper signature:
$nbgvVersion = Get-PSModuleVersionFromNBGV -ModuleRoot $moduleRoot
Required checks:
- Record
version.json.version,ResolvedPackageVersion,Get-PSModuleVersionFromNBGV.FullNuGetVersion,CurrentTier,CeilingTier, andBranchType. - Require
CeilingTierto beProduction. If it is notProduction, stop and tell the user the current version/branch can only promote as the reported tier, not topowershellget-stable. - If
version.json.versioncontains prerelease or build metadata, stop unless the user explicitly directed a non-standard stable rewrite. - Do not rely on
BranchTypebeingsprintfor branch names shaped like123-Sprint-0012-work-items; in current tooling those may still classify asstable. TreatCeilingTierandResolvedPackageVersionas the release gates, and report any surprising branch classification in the final notes.
4. Inspect installed module versions
Enumerate $env:PSModulePath in path order and look for the first root that
contains the module.
- If the module does not exist in any PSModulePath entry, record that there is no installed-version floor and continue.
- If the module exists, use only the first matching PSModulePath root for the installed-version comparison.
- In that first matching root, determine the highest installed version for the module.
Do not merge versions across later PSModulePath entries. The first matching root is the source of truth because earlier paths shadow later AllUsers installs.
5. Bump version.json if needed
Parse the version.json.version and the highest installed version as semantic
versions.
- If no installed module was found, leave
version.json.versionunchanged unless the user explicitly asked for a bump. - If an installed module was found and
version.json.versionis less than or equal to the installed-version floor, setversion.json.versionto one patch higher than the higher of the installed version and the currentversion.json.version. - If
version.json.versionis already higher than the installed-version floor, still bump it one patch higher than the currentversion.json.versionwhen the user's request is to ship a new stable replacement. ProGet versions are immutable; do not attempt to reship the same stable version.
Write back only the version property change.
6. Commit the module folder
From the selected sprint worktree root:
- Review
git status --short -- <moduleRoot>. - Stage
version.jsonand any other changed files under the module root or its subfolders. - Never stage unrelated files outside the selected module root.
- If unexpected module-root changes are present, pause and ask before committing unless the user explicitly told you those changes are part of the release.
Create one intentional commit for the release-prep change. A good default commit message is:
build(<ModuleName>): prepare <ResolvedPackageVersion> release
After committing, recompute Get-BuildContext and Get-PSModuleVersionFromNBGV
from the module root. Use the recomputed ResolvedPackageVersion for every
BuildMaster, feed, test, install, and verification step that follows.
7. Trigger the BuildMaster pipeline
Start the reviewed shared pipeline:
Start-BuildMasterModulePipelineBatch -ModuleName <ModuleName>
If the module-to-application mapping is absent or ambiguous, stop and ask the
user for an ApplicationByModule override rather than guessing.
Capture and retain the returned identifiers, including:
ApplicationPackageVersionReleaseNumberReleaseIdBuildNumberBuildIdExecutionId
Do not report success just because these identifiers exist. They mean the work was accepted/started, not that stable promotion completed.
8. Wait for stable promotion
The promotion gate is the exact recomputed version appearing in the Production PowerShell feed:
Find-Module -Name <ModuleName> -RequiredVersion <ResolvedPackageVersion> -Repository powershellget-stable
- Keep the BuildMaster identifiers visible in notes while polling.
- If the exact version appears in
powershellget-stable, continue. - If the BuildMaster run fails, stalls, requires manual approval, or never produces the exact stable-feed version, report the failing stage, BuildMaster identifiers, and surfaced error details to the user, then stop and wait for direction.
- Always include the BuildMaster execution log URL when reporting a failed or stalled promotion. Use the captured execution identifier in this form:
http://localhost:50017/executions/logs?executionId=<ExecutionId>&level=0&download=False
For example, if ExecutionId is 16464, show:
http://localhost:50017/executions/logs?executionId=16464&level=0&download=False
If the registered repository name differs from powershellget-stable, report
the discovered repository name and use the repository that resolves to the
Production PowerShell feed.
9. Test the promoted artifact
Run tests against the package restored from the stable feed, not just source tests:
$resultsPath = Join-Path <repoRoot> "_generated/build-deploy-module/<ModuleName>/<ResolvedPackageVersion>/Production"
Invoke-PromotedModuleTests -Name <ModuleName> -Version <ResolvedPackageVersion> -Feed powershellget-stable -Tier Production -ResultsPath $resultsPath -ModuleSourceRoot $moduleRoot
If promoted-module tests fail, report the result path and failure summary, then stop before installation unless the user explicitly directs otherwise.
10. Install the exact Production version
Once the exact version is visible in powershellget-stable and promoted-module
tests pass, install that exact version for all users.
Do not call Install-Module -Scope AllUsers directly, and never wrap it in
Start-Process -Verb RunAs. An AllUsers install writes under Program Files and
therefore needs elevation, which an agent session does not have; retrying a
denied UAC prompt is the exact misstep the elevation broker exists to eliminate.
Route it through the broker instead, which performs the install as the broker
service account with no UAC interaction and leaves a transcript either way.
The broker requires both FeedUrl and ExpectedSha256 — they are mandatory
on Install-ATAPModuleAllUsers, and the broker config rejects a request that
omits either. Resolve them from the same stable feed the promotion gate used:
Import-Module ATAP.Utilities.BuildTooling.ProGet.PowerShell -ErrorAction Stop
# Feed endpoint for the Production PowerShell feed.
$feedUrl = (Get-PSRepository -Name powershellget-stable).SourceLocation
# Hash the exact package that section 9 tested, so the bytes that were validated
# are the bytes that get installed. Save-Module writes the .nupkg into $stage.
$stage = Join-Path $env:TEMP ("bdm-{0}" -f [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $stage -Force | Out-Null
Save-Package -Name <ModuleName> -RequiredVersion <ResolvedPackageVersion> `
-Source powershellget-stable -Path $stage -ProviderName NuGet -Force | Out-Null
$nupkg = Get-ChildItem -Path $stage -Filter '*.nupkg' -Recurse | Select-Object -First 1
$expectedSha256 = (Get-FileHash -Algorithm SHA256 -Path $nupkg.FullName).Hash
$r = Request-ElevatedInstall -InstallerId 'install-atap-module-allusers' -Parameters @{
ModuleName = '<ModuleName>'
RequiredVersion = '<ResolvedPackageVersion>'
Repository = 'powershellget-stable'
FeedUrl = $feedUrl
ExpectedSha256 = $expectedSha256
}
Remove-Item -LiteralPath $stage -Recurse -Force -ErrorAction SilentlyContinue
if ($r.status -ne 'succeeded') {
throw "Elevated install failed [$($r.status)]: $($r.error). Transcript: $($r.transcriptPath)"
}
What the hash pin does and does not prove: because the caller downloads from the same feed the broker will, it is not an independent check against a compromised feed. What it does guarantee is that the exact artifact section 9 tested is the artifact installed — the feed cannot swap bytes between test and install.
Handling a non-succeeded result — report and stop, never retry:
broker-unreachable— the request was staged but the broker task could not be started. The account needs start rights:Grant-ElevationBrokerStartRights(elevated). Report this to the user; do not fall back to a UAC prompt.timeout— the broker ran but produced no result. ReporttranscriptPath.failed— the installer itself failed. ReporterrorandtranscriptPath.
If the broker is not provisioned on this machine at all, Request-ElevatedInstall
throws that the requests folder does not exist. Provision it with
Register-ElevationBrokerTask (elevated) — see NewComputerSetup.md — then re-run.
Verify locally with:
Get-Module -Name <ModuleName> -ListAvailable
Then verify from a fresh shell that normal module resolution imports the exact
version. If an earlier PSModulePath entry such as
C:\Dropbox\whertzing\PowerShell\Modules shadows the AllUsers install, report
the shadowing path and do not copy or mirror the module there unless the user
explicitly directs it.
Final report
Report:
- the selected sprint worktree and module root;
- the
version.jsonpath and finalversionproperty; - the recomputed
ResolvedPackageVersion; - whether a local installed-version floor was found, and where;
- the release-prep commit SHA;
- the BuildMaster application, release, build, and execution identifiers;
- the stable-feed exact-version proof;
- the promoted-module test result path and pass/fail status;
- the elevation-broker
requestId, returnedstatus, andtranscriptPath; - the final AllUsers install and fresh-shell import result.