Changelog
All notable changes to NAPT (Not a Pkg Tool) will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
version_patternforapi_jsonrecipes - A regex applied to the value found atversion_path, the same fieldapi_githubandweb_scrapealready have. Use it when the API wraps the version in a prefix or suffix ("v2.0","2.0 (stable)"), which a device would otherwise read as version 0. Without it the API's value is used as is, so existing recipes are unchanged-
Recipe
parentfield - A recipe can name another recipe as its parent and inherit everything it does not set itself. The parent merges between vendor defaults and the recipe, lists replace rather than append, and a parent cannot declare a parent of its own- Name the child
<app>.override.yaml;napt validatewarns when the name and the field disagree and reports the parent it merged
- Name the child
-
Downgrade label - When a vendor replaces a release with a lower version,
napt discoverstill records it as the pending release, and now says so: discover logs a warning,napt statusmarks the app[DOWNGRADE]("pending_is_downgrade": truewith--format json), and the reference discover workflow titles the PRPublish <Name> <version> (downgrade from <published>). Versions are ordered exactly as the detection script on a device orders them. Publishing a downgrade changes what new installs get; devices already on the higher version are left alone
Changed
- BREAKING:
--state-dirmeans the state root for every command -napt discover --state-dir Xwrote state files directly intoX, whilenapt promoteandnapt statusreadX/deployment/andnapt buildhad no such flag, so state written that way was never found andnapt uploadran without its hash check. Discover now writes toX/deployment/like the others, andnapt buildgains--state-dir - A section left empty in a recipe is a validation error -
psadt:,intune:,logging:,deployment:,psadt.app_vars:, orintune.detection:with nothing under it used to crash the loader or load as nothing, silently skipping the values NAPT computes for that section.napt validatenow names the empty key - Reference discover workflow continues past a failing recipe - One
recipe's
napt discoverfailure used to end the loop and silently skip every recipe after it. The workflow in the docs now records the failure, still opens publish PRs for the apps that succeeded, and fails the run at the end with each failed recipe annotated on the run summary - BREAKING: Versions must start with a digit -
napt discoverstops with an error for a version such asv2.0,release-2.0, orlatest. A device compares versions by each part's leading digits, so such a version read as 0 there: every device reported the app installed and none ever upgraded, with no error anywhere. The error names the value aversion_patternshould capture. An MSI or MSIX is judged by its own version, so only EXE recipes whose pattern orapi_jsonvalue keeps a prefix are affected;api_githubalready strips avby default - BREAKING: The installer's own version is the version - For an MSI
or MSIX,
napt discovernow reads the version out of the downloaded file and records that, rather than the value the page or API reported. The download folder, the pending release, the publish PR title, and{{discovered_version}}all carry the same version the detection script will compare on a device, and a file the vendor serves under a newer number than it really is gets recorded as what it really is. The reported value is still the trigger: a run that finds the same value as last time reuses the download without a request. EXE installers are unchanged, since they carry no readable version- An MSI or MSIX app whose reported version differs in format from the
installer's (
4.41.106against4.41.106.0) gets a new download folder name, so it is downloaded once more after upgrading napt buildrefuses an MSI or MSIX whose version differs from the folder it is in, which only happens to a file moved by hand- Discover needs
msitoolson Linux for MSI recipes of every strategy, not onlyurl_download
- An MSI or MSIX app whose reported version differs in format from the
installer's (
url_downloadaccepts MSIX installers - The version is read from the package's Identity, as it already was for MSI ProductVersion- BREAKING: Downloads are filed by version -
napt discoversaves each installer todownloads/{id}/{version}/, matchingbuilds/andpackages/. A vendor that serves every release under one filename can no longer overwrite an installer that is awaiting approval. Installers in the old flatdownloads/{id}/layout are not found; runnapt discoveronce per app to download them again- CI workflows that hash downloads need the two-level path:
sha256sum "downloads/$id/"*/*(reference workflow 2 is updated)
- CI workflows that hash downloads need the two-level path:
- BREAKING:
napt buildfinds the installer by its recorded hash - Build reads the release to build from deployment state (the pending release, or the published one when nothing is pending), looks in that version's download folder, and takes the file whose SHA-256 matches. A file that was swapped or corrupted since discovery now stops the build instead of being packaged. Guessing the file from URLs and app names is removed- With no recorded release (
napt discover --statelessand no state), build uses the single installer found in a version folder (downloads/{id}/{version}/) and stops if there is more than one. A file placed directly indownloads/{id}/is not found
- With no recorded release (
- BREAKING: The discovery cache is removed -
cache/discovery.json, thenapt discover --cache-fileflag, and thedirectories.cachesetting are gone.napt discoverdecides whether to download from the downloads folder alone: a version whose folder already holds the installer is not fetched again, andurl_downloadrecipes keep the server'sETagindownloads/{id}/.download.json. Deletecache/discovery.json, drop--cache-filefrom scripts, and cache onlydownloadsin CI (the reference workflows are updated)--statelessnow means only "do not read or write deployment state"; installers already in the downloads folder are still reused
- BREAKING: Recipe
idmust be a plain folder name -napt validatenow rejects anidcontaining anything other than letters, digits,.,-,_, and+, or one that does not start with a letter or digit. Rename anyidthat uses spaces, slashes, or other characters - BREAKING: Discovered versions must be plain folder names - The same
character rule applies to a version, because it names the build folder.
napt discoverandnapt buildstop with an error for a version such as2.0 (x64). Tighten the recipe'sversion_patternso it captures only2.0; detection compares just the numeric parts, so nothing is lost.psadt.releaseandintunewin.releasefollow the rule too
Fixed
- Fixed a child recipe that sets a section its parent also sets (the
documented base-recipe case, such as
discovery.urlunder a parent that setsdiscovery.strategy) crashing every command with aTypeError, whilenapt validatereported VALID - Fixed a backslash in a
psadt.app_varsvalue corrupting the generated deployment script:Contoso\1Toolswas written asContoso$adtSession = @{Tools, and some values crashed the build - Fixed group and app names containing
&or#never resolving in Microsoft Graph, sonapt promotecould not assign to a group namedR&D #1 Team - Fixed a recipe path that is a folder, or a recipe saved as UTF-16 (the
PowerShell
Out-Filedefault), producing a raw traceback instead of a message naming the file; innapt promoteone such file aborted every app - Fixed
napt validate --debughiding a failure to merge the recipe behind VALID with no provenance block; it now prints the reason - Fixed the "Unknown discovery strategy" hint omitting
url_download - Fixed three Microsoft Graph error messages crashing with
UnicodeEncodeErroron a cp437 console (a plaincmd.exewindow), replacing the real permissions or metadata error with a traceback - Fixed non-ASCII MSI product names being read through the wrong code page
on Windows, so a name such as
Café Office™was recorded asCaf‚ OfficeT. The detection script and the uninstall command then looked for a name that never matched on devices, with no error at build time; some names crashed the build instead.napt buildnow reads the name exactly as the MSI stores it - Fixed a line break in an app name (an MSI
ProductName, an MSIXDisplayName, orintune.detection.display_name) ending the comment on the first line of the generated detection and requirements scripts, so that whatever followed the break would run as code on devices; on a Windows build host the same name crashed the build instead, because the script filename kept the line break. Control characters in a name are now replaced with a space, with a warning - Fixed a wrong first download never being corrected. When a vendor's page
or API announced a new version before the file behind the link changed,
napt discoverfiled the old file and then, since the page kept reporting the same version, never looked again. Discover now trusts a repeated version only if it matched the installer's own version last time; otherwise it warns on every run and asks the server whether the file changed (a full download when the server sends noETag), until the two agree - Fixed
napt discoverlabelling the installer it already had with an older version number when a vendor rolled a release back. Withapi_github,api_json, andweb_scraperecipes, the older version was treated as "not newer", so the newer installer was reused under the older version and recorded as the pending release. Each version now has its own download, so the older release is fetched and recorded as itself - Fixed
url_downloadrecipes making a wasted request when the savedETagoutlived the installer it described: the server answered "not modified", and NAPT then had to request the file a second time. TheETagis now sent only while that installer is on disk - Fixed a server's
ETagbeing ignored when it sent the header name in lowercase - Fixed a download server being able to choose where a file is saved. A
filename such as
..\..\evil.exein theContent-Dispositionheader (or its percent-encoded form) was joined onto the download folder as-is. Only the final part of the name is kept now - Fixed characters in a downloaded file's name running as code when a recipe
uses
{{installer_filename}}inside a quoted PowerShell string.$,;, backticks, and quote characters in the name are replaced with_, with a warning showing the original and saved names - Fixed a version string containing
..or a path separator pointingnapt buildat a folder outsidebuilds/, where a rebuild would delete it (closed by the version rule under Changed) - Fixed typographic quotes (such as
’and”) in an app name, MSI ProductName, MSIX identity,psadt.app_varsvalue, or installer path ending a PowerShell string early, which let the rest of the value run as code in the generated deployment, detection, and requirements scripts and in the MSI metadata commands NAPT runs during a build - Fixed the auto-generated MSI and MSIX install and uninstall commands
embedding the installer filename and MSIX identity without escaping, so a
$(...)or quote in a downloaded file's name could run as code. The commands now single-quote both values (MSIX installs build the path withJoin-Path) - Fixed detection and requirements scripts being written without a UTF-8 byte order mark. Windows PowerShell 5.1, which Intune uses to run them, read such files as the ANSI code page, so an app name with a non-ASCII character never matched the installed app, and some accented letters decoded to a quote character that ended the string early. All generated scripts now carry a BOM, as Microsoft recommends for Win32 detection scripts
0.10.0 - 2026-09-12
Added
napt auth setup- Create the NAPT app registration in a tenant without touching the portal: signs in as an administrator, then creates (or completes) the registration with its redirect URIs, application and delegated Graph permissions, service principal, and admin consent--federated-issuer/--federated-subject(plus optional--federated-audienceand--federated-name) also add a federated credential for OIDC-based CI/CD, so no client secret is needed; the values come from your CI platform's OIDC documentation- Stamps the registration's internal notes with
napt/v1 spec=<n> version=<napt version> provisioned=<date>; admin notes below the stamp are preserved - Safe to re-run: compares the registration against what the installed
NAPT needs and adds only what is missing, so updating after a NAPT
release is the same command.
--nameand--client-idtarget an existing registration - A name match without a NAPT stamp is reported and left untouched
until
--adoptis given; nothing existing is ever removed --print-onlyprints the equivalent portal checklist for tenants where the automated path is not allowed
napt auth login/status/logout- Sign in to Microsoft Graph once and let every later command authenticate silentlyloginopens the Windows account picker (Web Account Manager) when available, otherwise the browser;--no-brokerforces the browser- The client and tenant IDs given to the first
loginare remembered;AZURE_*environment variables are for CI/CD credentials only and no longer configure interactive sign-in statusshows which credential NAPT would use, the account, tenant, expiry, and Graph permissions it carries, and exits 1 naming any required permission that is missing- Multiple tenants are remembered;
login --tenant-id <id>switches between signed-in tenants without a prompt, andlogout --allclears them all - Tokens are cached in the OS credential store (DPAPI, Keychain, libsecret), never in plain files
- OIDC federation for CI/CD - An Azure CLI session signed in as a
service principal (what OIDC login steps such as GitHub Actions
azure/loginleave behind) is now honored, so pipelines can run without a client secret. This is the recommended CI/CD setup. A CLI signed in as a person is refused so Intune activity is always attributed to the NAPT registration
Changed
napt buildfails early for EXE recipes without scripts - An EXE installer whose recipe lackspsadt.installorpsadt.uninstallnow stops the build with a clear error instead of producing a package with empty install and uninstall sections- BREAKING:
logging.log_formatandlogging.log_levelrecipe fields removed - The fields never had any effect: generated detection and requirements scripts always log in CMTrace format and always log every message. Remove them from recipes anddefaults/org.yaml(leftovers produce an unknown-field warning fromnapt validate);logging.log_rotation_mbis unchanged and still works - BREAKING: Device code flow removed - Interactive sign-in is now
napt auth login(authorization code + PKCE, or the OS broker). Device code is the flow most often abused to bypass MFA and is increasingly blocked by Microsoft-managed Conditional Access policies- App registrations need the
http://localhostandms-appx-web://Microsoft.AAD.BrokerPlugin/<client-id>redirect URIs under Mobile and desktop applications - "Allow public client flows" is no longer needed; registrations set to Yes for earlier NAPT versions can go back to the default No, which blocks device code against the app
napt uploadandnapt promotenever open a browser; without a credential they fail withNot authenticated.and a hint to runnapt auth login
- App registrations need the
- BREAKING: Managed identity removed -
ManagedIdentityCredentialis no longer tried; use a service principal or OIDC federation - Removed the unused
--tenant-idoption fromnapt upload
Fixed
napt uploadnow honorsdirectories.packageinstead of always looking inpackages/, so a custom package directory indefaults/org.yamlno longer breaks upload with "No package found"
0.9.0 - 2026-07-20
Changed
- BREAKING: Per-app promotion plans -
napt promote plannow writes one plan file per app (state/plans/<app-id>.json) instead of a single batchedstate/plan.json, andnapt promote applyconsumes each file independently- One app's failure (unresolvable group, Graph error) keeps its plan file for retry and no longer blocks the other apps' promotions; failed apps are reported and fail the run after the others apply
- To hold one app's promotions during review, delete its plan file — no more hand-editing action entries inside a shared JSON
- Plan actions now carry reviewer context: the app's display name, and for ring advancement, when the release entered the ring it is leaving and that ring's bake threshold
- The reference workflows in Common Tasks watch
state/plans/**
- BREAKING: Plan files read at a glance - Every planned action now
opens with a plain-English
summarysentence and names the Intuneentryit touches (installorupdate) and the version itdisplaces; the app's id and display name appear once at the top of the file, and keys follow reading order instead of alphabetical- The action vocabulary now matches the feature:
promotemoves a release one ring forward (from_ring: nullmarks a first rollout) andassignpoints new installs at it — replacingenter_ring,advance_ring, andassign_install displacesmeans the older release loses the assignment but stays in Intune for rollback perdeployment.retain_versions— NAPT never uses Intune's Win32 supersedence featurenapt promote planandapplyprint the same summary sentences, so console output and plan files never disagree
- The action vocabulary now matches the feature:
- BREAKING: Deployment state files read at a glance -
state/deployment/<app-id>.jsonnow reads top-to-bottom as the publish PR's review recorddeployedis renamedpublished: publishing uploads a release to Intune without assigning it, andnapt promotedeploys it through the rings afterwards- Each file names its app once at the top:
app_id(stamped from the filename; a copied or renamed state file is now rejected) and the recipe's displayname, refreshed on every save - Keys follow reading order — lifecycle order at the top level,
versionfirst and hashes last inside blocks — instead of alphabetical
- Reviewer-friendly GitOps PRs - The reference workflows in Common
Tasks now generate the review surface instead of one-line PRs
- Publish PRs are titled
Publish <Name> <version>with a fact sheet body: version, currently published version, installer URL, SHA-256, what merging does, and how to hold or reject - The promotion PR body opens with a risk line
(
**This plan:** 2 to pilot, 1 to production), lists every app's plan action summaries, embeds the plan run's drift warnings, and carries apromotes-to-productionlabel when the final ring is targeted - Titles and bodies regenerate on every refresh, so a superseding release never leaves a stale decision under review
- The promotion PR branch is now
napt/promote-plan, matching thenapt promote plancommand
- Publish PRs are titled
- Graph API calls retry transient failures - Throttling (HTTP
429/503/509, honoring Retry-After) and transient server or
connection errors now retry with bounded exponential backoff across
upload, promotion, and drift operations, so a momentary Graph hiccup
no longer fails a publish or marks an app's promotion as failed
- Resource-creating calls retry only unambiguous throttling responses, so a reply lost after a processed create can never duplicate an Intune app; re-running converges via upload's stamp adoption
- Every Graph request carries a
client-request-idfor Microsoft support correlation - All remaining HTTP calls — GitHub release lookups (PSADT,
IntuneWinAppUtil,
api_githubdiscovery), vendor page and API fetches (web_scrape,api_json), and tool downloads — now share the installer download layer's retrying session instead of being single-shot
0.8.0 - 2026-07-12
Added
- Plan group validation - Every group named in a promotion plan is
now resolved against Entra ID before anything can act on it:
authenticated
napt promote planruns (--check-driftor--reconcile) fail without writing a plan when a group does not resolve, so a broken plan never becomes a reviewable promotion PR, andnapt promote applypreflights every action it would execute before executing any, so an unresolvable group aborts with zero tenant mutations instead of stranding a half-applied plan (a dead group referenced only by stale or already-applied actions never blocks a run). Offline plans (no flags) skip validation and warn when they produce actions; the apply preflight is their backstop - Publication recovery -
napt promote apply(always) andnapt promote plan --reconcile(opt-in) record publications that are fully committed in Intune but whose deployment state writeback was lost (e.g. a CI runner uploaded successfully, then failed to push the state commit). Recovery uses the same provenance-stamp evidence as idempotent upload adoption, requires every entry of the release to have committed content, and makes the recovered release promotable in the same run. Partially published releases are warned about instead — re-runnapt uploadto finish them
Changed
- Drift findings state what NAPT knows, not what it guesses - An
assignment NAPT has no record of making is now classified by
evidence:
unrecorded_assignmentwhen it matches a currently configured target (either a lost apply writeback, which a later apply converges, or an admin pre-empting configured policy) andunexpected_assignmentwhen it matches no configured target. The oldunexpected_assignmentmessage asserted "was not made by NAPT", which drift cannot actually know — Intune assignments carry no authorship, so state records are the only memory
Fixed
- Fixed three defects in the reference GitHub Actions workflows: the
publishandpromote-applywriteback pushes now rebase and retry instead of failing whenmainadvanced mid-run; the two workflows no longer share a concurrency group, which cancelled one of them when a single merge triggered both; andpublishnow fails fast with a clear error when the vendor no longer serves an approved installer, instead of failing minutes later at the upload hash gate - Fixed
napt uploadfailing with HTTP 400 (The mobile app content cannot be updated before the first content version is committed) when re-running after a crash between app creation and content commit. Such orphaned app records are now deleted and recreated, since Intune cannot resume them in place. - Fixed
napt uploadfailing intermittently with HTTP 403 (SAS identifier cannot be found) when Azure Storage had not yet propagated a freshly issued SAS URI. Blob uploads now retry transient failures with exponential backoff. - Fixed
napt buildfailing to find the installer (or its version) on machines that never rannapt discover, such as CI publish jobs restoringdownloads/from a cache. Build now falls back to the pending release URL and version recorded in committed deployment state when the local discovery cache is absent. Previously any non-url_downloadrecipe failed with "Cannot locate installer file" in the reference GitOps publish workflow.
0.7.0 - 2026-07-07
Added
- Assignment drift detection -
napt promote apply(always) andnapt promote plan --check-drift(opt-in) compare Intune assignments against deployment state and warn on every discrepancy: removed or changed NAPT assignments, admin-made assignments on NAPT-managed apps, releases missing from the tenant, and stamped apps no state references. Drift is reported, never corrected. - Built-in assignment targets -
"All Users"and"All Devices"in deployment group lists assign Intune's virtual targets instead of Entra ID groups. A real group sharing one of these display names must be referenced by its object ID -
napt promote apply- Executes a promotion plan against Intune: assigns install entries, enters and advances releases through rings, unassigns displaced releases, and retires them perdeployment.retain_versions(only NAPT-stamped apps are ever deleted). Consumesstate/plan.jsonwhen present, plans fresh otherwise. Stale or already-applied actions are skipped, so re-running after a partial failure is safe. Admin-made assignments are always preserved -
napt promote plan- Computes which releases enter or advance the configured deployment rings (plus first-time install-entry assignments) and writes a reviewablestate/plan.json. The plan file exists exactly when there is work — a run that finds nothing removes a stale plan — so its git status drives review workflows. Read-only; applying plans arrives withnapt promote apply -
napt status- Shows deployed version, pending release, and ring positions across all apps;--format jsonfor scripting -
Deployment configuration - New
deployment:recipe section:require_pendingmakesnapt uploadfail unless the release was recorded at discovery (for review-gated publish workflows);rings,install, andretain_versionsdefine update promotion policy for the upcomingnapt promotecommand. Group name resolution requires theGroup.Read.Allapplication permission -
Deployment state files -
napt discoverrecords each discovered release instate/deployment/{id}.jsonas a pending publication candidate. One file per app; the newest discovery wins - Idempotent upload - Re-running
napt uploadadopts the existing NAPT-stamped Intune apps for a release instead of creating duplicates, and resumes the content upload when a previous run crashed mid-publish. Pass--forceto re-send metadata and content to the existing apps (e.g., after changing PSADT commands without a new installer release) - Upload provenance -
napt uploadstamps each Intune app entry's notes field withnapt/v1 id=<recipe-id> entry=<install|update> sha256=<hash>and records the published version, hash, and Intune app IDs in deployment state - Installer hash verification -
napt uploadrefuses to publish a package whose installer hash does not match the pending release recorded at discovery, so what was approved is exactly what ships
Changed
- BREAKING: Deployment state and plan files carry a schema version -
state/deployment/*.jsonandstate/plan.jsonare now stamped withschemaVersion: 1; NAPT refuses files whose schemaVersion is missing or unsupported. Add"schemaVersion": 1to files written by earlier pre-releases (plan files can simply be regenerated) - BREAKING:
intune.notesremoved - The Intune notes field is reserved for NAPT's provenance stamp. Move any notes content tointune.descriptionorintune.owner - BREAKING: Discovery cache moved -
state/versions.jsonis nowcache/discovery.json, andnapt discover --state-filewas renamed to--cache-file. Delete the old file; the cache rebuilds on the next run- New
directories.cacheanddirectories.statesettings --statelessalso skips deployment state writesnapt initadditionally createsstate/deployment/
- New
0.6.0 - 2026-07-04
Added
-
Auto-generated MSI install/uninstall commands -
napt buildnow generates install (Start-ADTMsiProcess, withALLUSERS=1appended for system deployments) and uninstall (Uninstall-ADTApplicationby exact ProductName) commands for MSI installers, so MSI recipes no longer needpsadt.installorpsadt.uninstall. Uninstall matches by name, not ProductCode, so it keeps working when vendors change the ProductCode between versions. Setpsadt.override_msi_commands: trueto use recipe commands instead -
Automatic app icon extraction -
napt buildextracts the app icon from MSI, EXE, and MSIX installers toicons/{id}.png, andnapt uploadsends it as the app logo in Intune and the Company Portal. Only PNG-encoded frames of at least 128px are used, preferring the size closest to Intune's recommended 256px. Drop a custom PNG aticons/{id}.pngto override (NAPT never overwrites existing files), or setintune.logo_pathto disable extraction for a recipe {{installer_filename}}recipe variable - Substituted at build time inpsadt.install,psadt.uninstall, andpsadt.app_varswith the exact filename of the downloaded installer in the package's Files directory. Replaces wildcard paths, which PSADT does not support- Unrecognized variable warning -
napt buildwarns when anapp_varsvalue or an install/uninstall script contains a{{snake_case}}token that is not a supported NAPT variable
Changed
- BREAKING: Recipe variable syntax - NAPT build-time variables now use
{{...}}instead of${...}. Replace${discovered_version}with{{discovered_version}}in recipes.${...}now exclusively means environment variables, which work only indiscovery.tokenanddiscovery.headers intune.logo_pathfile types restricted to PNG and JPEG - Other file types now warn and fall back to the extracted icon instead of uploading with a guessed MIME type
Fixed
- Fixed the version variable never being substituted in
psadt.installandpsadt.uninstallscripts, where it reached PowerShell as literal${...}syntax and silently expanded to an empty string at deploy time - Fixed 7-Zip recipes failing at deploy time because PSADT resolves
-FilePathliterally and does not expand wildcards; the recipes now use{{installer_filename}} - Fixed
intune.logo_pathrelative paths resolving from the current working directory instead of the recipe file's location as documented
0.5.1 - 2026-06-22
Fixed
- Fixed system-scope MSIX detection and requirements scripts matching any installed package instead of only the target app, which could cause Intune to report the wrong install state
- Fixed malformed timestamps in detection and requirements script logs when the device is in a UTC+ timezone, which made CMTrace display incorrect log times
- Fixed detection and requirements scripts failing to detect installed apps
whose registry version contains non-numeric text (e.g.
5.2 (64-bit)) - Fixed generated detection and requirements scripts breaking when the app name contains special characters such as quotes or dollar signs
- Fixed crash when an MSIX manifest's
<PublisherDisplayName/>element is present but contains no text. Publisher now defaults to an empty string in that case instead of triggering a downstreamTypeError.
0.5.0 - 2026-04-05
Added
- Automatic recipe validation in pipeline commands -
napt discover,napt build, andnapt uploadnow validate the merged configuration before proceeding. Invalid recipes produce clear error messages instead of failing with unexpected errors mid-pipeline - Configuration provenance in
napt validate --debug- Shows which config layer (code default, org.yaml, vendor defaults, or recipe) set each value. Helps debug unexpected configuration by tracing the full merge history - MSIX installer support - NAPT now supports
.msixinstallers alongside MSI and EXE. Metadata (display name, version, architecture, package identity) is extracted fromAppxManifest.xmlinside the package. Detection and requirements scripts query the AppX package database; install and uninstall commands are auto-generated from manifest metadata unless overridden withpsadt.override_msix_commands: true. Install scope is controlled byintune.run_as_account:"system"(default) uses provisioned cmdlets (Add-AppxProvisionedPackage/Remove-AppxProvisionedPackage);"user"uses per-user cmdlets (Add-AppxPackage/Remove-AppxPackage). Detection and requirements scripts automatically query the correct store based on the same setting.psadt.app_vars.RequireAdmindefaults tofalsewhen scope is"user" intunewin.releaseconfig key - PinIntuneWinAppUtil.exeto a specific release for reproducible builds (e.g.,release: "1.8.6"). Defaults to"latest", which resolves the current release via GitHub API. Each version is cached independently undercache/tools/{version}/logging:top-level section - New optional section for per-recipe logging configuration. Supportslog_format(cmtrace,text),log_level(verbose,debug), andlog_rotation_mb- New
intune:fields - Expanded metadata and upload behavior fields, all configurable at org, vendor, or recipe level:developer,owner,notes,logo_path,minimum_supported_windows_release,install_command,uninstall_command,is_featured(Company Portal featured app, defaults tofalse),allow_available_uninstall,device_restart_behavior,max_run_time_minutes,enforce_signature_check, andrun_as_32_bit - Sample recipe:
recipes/Microsoft/vscode.yaml- New example recipe for Visual Studio Code using theapi_jsonstrategy
Changed
- Build script module restructured - Detection and requirements script
generation consolidated into
registry_scriptsmodule (registry-based MSI/EXE) and newmsix_scriptsmodule (AppX-based MSIX). Shared PowerShell logging functions split from registry-specific helpers for cleaner template reuse - BREAKING: Recipe schema flattened - The
app:wrapper is removed. Fieldsname,id,discovery:,psadt:,intune:, andlogging:are now top-level. Update all recipes by moving fields out ofapp: - BREAKING:
source:renamed todiscovery:- All discovery configuration must move fromsource:todiscovery:. Affects all four strategies:api_github,api_json,url_download,web_scrape - BREAKING:
win32.installed_checkreplaced byintune.detection- Detection configuration moves fromwin32.installed_check.*tointune.detection.*. Fields are unchanged (display_name,architecture,exact_match,override_msi_display_name) - BREAKING: Directory config keys renamed -
defaults.discover.output_dir,defaults.build.output_dir, anddefaults.package.output_dirare nowdirectories.discover,directories.build, anddirectories.package. Updatedefaults/org.yamlif you set these keys
Fixed
- Cached installer filename now survives re-runs - NAPT stores the actual
downloaded filename in state after each download. On subsequent runs,
cache hits use the stored path instead of re-deriving from the recipe URL.
Previously, if the server returned a
Content-Dispositionheader or the download URL redirected to a different path, the cached file could not be found and the run would fail - Incomplete or stale cache no longer requires
--stateless- If the cache is missing fields or the cached file was deleted, NAPT automatically forces a fresh download instead of failing with a confusing error message intune.device_restart_behaviordefault corrected to"basedOnReturnCode"- Previously an inline fallback used"allow"instead of the value defined inDEFAULT_CONFIGnapt uploadnow creates two Intune Win32 app entries whenbuild_typesis"both"- Previously only one entry was created regardless ofbuild_types. The install entry (detection script only, base app name) and the update entry (detection + requirements scripts, prefixed withupdate_name_prefix) are now each created, uploaded, and committed in sequence. Single-entry behavior for"app_only"and"update_only"is unchanged- Version cache uses semantic comparison - Versions with a
vprefix (e.g.,v1.2.3vs1.2.3) are now correctly recognized as matching and won't re-download unnecessarily
0.4.0 - 2026-03-08
Added
napt upload <recipe>- New command uploads.intunewinpackages directly to Microsoft Intune via the Graph API. Authentication tries service principal (AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_TENANT_ID), then managed identity, then device code (requiresAZURE_CLIENT_ID+AZURE_TENANT_IDset and a TTY)napt package --version VERSION- New flag to target a specific build version instead of the most recent (e.g.,napt package recipes/Google/chrome.yaml --version 130.0.6723.116)- Configurable directory defaults and new CLI flags - All pipeline directory
flags now read from
defaults/org.yamlwhen not set on the CLI, and new flags are available on each command:napt discover --output-dir DIR- where to save downloaded installersnapt build --downloads-dir DIR- where to find the installernapt build --output-dir DIR- where to save buildsnapt package --builds-dir DIR- where to find the buildnapt package --output-dir DIR- where to save packages Three new config keys control the defaults:defaults.discover.output_dir(downloads),defaults.build.output_dir(builds), anddefaults.package.output_dir(packages). Each key is shared between the command that produces and the command that consumes the directory
- Code-Based Defaults - NAPT now ships with complete built-in defaults, making
pip install naptwork out of the box without requiring any configuration files. Organization defaults (defaults/org.yaml) and vendor defaults are now optional overrides rather than requirements napt initCommand - New command to scaffold NAPT project structure. Createsrecipes/,defaults/vendors/, and a commenteddefaults/org.yamltemplate. Safely skips existing files; use--forceto overwrite with automatic backup
Changed
napt packagenow takes<recipe>instead of<build_dir>- All commands now take a recipe path for consistent CLI usage. The build directory is inferred automatically from the recipe's app ID by scanning the builds output directory for the most recent completed buildnapt packageoutputs to versioned paths - Package output is nowpackages/{app_id}/{version}/Invoke-AppDeployToolkit.intunewin. Only one version is kept per app — the previous version directory is removed automatically when a new one is packaged (single-slot). Detection and requirements scripts are copied alongside the.intunewinfile sonapt uploadis self-contained and does not need the builds directory- Four-Layer Configuration - Configuration system now has four layers: code defaults (baseline) -> org.yaml (optional) -> vendor.yaml (optional) -> recipe (required). Old configs continue to work; new fields automatically get code defaults
0.3.1 - 2026-02-03
Changed
- PyPI Package Name - Package renamed from
notapkgtooltonaptfor simpler installation (pip install napt) - Automated PyPI Publishing - Releases now automatically publish to PyPI via GitHub Actions using Trusted Publisher (OIDC)
0.3.0 - 2026-02-02
Added
- Win32 Configuration Validation - Recipe validation now checks
win32configuration fields for correct types and values, with typo detection suggesting similar field names (e.g., "Did you mean 'display_name'?") - Detection Script Generation - Automatic PowerShell detection script generation for Intune Win32 app deployments during build process. Scripts check Windows uninstall registry keys, support exact or minimum version matching, and include CMTrace-formatted logging
- Requirements Script Generation - Automatic PowerShell requirements script generation for Intune Update app entries. Scripts check if an older version is installed and output "Required" when applicable
- Installer Type Filtering - Detection and requirements scripts now filter registry entries based on installer type. MSI builds strictly match MSI registry entries only; EXE builds permissively match any entry to handle EXE installers that run embedded MSIs internally
- Architecture-Aware Detection - Detection and requirements scripts now use explicit registry views based on target architecture, preventing false positives when x86 and x64 versions coexist. MSI architecture is auto-detected from Template property; non-MSI installers require
win32.installed_check.architectureconfiguration (x86, x64, arm64, or any) - MSI Display Name Override - New
override_msi_display_nameflag allows using customdisplay_nameinstead of MSI ProductName for registry lookups, solving edge cases where ProductName contains version numbers (e.g., "7-Zip 25.01"). Supports wildcards (*,?) for flexible matching
Changed
- BREAKING: Non-MSI Architecture Required -
win32.installed_check.architectureis now required for non-MSI installers (EXE, etc.). Allowed values:x86,x64,arm64,any - Discovery Performance Optimization - Version-first strategies (web_scrape, api_github, api_json) now check versions before downloading, enabling ~100-300ms update checks when unchanged instead of full downloads
- State file now saves actual download URLs for all strategies
- url_download Strategy Simplification - Removed
version.typeconfiguration requirement. MSI files are now auto-detected by file extension (.msi) for version extraction - BREAKING: Uniform Strategy Naming - Discovery strategies renamed to follow consistent
<source>_<method>pattern for better discoverability and scalability:http_static→url_download(fixed URL with file extraction)url_regex→web_scrape(web scraping for vendor download pages)http_json→api_json(generic JSON API queries)github_release→api_github(GitHub releases API)
- BREAKING: Simplified Version Types - Version type names shortened for clarity:
msi_product_version_from_file→msi- Removed nested
version.typeforweb_scrape(simplified tosource.link_selectorandsource.version_pattern)
- BREAKING: Recipe Format Change - Changed recipe format from
apps:array toapp:single object. Recipes now define a single application per file instead of an array. This simplifies the schema and matches actual usage (only one app was ever processed per recipe). - Documentation Rendering - Fixed module docstrings to follow Google-style format with proper indentation for mkdocstrings
Fixed
- Fixed ETag preservation bug causing alternating download/cached behavior in url_download strategy
- Fixed docstring formatting issues across multiple modules (missing blank lines, incorrect Args sections)
0.2.0 - 2025-11-07
Added
- PSADT Package Generation with
napt buildcommand - Creates complete PSADT v4 deployment packages with custom branding support - .intunewin Package Creation with
napt packagecommand - Generates Intune-ready packages using Microsoft's IntuneWinAppUtil.exe - GitHub Release Strategy (
api_github) - Discovers versions from GitHub releases with asset pattern filtering - HTTP JSON Strategy (
api_json) - Extracts versions and download URLs from JSON API endpoints using JSONPath - URL Regex Strategy (
url_pattern) - Extracts versions directly from URLs using regex patterns - Roadmap Management (
docs/roadmap.md) - Structured feature tracking with status categories and workspace automation - MkDocs Documentation Site - User guide and auto-generated API reference
Changed
- State File Schema v2 - Convention-based file paths, improved metadata tracking, per-app isolation (Breaking: old state files need regeneration)
- CLI Commands - Renamed
checktovalidatefor clarity - Recipe Format - PSADT
install/uninstallblocks now generate PSAppDeployToolkit v4 scripts - Console Output - Replaced Unicode symbols with ASCII for Windows compatibility (
✓→[OK], etc.)
Fixed
- PSADT Template Handling - Correctly identifies and copies PSAppDeployToolkit_Template_v4.zip files
- ETag Preservation - Fixed bug causing alternating download/cached behavior in url_download strategy
- Branding Application - Fixed Assets/ directory path resolution for custom icons and banners
- Version Extraction - Corrected regex escape sequences causing SyntaxWarnings
0.1.0 - 2025-10-23
Initial internal release.
Added
- Recipe Validation with
napt checkcommand - Validates recipe syntax and configuration without network calls - HTTP Static Discovery - Downloads installers from static URLs with ETag caching for efficiency
- Three-Layer Configuration - Organization defaults, vendor overrides, and recipe-specific settings with deep merging
- Version Comparison - Supports semantic versioning, MSI/EXE numeric versions, and Chrome-style multi-part versions
- MSI Version Extraction - Cross-platform support (Windows via msilib/PowerShell, Linux/macOS via msitools)
- Robust Downloads - Retry logic, atomic writes, SHA-256 verification, and conditional requests