Releasing
Public repository documentation · View source on GitHub
On this page
Source lives in the private repo; binaries, docs, and issues live in the public release hub ArasaniRohithReddy/app-releases. A release promotes an accepted, signed candidate there as a GitHub Release. Building, signing, uploading a draft, and making it public are separate authorization gates.
Why dotnet build, not dotnet publish
The app embeds the GitHub Copilot SDK, which bundles a CLI runtime at
runtimes\win-x64\native\copilot.exe and spawns it as a child process. That file is
placed by the SDK's _CopyCopilotCliToOutput build target.
dotnet publishdoes not run that target and tripsMSB3094(the SDK registers the runtime viaContentWithTargetPath).- Single-file publish would bundle
copilot.exeinside the host exe, so the SDK can't spawn it.
So scripts\build-release.ps1 uses dotnet build -c Release -r win-x64 --self-contained
and ships the resulting folder. (The folder is also what the installer and MSIX package.)
Paths containing apostrophes can cause SDK copy-task problems. The build script builds into a fresh, apostrophe-free folder under
%TEMP%before zipping/packaging. It does not reuse an olderbin\Releasepayload.
Build the artifacts
# Only after the exact source head, version, runtime and signer are approved.
# Requires installed .NET 10 SDK, WiX 5.0.2 + UI extension, Inno Setup 6,
# and Windows SDK makeappx/signtool. See the read-only preflight below first.
$ver = ([xml](Get-Content .\ThreatModelReviewer.App\ThreatModelReviewer.App.csproj)).Project.PropertyGroup.Version |
Where-Object { $_ } | Select-Object -First 1
pwsh scripts\build-release.ps1 -Version $ver -CertThumbprint $approvedThumbprint `
-ExpectedSourceCommit $acceptedHead `
-CopilotCliBinaryPath $approvedRuntime -ExpectedCopilotCliSha256 $approvedRuntimeHash
Outputs to dist\vX.Y.Z\ (git-ignored), or the explicit -OutputDirectory.
The directory must be new or empty. A retry must use a new empty directory rather
than deleting historical packages, screenshots or another build's output.
Bundled quick-start instructions come from docs/PACKAGE-README.md and
docs/PACKAGE-CLI.md, not the private source README's candidate/publication notices.
Missing bundled guides or licenses fail the build.
| Artifact | What it is |
|---|---|
ThreatModelReviewer-vX.Y.Z-x64.msi |
Dual-scope MSI — install for all users (per-machine) or just me (per-user). |
ThreatModelReviewer-vX.Y.Z-win-x64-portable.zip |
Self-contained folder — extract & run. |
ThreatModelReviewer-vX.Y.Z-setup.exe |
Per-user-default Inno Setup installer, with an optional elevated all-users mode. |
ThreatModelReviewer-vX.Y.Z-x64.msix |
Signed MSIX (experimental). |
ThreatModelReviewer-vX.Y.Z-cli-win-x64.zip |
Self-contained command line for CI and scripting. |
ThreatModelReviewer-vX.Y.Z-skill.zip |
GitHub Copilot CLI skill. Markdown plus a resolver script, so there is nothing to compile — it is staged, version-stamped and zipped. |
ThreatModelReviewer-publisher.cer |
Public signing certificate; trust requires an approved deployment policy. |
ThreatModelReviewer-vX.Y.Z-release.json |
Source SHA, SDK/runtime/framework versions, runtime hashes, signing observations and artifact digests. Not an installation acceptance claim. |
ThreatModelReviewer-vX.Y.Z-SHA256SUMS.txt |
SHA-256 for the seven distribution files and the release report. |
MSI is the recommended desktop distribution. The skill ZIP is not the CLI:
users must obtain the separate CLI ZIP and configure its resolver. An ordinary
release requires all seven distribution files. -SkipMsi, -SkipInstaller and
-SkipMsix are deliberate partial-development builds, not substitutes for release
acceptance. Missing tools or missing requested outputs now fail instead of
silently producing an unsigned/incomplete candidate.
Use build-release.ps1 as the release entry point: the standalone legacy
build-msi.ps1 does not provide this complete prerequisite/evidence gate.
Runtime acquisition in managed environments
The release script defaults to the app project's declared version and rejects a
different -Version; an artifact filename must not claim a different build version.
Each invocation uses its own staging directory rather than deleting another
concurrent build's staging files.
Run a side-effect-free preflight before staging or signing:
pwsh scripts\build-release.ps1 -PreflightOnly -CheckPackagingTools `
-CopilotCliBinaryPath $approvedRuntime -ExpectedCopilotCliSha256 $approvedRuntimeHash `
-CertThumbprint $approvedThumbprint -ExpectedSourceCommit $acceptedHead
It checks the app version, supported win-x64 target, SDK-pinned runtime version,
and supplied runtime metadata/hash. -CheckPackagingTools also checks the installed
tools, app file/assembly versions, empty output directory, and the selected
certificate's private-key association, validity, Code Signing EKU and exact MSIX
publisher match. It reads certificate metadata but creates/imports/trusts nothing.
HasPrivateKey does not prove key-provider/ACL access; only a separately authorized
signing attempt can establish that. SourceClean=false is not an accepted release head.
PackagingVerified is deliberately false: preflight is
not an installer, signature, packaged-runtime or upgrade test.
Preflight does not restore dependencies. On a fresh worktree it can read the
exact declared SDK version's existing NuGet GitHub.Copilot.SDK.props when
obj imports are absent; it never picks the newest cached SDK. If neither the
evaluated pin nor those cached props are available, it stops. A normal authorized
build may restore the declared projects and recheck the pin before staging.
Do not use CopilotSkipCliDownload=true for a release. That option is useful
for synthetic tests but can leave the real Copilot runtime absent or stale.
If managed security blocks runtime acquisition, stop; do not disable controls,
bootstrap npx, switch download routes or copy unapproved packages. Use an
already-approved runtime through the documented override:
pwsh scripts\build-release.ps1 -Version $ver -ExpectedSourceCommit $acceptedHead `
-CopilotCliBinaryPath $approvedRuntime -ExpectedCopilotCliSha256 $approvedRuntimeHash `
-CertThumbprint $approvedThumbprint
This uses the SDK's supported CopilotCliBinaryPath property. The script checks
the copied runtime's file version against CopilotCliVersion from the installed
SDK and, for an explicitly supplied binary, compares its SHA-256 after both
app and CLI builds, and requires both payloads to contain the same bytes. App,
CLI and setup executable file versions must equal X.Y.Z.0; the CLI build is
version-stamped without editing project metadata. MSI ProductVersion/UpgradeCode
and self-contained .NET 10 runtime files/configs are checked too.
A compile-only pass is not packaged-runtime validation.
Inspect the actual runtime behavior as well; package/file metadata and a
launcher-reported version are separate observations.
Tools
The lightweight packaging regression harness writes an actual counted JSON report:
pwsh packaging\Test-ReleasePreflight.ps1 -ApprovedRuntimePath $approvedRuntime `
-ReportPath C:\release-evidence\packaging-fixtures.json
Use a new report path. Require Status: Passed and a nonzero Executed count;
a printed success line does not replace the report or a successful exit code.
- WiX v5 for the MSI:
dotnet tool install --global wix --version 5.0.2thenwix extension add -g WixToolset.UI.wixext/5.0.2. (WiX v7 requires a paid OSMF EULA — pin to v5.) - Inno Setup:
winget install JRSoftware.InnoSetup. - Windows SDK (MSIX:
makeappx+signtool):winget install Microsoft.WindowsSDK.10.0.26100.
These are provisioning instructions, not permission to install tools on a managed
host. Prefer already-installed approved tools. An existing winapp installation
can be inspected with winapp --cli-schema; do not bootstrap another copy.
This WPF app does not need winapp init or Windows App SDK version metadata to
package its existing build. The maintained manifest is
packaging\msix\AppxManifest.xml; its version is stamped only into staging.
SDK and bundled-runtime update policy
v2.6.0 baseline qualified 2026-09-14: GitHub.Copilot.SDK 1.0.11
with the SDK-pinned Windows runtime 1.0.79. Package/file metadata and the
local RPC status reported that runtime version; RPC protocol 3 identifies the
SDK/runtime transport, not the MCP specification version. A standalone installed
CLI's banner is not evidence about the bundled runtime; use the approved file's
metadata/hash and actual SDK RPC behavior, not a different launcher.
The model catalogue is fetched for the user's signed-in seat. A newer model appearing in that catalogue does not by itself require an SDK update. Offline verification hosts intentionally do not establish account/model availability.
An SDK 1.0.13 update is available for review, not automatically approved. Its published changes include host-tool cancellation, optional client identity, session credential callbacks and additional session behavior. Assess each new default and capability against the existing consent, authentication, environment and tool restrictions; do not enable a new channel merely because it exists.
For each candidate, use an isolated branch and record:
- The exact package version and its declared runtime pin. Acquire required binaries only through approved channels; never bypass an organizational block.
- Build, model discovery and actual SDK wire/tool-permission compatibility, including default-off/single/selected/all MCP behavior and shutdown.
- The complete relevant suite and packaged-runtime checks. Keep intermittent failures and unverified authenticated/native paths explicit; one passing rerun does not prove an earlier failure's cause was resolved.
- Updated dependency notices, feature/privacy documentation and skill syntax before release. Keep the previous accepted pin until the candidate qualifies.
See the upstream SDK releases and the runtime acquisition and preflight instructions above.
Signing
scripts\build-release.ps1 signs application executables and installers, not ZIP containers:
- with your certificate when you pass
-CertPfx <file> [-CertPassword <pw>]or-CertThumbprint <thumb>— use an appropriately issued certificate or Azure Trusted Signing for publisher trust; signing does not guarantee SmartScreen reputation; - otherwise with a reused/generated development-only self-signed certificate. That is not public publisher trust and does not by itself make an MSIX installable.
All five signed files (desktop/CLI hosts, MSI, setup and MSIX) use the same selected
certificate and RFC 3161 timestamping. -CertPfx is passed through to MSIX, not
replaced by another certificate. -CertThumbprint defaults to CurrentUser\My;
use -CertStoreLocation LocalMachine explicitly for a machine-store key. No key
is exported just to accommodate a store-backed certificate. PFX inspection uses
ephemeral key storage; the scripts never import certificate trust.
The MSIX publisher remains CN=ArasaniRohithReddy. A differently named
certificate is a release blocker, not a reason to silently change the package
identity. Preserve the MSI UpgradeCode, Inno AppId and MSIX Name/Application Id.
Certificate validity and key presence do not establish signer reputation.
Windows can report an untrusted self-signed root as UnknownError; the release
report preserves that status and TrustVerified=false, rather than calling it a
pass. winapp cert install requires an administrator terminal and separate
approval in the disposable acceptance environment, never routine preflight.
Acceptance gates (do not publish on build success alone)
-
Accepted source: the coordinator chooses the version and exact clean SHA, updates app/skill/docs metadata, and accepts the exact-head CI result. Passing an earlier head is insufficient. Source
main, other worktrees and stable public documentation are not changed by packaging. -
Read-only preflight: run the command above and the lightweight fixture suite:
pwsh packaging\Test-ReleasePreflight.ps1 -ApprovedRuntimePath $approvedRuntimeNeither command compiles or signs. On shared validation hosts, coordinate before the real build; do not overlap another agent's validation.
-
Authorized build/sign, once: keep the same thread holding the bounded lease for the entire child build and release it in
finally:$lease = [Threading.Mutex]::new($false, 'Local\TMR-Fleet-Validation-20260913') $held = $false try { try { $held = $lease.WaitOne([TimeSpan]::FromSeconds(30)) } catch [Threading.AbandonedMutexException] { $held = $true } if (-not $held) { throw 'Another validation owns the lease; coordinate and retry later.' } & .\scripts\build-release.ps1 -Version $ver -ExpectedSourceCommit $acceptedHead ` -CopilotCliBinaryPath $approvedRuntime -ExpectedCopilotCliSha256 $approvedRuntimeHash ` -CertThumbprint $approvedThumbprint } finally { if ($held) { $lease.ReleaseMutex() } $lease.Dispose() } -
Artifact integrity and metadata: read the immutable output; do not mix local MSIX with a different CI build/signer. Never use
dist/*for publication.. .\packaging\Release.Common.ps1 $out = Join-Path $PWD "dist\v$ver" Test-TmrReleaseEvidence -Directory $out -Version $ver ` -ExpectedSourceCommit $acceptedHead -ExpectedCopilotCliSha256 $approvedRuntimeHash foreach ($name in @("ThreatModelReviewer-v$ver-x64.msi", "ThreatModelReviewer-v$ver-setup.exe", "ThreatModelReviewer-v$ver-x64.msix")) { Get-TmrSignatureCheck (Join-Path $out $name) $approvedThumbprint } Get-TmrMsiProperty (Join-Path $out "ThreatModelReviewer-v$ver-x64.msi") ProductVersion Get-TmrMsiProperty (Join-Path $out "ThreatModelReviewer-v$ver-x64.msi") UpgradeCodeThe evidence check is only an integrity/provenance check. Independently inspect the ZIP/MSIX entries: root app/CLI executables;
runtimes/win-x64/native/copilot.exewith the approved SHA-256 in each app, CLI and MSIX payload; .NET 10includedFrameworks; the declared SDK in.deps.json; MSIX versionX.Y.Z.0, x64 architecture, unchanged publisher/identity, required logos andAppxSignature.p7x. Check the skill'splugin.json, resolver and separate-CLI instructions. Hashes are not a substitute for Authenticode verification. -
Disposable Windows acceptance, separately authorized: verify the actual signatures/timestamps under the intended trust policy and executable versions after extraction; clean MSI install and previous-stable → candidate upgrades in both scopes; setup install/upgrade; portable and CLI launch on a VM without preinstalled .NET; the separate skill-to-CLI resolver; MSIX install/upgrade where policy permits. Use synthetic files and no real OneDrive/Documents profile. Verify the packaged SDK/runtime path and expected RPC behavior, not a globally installed CLI banner. Do not launch native TMT on the shared profile. Hash-verified files, a signed file and a working/accepted installation are three different claims. Record unverified authenticated/native paths explicitly.
-
Publication approval: the coordinator accepts the evidence, trust limitations, full runtime/upgrade results and documentation/link fixes before promotion.
Publish the GitHub Release (public repo)
Manual (gh CLI), only after upload authorization:
$tag = "threat-model-reviewer-v$ver"
. .\packaging\Release.Common.ps1
$out = Join-Path $PWD "dist\v$ver"
Test-TmrReleaseEvidence -Directory $out -Version $ver `
-ExpectedSourceCommit $acceptedHead -ExpectedCopilotCliSha256 $approvedRuntimeHash
$assets = @(Get-TmrReleaseAssetNames -Version $ver -IncludeEvidence |
ForEach-Object { Join-Path $out $_ })
gh release create $tag @assets `
--repo ArasaniRohithReddy/app-releases `
--draft --latest=false --target main `
--title "Threat Model Reviewer v$ver" `
--notes-file docs\release-notes\v$ver.md
if ($LASTEXITCODE -ne 0) { throw 'Inspect the failed draft/upload; do not delete or overwrite existing releases.' }
Stop after draft creation. Inspect its exact nine assets and complete acceptance. The following is a separate operation, only after the coordinator authorizes public promotion:
gh release edit $tag --repo ArasaniRohithReddy/app-releases --draft=false --latest=false
The hub is multi-product. Never interpret /releases/latest as "latest TMR",
make TMR global latest, or edit/deprecate/delete another product's release to fix
discovery. As of the 2026-09-14 preflight, global latest is shot2code-v0.3.2;
TMR stable is threat-model-reviewer-v2.5.1. Discover by prefix, ignore drafts
and prereleases, and choose the highest product version:
gh api --paginate 'repos/ArasaniRohithReddy/app-releases/releases?per_page=100' `
--jq '.[] | select((.tag_name | startswith("threat-model-reviewer-v")) and (.draft | not) and (.prerelease | not)) | {tag_name,html_url}'
gh api repos/ArasaniRohithReddy/app-releases/releases/tags/threat-model-reviewer-v2.5.1
gh api repos/ArasaniRohithReddy/app-releases/pages --jq '{build_type,source,status}'
Use exact tag/download links in versioned docs, and a product-filtered listing for moving discovery links. The in-app checker filters the prefix rather than using global latest, but its API window is currently only 50 releases and Atom is finite; pagination/exhaustion and prerelease fallback remain updater-owner follow-ups.
--target main above identifies the hub's existing documentation commit when
GitHub creates the new hub tag; it does not push/merge source main. The build's
private-source SHA is recorded in the release report. Do not use --clobber,
auto-generated hub-wide notes, release deletion, force tags or history rewrites.
If upload fails, inspect the draft and resume only the new candidate's missing
assets with explicit approval. Preserve all prior release assets and screenshots.
Automated (GitHub Actions): an authorized product-prefixed private tag (or
manual dispatch) invokes .github/workflows/release.yml. It requires successful
CI for the exact head and the release environment, which administrators must
configure with required reviewers before enabling it. Required secrets are
RELEASE_TOKEN (hub contents:write), RELEASE_CERT_PFX_BASE64 and the appropriate
RELEASE_CERT_PASSWORD. It builds all formats with one approved signer and
creates a draft with --latest=false, never directly publishes stable.
The approved runtime must be obtainable under that runner's organizational policy;
do not use hosted execution as a workaround for a managed host's acquisition block.
For the currently blocked local environment use only the approved existing binary
override, and do not trigger a second hosted build to replace that candidate.
Actions backup upload is opt-in and continue-on-error; exhausted Actions storage
must not block the separate Release asset upload, and is never a reason to delete
existing artifacts. At the 2026-09-14 preflight only RELEASE_TOKEN was configured;
the protected environment and approved signing secrets were still prerequisites.
Versioning & tags
- Bump
<Version>inThreatModelReviewer.App\ThreatModelReviewer.App.csproj. - Keep its
<AssemblyVersion>/<FileVersion>atX.Y.Z.0, and update the skill version/docs under the coordinator's ownership before approving a release head. - Tag format is product-prefixed so the hub can host multiple apps:
threat-model-reviewer-vX.Y.Z(a future app would useother-app-vX.Y.Z). - Add a
CHANGELOG.mdentry and adocs\release-notes\vX.Y.Z.md.
Documentation must move with the release
The version number appears in several documents as a claim about the current release, and a stale claim is worse than no claim — SECURITY.md telling a reader that an older build is the supported one is a security-relevant inaccuracy. So these are checked automatically:
| What | Where | Checked by |
|---|---|---|
| Changelog entry and link | CHANGELOG.md |
DocumentationCurrencyTests |
| Release notes file | docs\release-notes\vX.Y.Z.md |
DocumentationCurrencyTests |
| Supported-versions table | SECURITY.md |
DocumentationCurrencyTests |
| Deployment version floor | docs\ENTERPRISE-DEPLOYMENT.md |
DocumentationCurrencyTests |
| Build example | README.md |
DocumentationCurrencyTests |
| Every tab is described | docs\USER-GUIDE.md |
DocumentationCurrencyTests |
dotnet test fails if any of these fall behind, so a release cannot be tagged with documentation
that still describes the previous one. Bump the version first, then run the suite: the failures tell
you exactly which documents to update.
Keep documentation aligned during development using docs\DOCUMENTATION.md.
The complete public mapping lives in docs\publication.json, including the public
README, user/CLI/skill guides, security guidance and synthetic sample.
pwsh scripts\sync-public-docs.ps1 -HubPath C:\work\app-releases
pwsh scripts\sync-public-docs.ps1 -HubPath C:\work\app-releases -Check
Those commands require a stable documentation channel. During development,
docs\publication.json is marked development; use -AllowDevelopment only
with a separate preview worktree. Do not overwrite the stable v2.5.1 guides with
new MCP command instructions before publishing matching app, CLI and skill
bundles. docs\DOCUMENTATION.md describes the stable-source
and preview workflows.
Review the hub portal README and all Pages surfaces separately: hero claims,
screenshots, version fallbacks, JSON-LD, MSI recommendation, CLI/skill links and
asset classifiers. The copy command does not certify those manual surfaces.
After publishing, run the hub release-snapshot workflow and verify the live result.
Preserve the working legacy Pages source main:/docs; a product release is not
authorization to migrate Pages, enable a different deployment mode, or replace the
portal with one application's site. Recheck the Pages API and live product pages,
historical downloads and screenshots after any separately approved docs update.
Do not label a source change as shipped just because the project still carries the last release's version. Preserve historical release notes; document unreleased work separately until new packages are built and verified.