-
Notifications
You must be signed in to change notification settings - Fork 0
Development
startset/
├── src/
│ ├── Core/ models, enums, path constants
│ ├── Infrastructure/ preferences, logging, tracking, validation, network
│ ├── Engine/ execution engine, processors, user-session launcher
│ ├── CLI/ System.CommandLine surface -> managedstatekeeper.exe
│ └── Service/ Generic Host workers -> StartSetService.exe
├── tests/StartSet.Tests/
├── build/
│ ├── msi/ WiX v6 Package.wxs and .wixproj
│ ├── nupkg/ nuspec template
│ └── pkg/ build-info.yaml, preinstall.ps1, postinstall.ps1 templates
├── scripts/ pre/postinstall for a repo-root package layout
├── .github/workflows/ ci.yml, release.yml
├── build.ps1
├── build-info.yaml
├── Directory.Build.props
└── StartSet.sln
The five projects are named StartSet.Core, StartSet.Infrastructure, StartSet.Engine,
StartSet.CLI and StartSet.Service — note that the project directories are src/Core,
src/Engine and so on, without the prefix.
| Tool | Needed for |
|---|---|
| .NET 10 SDK | Building and testing |
| Windows 10/11 | The projects target net10.0-windows and use Win32 APIs |
| Windows 10/11 SDK (Signing Tools) |
signtool.exe, only for signed builds |
cimipkg.exe |
Building .msi and .pkg artifacts via build.ps1
|
nuget.exe |
Building and signing .nupkg artifacts |
IntuneWinAppUtil.exe |
Building .intunewin artifacts |
Shared build properties live in Directory.Build.props: net10.0-windows, runtime identifiers
win-x64;win-arm64, LangVersion preview, nullable and implicit usings enabled, and
self-contained single-file ReadyToRun publishing with compression.
Plain SDK build and test:
dotnet build StartSet.sln --configuration Releasedotnet test StartSet.sln --configuration Releasebuild.ps1 is the full pipeline. With no arguments it builds binaries for both architectures,
signs them if a certificate is found, and produces MSI, NuGet and .pkg artifacts into release\.
.\build.ps1Binaries only, unsigned — the fastest useful loop and what CI uses for releases:
.\build.ps1 -NoSign -BinariesDevelopment mode stops the StartSet service and any running processes first, builds Debug, and
skips signing:
.\build.ps1 -Dev -Install| Parameter | Effect |
|---|---|
-Sign |
Sign binaries with a code-signing certificate |
-NoSign |
Skip signing |
-Thumbprint <hex> |
Use a specific certificate thumbprint |
-Binaries |
Build binaries only, then exit |
-Install |
Install the built MSI for the current architecture (requires elevation) |
-IntuneWin |
Also produce .intunewin packages |
-Dev |
Stop services, build Debug, skip signing |
-SignMSI |
Sign existing MSIs in release\ and exit; cannot be combined with other build flags |
-SkipMSI |
Skip MSI packaging |
-PackageOnly |
Package existing binaries (MSI and NuGet), skip the build |
-NupkgOnly |
NuGet packages only |
-MsiOnly |
MSI packages only |
-PkgOnly |
.pkg packages only; cannot be combined with -MsiOnly or -NupkgOnly
|
-Clean |
Remove release\ and every bin/obj under src\ first |
-Test |
Run dotnet test after building |
-Configuration <Debug|Release> |
Build configuration; default Release
|
-Architecture <x64|arm64|both> |
Target architecture; default both
|
Output lands in release\<arch>\ for binaries and release\ for packages.
Both projects publish under their assembly names and are then renamed:
| Published | Shipped |
|---|---|
StartSet.CLI.exe |
managedstatekeeper.exe |
StartSet.Service.exe |
StartSetService.exe |
Anything that references the CLI by path must use managedstatekeeper.exe.
build.ps1 derives the version from the build time:
| Form | Pattern | Example |
|---|---|---|
| Full — assembly version | yyyy.MM.dd.HHmm |
2026.03.01.1030 |
| Semantic — NuGet | YY.M.D.HHmm |
26.3.1.1030 |
| MSI-compatible | YY.M.DDHH |
26.3.0110 |
The CLI prints the assembly version as Major.MM.DD.HHMM.
build.ps1 loads a .env from the repo root if present (it is gitignored). The certificate is
selected from these environment variables, in order:
| Variable | Purpose |
|---|---|
STARTSET_CERT_CN / CIMIAN_CERT_CN / SIGNING_CERT_CN
|
Certificate common name, used for NuGet signing |
STARTSET_CERT_SUBJECT / CIMIAN_CERT_SUBJECT
|
Subject substring used to find the certificate in the store |
The script searches Cert:\CurrentUser\My then Cert:\LocalMachine\My for a certificate with a
private key whose subject matches, newest expiry first. signtool.exe is located on PATH, then
under the Windows Kits bin tree, then via HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots
— always preferring an x64 build. Signing retries up to four times across three timestamp
authorities (DigiCert, Sectigo, Entrust).
If no certificate is found the build continues and warns; it does not fail.
xUnit, under tests/StartSet.Tests:
| Area | Covers |
|---|---|
Core/ |
ExecutionResult, Paths, PayloadType extensions, ScriptPayload, preferences model |
Engine/ |
Processor routing and execution, script output formatting, user-context deferral |
Infrastructure/ |
Checksum service, items report, permission validator, preferences service, run-once tracker, session log retention |
Integration/ |
Engine integration, run-once lifecycle |
dotnet test StartSet.sln --configuration ReleaseExecutionEngine takes an optional payloadRoot constructor argument that redirects discovery to
a directory the test controls. This is not a convenience — without it, any test exercising
discovery reads the live payload directories of whatever machine it runs on, and on a machine
with StartSet installed would execute real login payloads for effect before failing its assertion.
Always pass a temporary root in tests that touch discovery; tests/StartSet.Tests/Helpers/TempDirectory.cs
exists for this.
.github/workflows/ci.yml runs on pull requests to main and pushes to main, on
windows-latest, with a 45-minute timeout and in-progress cancellation per ref:
- Checkout, set up .NET 10, restore NuGet from cache
dotnet restore StartSet.slndotnet build StartSet.sln --configuration Release --no-restore-
dotnet test StartSet.sln --configuration Release --no-restorewith a TRX logger - Upload
TestResults/*.trxas an artifact, always
.github/workflows/release.yml triggers on a pushed tag matching v*. There is no manual
dispatch and no branch trigger — a merge to main ships nothing.
git tag v2026.03.01.1030
git push origin v2026.03.01.1030The workflow, on windows-latest with contents: write:
- Checks out and sets up .NET 10
- Derives the version by stripping the leading
vfrom the tag name - Runs
.\build.ps1 -NoSign -Binaries— binaries only, unsigned - Copies any
.msi,.nupkg,.pkg,.intunewinor.zipfromrelease\intorelease-out\ - Zips
release\x64\*.exeandrelease\arm64\*.exeintostartset-x64.zipandstartset-arm64.zip, failing the run ifStartSetService.exeis missing from either - Generates release notes via the GitHub API, strips the contributor attributions and the "New Contributors" section, and wraps them with a build-info preamble and signing instructions
- Creates the GitHub release with
gh release createand uploads everything inrelease-out\
Because step 3 passes -Binaries, no MSI or NuGet package is produced in CI. The published
release contains only the two unsigned zips. Signed MSIs are a downstream concern: consumers
sign the binaries with their own certificate and repackage, per the instructions the workflow
embeds in the release notes.
The release body always states that artifacts are unsigned and links back to the workflow run and the commit that produced them.
-
Comments explain why, not what. The codebase's comments are unusually long and are load
bearing: they record the failure that motivated the current behaviour (why there is no SYSTEM
fallback for user-context payloads, why logon type 11 is watched, why
DWM-nis a regex and not a list, why the retention sweep is not recursive). Preserve that reasoning when changing the code around it. -
A silent success is a bug.
Deferredexists because a login payload that did nothing looked identical to one that worked. When adding a new failure mode, give it a status that says what is true rather than folding it intoSuccessorFailed. -
Payload-type behaviour belongs in the enum extensions.
IsRunOnce,RequiresElevation,IsUserContextandDeleteAfterExecutiononPayloadTypeExtensionsare the single source of truth.UserContextExecutionwas extracted from the PowerShell processor precisely because the user-session decision is a property of the payload type, not the file extension —.bat,.cmdand.exepayloads had been running as SYSTEM in session 0 while.ps1did the right thing. -
Pure functions where a test would otherwise need a machine.
FormatOutputLines,BuildItemRecord,CountRecentFailuresandSweepExpiredFilesare deliberately static and side-effect-light so they can be tested without a session on disk. - No
Co-Authored-Bytrailers, and no machine, user or organisation identifiers in the repository — it is public.
StartSet — MIT licensed — windowsadmins/startset — a Windows port of macadmins/outset.
StartSet
How it works
Migrating
Operating
Contributing