state
napt.state.deployment
Deployment state persistence for NAPT.
This module implements per-app deployment state: authoritative records of what NAPT has published to Intune and what is awaiting publication. Unlike the downloads folder, deployment state is not regenerable.
Each app gets its own file, state/deployment/<recipe-id>.json, so that
concurrent changes to different apps never conflict and each file's diff
is scoped to one app. A file names its app once at the top (app_id
matching the filename, plus the recipe's display name, refreshed on
every save) and holds five sections:
published: The release currently in Intune, with its SHA-256 hash and Intune app IDs. Null until the first upload. Publishing uploads the release without assigning it —napt promotedeploys it through the rings afterwards.install_assigned: The release the install entry is currently assigned to (the result of apromoteplan'sassignaction).pending: The discovered release awaiting publication, with version, download URL, and SHA-256 hash. A single slot — a newer discovery replaces an unpublished candidate (newest wins). Null when nothing is awaiting publication.rings: Which version currently holds each deployment ring. Written bynapt promote.retained: Displaced versions kept in Intune for rollback.
Serialization is deterministic (fixed reading-order keys, fixed
indentation, no timestamps), so re-running a command that produces no
logical change produces a byte-identical file and a clean git diff.
Keys follow reading order — lifecycle order at the top level, version
first and hashes last inside blocks — because these files are what a
publish PR diff shows its reviewer.
deployment_state_path
Returns the deployment state file path for a recipe.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state_dir
|
Path
|
Directory holding per-app deployment state files
(typically |
required |
recipe_id
|
str
|
Recipe identifier (from recipe's 'id' field). |
required |
Returns:
| Type | Description |
|---|---|
Path
|
Path to the app's deployment state file. |
Source code in napt/state/deployment.py
create_default_deployment_state
Creates an empty deployment state structure.
The identity fields (app_id, name) are stamped at save time —
app_id from the filename, name by whichever writer holds the
recipe configuration.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Deployment state with no published release, no pending release, |
dict[str, Any]
|
no ring assignments, and no retained versions. |
Source code in napt/state/deployment.py
load_deployment_state
Loads deployment state for one app.
Returns a default empty structure when the file does not exist. Does not create the file — deployment state is only written when there is something to record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state_path
|
Path
|
Path to the app's deployment state file. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Deployment state dictionary. |
Raises:
| Type | Description |
|---|---|
StateError
|
If the file exists but contains invalid JSON, its schemaVersion is missing or unsupported, or its declared app_id disagrees with the filename. Deployment state is authoritative, so a corrupted file is never silently replaced. |
Source code in napt/state/deployment.py
save_deployment_state
Saves deployment state for one app deterministically.
Creates parent directories if needed. Stamps the schema version and
the app_id (from the filename, which is the identity). Output is
byte-identical for logically identical state: keys follow reading
order, indentation is fixed at 2 spaces, rings are sorted by name,
and no timestamps or run-specific values are written.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
dict[str, Any]
|
Deployment state dictionary to save. |
required |
state_path
|
Path
|
Path to the app's deployment state file. |
required |
Raises:
| Type | Description |
|---|---|
OSError
|
If the file cannot be written due to permissions. |
Source code in napt/state/deployment.py
record_pending
Records a discovered release as the pending publication candidate.
The pending slot holds exactly one candidate and the newest discovery wins: a release that differs from both the published release and the current pending candidate replaces the pending candidate. Identity is the SHA-256 hash, not the version string, so a vendor re-release of the same version with a different binary is treated as new.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
dict[str, Any]
|
Deployment state dictionary to update in place. |
required |
version
|
str
|
Discovered version string. |
required |
sha256
|
str
|
SHA-256 hash of the discovered installer. |
required |
url
|
str
|
Download URL of the discovered installer. |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
A string naming the change made ("recorded" for a first candidate, "replaced" when a candidate was overwritten, "cleared" when the vendor serves the already-published release), or None when the state did not change. |
Source code in napt/state/deployment.py
record_published
record_published(
state: dict[str, Any],
version: str,
sha256: str,
intune_app_id: str | None,
intune_update_app_id: str | None,
) -> None
Records a successful publication as the published release.
Replaces the published section and clears the pending slot when
the pending candidate is the release that was just published. A
pending candidate with a different hash (a newer discovery) is left
in place.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
dict[str, Any]
|
Deployment state dictionary to update in place. |
required |
version
|
str
|
Published version string. |
required |
sha256
|
str
|
SHA-256 hash of the published release's installer. |
required |
intune_app_id
|
str | None
|
Graph API object ID of the install entry, or None when build_types is "update_only". |
required |
intune_update_app_id
|
str | None
|
Graph API object ID of the update entry, or None when build_types is "app_only". |
required |
Source code in napt/state/deployment.py
summarize_deployment_states
Summarizes all per-app deployment state files in a directory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
deployment_dir
|
Path
|
Directory holding per-app deployment state files. |
required |
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
One summary dict per app, sorted by app id, each with the app id,
published version, pending version, whether the pending
version is lower than the published one
( |
Raises:
| Type | Description |
|---|---|
StateError
|
On a corrupted deployment state file. |
Source code in napt/state/deployment.py
napt.state.stamp
Provenance stamp for NAPT-managed Intune apps.
The stamp is a single machine-parseable line written to the Intune notes field of every app NAPT publishes:
napt/v1 id=<recipe-id> entry=<install|update> sha256=<installer-hash>
It serves two purposes: ownership (presence of the stamp marks an app as NAPT-managed; unstamped apps are never touched) and identity (the recipe id, entry type, and installer hash tie the Intune object to a specific publish instance recorded in deployment state). The notes field is reserved for NAPT and is not recipe-configurable.
build_stamp
Builds the provenance stamp for one Intune app entry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
recipe_id
|
str
|
Recipe identifier (from recipe's 'id' field). |
required |
entry
|
str
|
Entry type, either "install" or "update". |
required |
sha256
|
str
|
SHA-256 hex digest of the source installer. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The stamp line to write to the Intune notes field. |
Raises:
| Type | Description |
|---|---|
ConfigError
|
If the stamp would exceed Intune's notes field length limit (only possible with an extremely long recipe id). |
Source code in napt/state/stamp.py
find_stamped_app
Finds the app whose provenance stamp matches a publish instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
apps
|
list[dict]
|
Mobile app dicts (with "notes") from list_mobile_apps. |
required |
recipe_id
|
str
|
Recipe identifier to match. |
required |
entry
|
str
|
Entry type to match ("install" or "update"). |
required |
sha256
|
str
|
Installer hash to match. |
required |
Returns:
| Type | Description |
|---|---|
dict | None
|
The matching app dict, or None when no stamped app matches. |
Source code in napt/state/stamp.py
parse_stamp
Parses a provenance stamp from an Intune notes field value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
notes
|
str | None
|
The notes field content, or None. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str] | None
|
A dict with "id", "entry", and "sha256" keys, or None when the notes do not carry a complete NAPT stamp. |