Skip to main content

Publishing software

A destination connection belongs to the Project. Each software document supplies destination settings under that connection's alias. An alias can be any useful name; its operation selects the implementation.

Use stemma plan to read a destination and see proposed changes. stemma apply re-observes and reconciles it. Both require prepared, reviewed input locks. Stemma reconciles destinations independently; publication to several services is not one transaction.

Mac installer outputs use <name>-<version>.<ext>, with a digest prefix when no version is available. BuildMacPkg can override the filename. Intune Win32 envelopes follow the same naming convention while preserving setup filenames inside the payload. Input filenames and publication names are independent.

Native fields and derived values​

Stemma derives supported fields from the selected artifact. Values you set take precedence. For fields you manage:

  • Omitted fields are left unchanged unless derivation owns them.
  • Supported null values clear a field.
  • Supplied lists replace the complete collection, including an empty list.

A field you set or derivation owns is managed. Missing derived values clear the field, or fail publication if the destination requires a value. Other fields remain unchanged. For example, omitting Intune assignments preserves existing assignments. It does not assert that an app is unassigned. Likewise, removing a field from YAML is not an instruction to clear its remote value unless derivation owns it. To publish a value other than the derived one, set it.

Munki​

The built-in munki destination writes a filesystem repository:

spec:
destinations:
munki:
operation: munki
config:
path: /srv/munki

Resource settings use native pkginfo fields:

destinations:
munki:
pkginfo:
catalogs:
- testing
description: Example application.
category: Productivity
retention:
keep: 1

The provider writes pkginfo, installers, declared icons and catalog indexes as munkiimport and makecatalogs would: a new item becomes pkgsinfo/<name>-<version>.plist and pkgs/<installer filename>, numbered when another item holds the name, and catalog entries omit notes and private keys. An item already in the repository keeps its paths. You do not need an intermediate pkginfo-rendering document. version defaults to the prepared installer's managed version: the selected application's, otherwise the Distribution product version or the version shared by its components. Application evidence supplies detection and DMG copy details where applicable. A PKG's static PackageInfo and Distribution declarations supply receipts, installed size and restart requirement; installer scripts are never evaluated. Receipts or copied items also supply the removal method, and an item with a method is uninstallable unless uninstallable is set. Explicit installs, receipts, installcheck_script and other supported native fields allow more specific behaviour. An installs item can be an inspected application, written as "{{ facts.name }}", to detect several applications.

minimum_os_version is the software's minimum macOS; pkginfo cannot declare it.

Munki's supported_architectures is an optional pkginfo restriction. There is no core spec.arch: download selection, installation eligibility and runner architecture are separate decisions. Items sharing a name and version are architecture variants; set the restriction on each so a document selects its own.

A declared icon arrives as an artifact input. Munki publishes it by content hash and Intune through its app icon field, both independently of installer uploads. Changed bytes replace the published icon and a missing one is added to an existing publication. Icons already in the Munki repository can be referenced with pkginfo.icon_name instead; that path is relative to the repository's icons/ directory, not to the catalog.

For source-free script items, see nopkg. Native endpoint behaviour is described in the Munki documentation.

Publication relationships​

Munki requires and update_for accept native strings or explicit resource references. Built-in Munki and the Woodstar destination use the same syntax:

pkginfo:
requires:
- Some Manually Created Munki Item
- External Item--1.2.3
- resource:
kind: MacSoftware
name: google-chrome
- resource:
kind: MacSoftware
name: example
version: "1.2.3"

A string is always a literal native Munki reference, including Munki's version syntax. It never selects a Stemma resource. A resource reference resolves the exact apiVersion/kind/name, with apiVersion defaulting to stemma/v1alpha1. The peer must declare the same named destination connection. The destination translates its metadata into the native name: google-chrome may publish as Google Chrome through pkginfo.name. An optional relationship-level version selects a package version.

Selected peers reconcile first. Unselected peers contribute declared metadata to locate an existing publication without being added to the run. Unknown references, wrong connections and cycles fail validation; they never fall back to native names. These publication relationships are separate from immutable resource-output dependencies.

Intune​

The Windows apps in Intune guide includes a complete connection, installation commands and detection examples. Authentication uses client credentials or an explicit access token. The Entra app needs these Graph application permissions, with admin consent. Planning needs only the read permission; DeviceManagementApps.ReadWrite.All also grants read access:

FeatureReadWrite
Apps, content and metadataDeviceManagementApps.Read.AllDeviceManagementApps.ReadWrite.All
Assignments (assignments)DeviceManagementApps.Read.AllDeviceManagementApps.ReadWrite.All
Categories (categories)DeviceManagementApps.Read.AllDeviceManagementApps.ReadWrite.All
Relationships (dependencies, supersedes)DeviceManagementApps.Read.AllDeviceManagementApps.ReadWrite.All

Groups and assignment filters are referenced by ID, so no directory or filter permission is needed.

Metadata names Intune concepts in Stemma's own fields, such as display_name, install_experience.run_as and detection; the schema lists them all. The software kind fixes the platform, so type follows the installer:

typeSoftwareContent
win32WindowsSoftwareMSI, EXE or setup tree, prepared internally as .intunewin
dmgMacSoftwareA Mac DMG
pkgMacSoftwareA Mac PKG
lobMacSoftwareA signed flat PKG, as a line-of-business app

Assignments target an Entra group by object ID, an excluded group, all devices or all users:

assignments:
- intent: required
group: 11111111-2222-3333-4444-555555555555
- intent: available
all_users: true

An included target can carry an assignment filter by ID and, for a Win32 app, its end-user notifications. A setting the assignment omits keeps its value, and filter: null removes a filter:

assignments:
- intent: required
all_devices: true
filter:
id: 66666666-7777-8888-9999-000000000000
mode: include
notifications: hide_all

categories lists the app's Company Portal categories by name and replaces the app's whole set; [] clears them. Names match exactly, including case. Apply creates a category the tenant doesn't have yet:

categories:
- Productivity

For macOS, detection uses the selected application's bundle identifier and version. Intune reports an installation only when every included app is present, so other applications an installer carries, such as a bundled updater, are left out. PKG apps can detect an application outside /Applications, and use package receipt identifiers and versions when a package selects no application. Only a package with a payload leaves a receipt. BuildMacPkg includes an empty payload when no files are declared. A payloadless vendor package needs explicit included_apps that detect what its scripts install. DMG and line-of-business apps require the application under /Applications. Set included_apps to replace the derived list, for example to require every application of a suite, or when static inspection cannot determine the installed application or an installer chooses it conditionally. A PKG app accepts receipts there too, so it can be detected by its package even when it installs an application. Each entry can be an inspected subject or an explicit identifier and version:

included_apps:
- "{{ facts.draw }}"
- "{{ facts.graph }}"
- id: org.example.script-installed
version: "2.0"

Application subjects use CFBundleShortVersionString; line-of-business detection also retains CFBundleVersion. Package subjects use the receipt identifier and version and require a payload. DMG and line-of-business entries accept application subjects installed under /Applications. The list replaces the derived detection list; its first entry supplies the primary identity.

display_name, description and publisher are always set; the artifact never supplies them.

A PKG can also publish as a line-of-business app, so that is the one type to declare. Stemma checks what Intune requires before uploading it: a flat PKG with a payload that installs an application under /Applications, at most 2 GiB, with a verified Developer ID Installer signature, so the software needs a signatures entry for the package with the expected signer. An unsigned PKG can use type: pkg; it cannot use type: lob. install_as_managed also needs one component that installs one application under /Applications.

The minimum OS is the setting for the release of the software's effective minimum macOS: its major version, or major and minor for 10.x, so 14.2 selects macOS 14 and the plan shows that mapping. A release Intune has no setting for fails publication.

A PKG app can run a script before and after it installs:

pre_install_script: |-
#!/bin/zsh --no-rcs

# Wait for someone to log in.
case $(/usr/bin/stat -f %Su /dev/console) in
root | loginwindow | _mbsetupuser | "") exit 1 ;;
esac
post_install_script: |-
#!/bin/zsh --no-rcs

/Applications/Example.app/Contents/MacOS/Example enroll --token '{{ env.EXAMPLE_TOKEN }}'

A pre-install script that exits nonzero stops the installation, and Intune tries again at the next check-in. Intune reports the app as installed whatever the post-install script's exit status. Each script holds at most 15359 characters, and null removes one. A script can carry environment values, so plans say that a script changes without showing it. A DMG app has no scripts; publish its application in a package to use them. A line-of-business app has none either.

Intune relationships​

Dependencies and supersedence connect compatible Win32 apps on the same Project connection:

destinations:
intune:
dependencies:
- resource:
kind: WindowsSoftware
name: vc-runtime
auto_install: true
supersedes:
- resource:
kind: WindowsSoftware
name: legacy-client
uninstall_previous: true

Resource references resolve to the app carrying that resource's canonical identity marker, or to the app_id it declares for this connection. An external app uses app_id directly instead of resource, with the same relationship policy:

dependencies:
- app_id: 11111111-2222-3333-4444-555555555555
auto_install: true

Exactly one of resource and app_id is required. Cycles, unpublished references and incompatible app types fail. The provider permits up to 99 declared dependencies and 9 supersedence targets, subject to the service's graph limits.

An ordinary update publishes new content to the same app ID, and the app keeps its earlier content versions. Supersedence relates separate apps explicitly; it does not create a new app for every release or retire the old app automatically. Omitted relationship categories are left unchanged; dependencies: [] clears that outgoing category.

An unassigned app may still install as a dependency of an assigned parent. Review incoming relationships as well as assignments when testing in a live tenant.

Jamf​

Connect using a Jamf API client:

spec:
destinations:
jamf:
operation: jamf
config:
url: https://example.jamfcloud.com
client_id: "{{ env.JAMF_CLIENT_ID }}"
client_secret: "{{ env.JAMF_CLIENT_SECRET }}"

Jamf publishes PKG artifacts: a vendor package, one selected with package_path, an application published in a package or a BuildMacPkg output. Jamf installs a DMG by copying its contents onto the startup disk, so an application DMG is not a Jamf package.

Each installer filename is one Jamf package record, named after the file. Changed bytes under the same filename upload into the same record. Upload requires a Jamf distribution configuration supporting the package upload API. package_id pins an existing package by its ID instead.

Install policies handle first installs, Self Service and enrollment workflows; the patch policy updates Macs that already have the software.

Install policies​

destinations:
jamf:
category: Productivity
policies:
- name: ALL - Visual Studio Code - ALL
enabled: true
category: Applications
frequency: ongoing
event: visual_studio_code
self_service:
description: Code editor for staff and students.
scope:
all_computers: true
- name: ALL - Visual Studio Code - Staff
enabled: true
category: Applications
triggers: [checkin, enrollment_complete]
frequency: once_per_computer
retries: 3
scope:
all_computers: false
computer_groups:
- Staff Macs

Policies are found by exact name. Renaming one creates a new policy and leaves the old one in Jamf. New policies start disabled and unscoped, with inventory updates enabled. Set enabled: true and a scope to deploy them.

Each policy installs the current package. Managing an existing policy replaces its package list; the plan lists removed packages.

triggers selects automatic events; event sets a custom trigger for jamf policy -event NAME, and event: "" removes it. frequency controls how often the policy runs. Use ongoing for Self Service reinstall availability; once_per_computer removes the item after it runs. retries retries failures at check-in and requires frequency: once_per_computer. update_inventory controls the inventory update after installation.

self_service takes true, false or settings. The Self Service name defaults to the selected application's name, then the software name; the category defaults to the package category; the icon is the software's icon. The policy category also defaults to the package category, and null removes either category. Omitting icon leaves existing artwork unchanged. Other omitted settings, including scripts and restarts, keep their Jamf values.

Patch policy​

destinations:
jamf:
patch:
title: Visual Studio Code
policy:
enabled: true
distribution: self_service
reminder_days: 3
deadline_days: 7
grace_minutes: 30
scope:
all_computers: true

patch associates the package with a version in an existing patch title. patch.version defaults to the prepared artifact's version: the selected application's version under version_key, otherwise the installer's. Set it explicitly when the title uses a different version, including with an expression:

patch:
title: Draw
version: "{{ facts.draw.app.version }}"

This changes the patch association and policy target without changing the prepared file's version or filename. Preparation requires a nonempty effective patch version. Until the title defines that version, the package publishes alone and a warning lists the title's recent definitions, which also shows when a title writes versions differently. The link and the policy's target version follow once the title defines it. The policy's other settings apply meanwhile, and a new policy waits for the definition. Stemma does not create definitions from the package.

The policy is found by its name under the title, which defaults to the software name, and keeps its ID as its target version changes. Like an install policy, a new one is created disabled and unscoped.

distribution: automatic installs updates at check-in. self_service offers them in Self Service with a notification, reminders and the software's icon. reminder_days sets the days between reminders, 1 unless set, and null turns reminders off. deadline_days installs an update automatically once it has been offered for that many days, and null removes the deadline. grace_minutes gives users time to save their work before the title's apps quit for an update. The title's definitions supply which apps quit and the minimum macOS. patch_unknown: true also updates Macs whose installed version the title does not define. allow_downgrade: true installs the target version over a newer one, as when the catalog returns to an earlier release.

Retention preserves every package still linked by a patch title, even with keep: 1. Policies omitted from the declaration remain unchanged.

Names and privileges​

Categories, patch titles and scope objects are named exactly as in Jamf. Each name must match exactly one object, or publication fails before anything is written. Scope privileges follow the kind of object named, whether it is a target, a limitation or an exclusion.

The API client's role needs these privileges for the features a catalog uses. Planning needs only the read privileges; applying needs both columns. Uploading Self Service icons needs none.

FeatureReadWrite
PackagesRead PackagesCreate Packages, Update Packages
Category by name (category)Read Categories
Install policies (policies)Read PoliciesCreate Policies, Update Policies
Patch title link (patch)Read Patch Management Software TitlesUpdate Patch Management Software Titles
Patch policy (patch.policy)Read Patch PoliciesCreate Patch Policies, Update Patch Policies
Scope naming computersRead Computers
Scope naming computer groupsRead Smart Computer Groups, Read Static Computer Groups
Scope naming buildingsRead Buildings
Scope naming departmentsRead Departments
Scope naming network segmentsRead Network Segments
Retention (retention)Read Policies, Read Computer PreStage Enrollments, Read Patch Management Software TitlesDelete Packages

Identity and retention​

Destinations identify publications from native keys or markers. Planning and applying need no local publication state.

DestinationIdentity
IntuneA marker line Stemma keeps in the app's notes, or app_id when set
JamfA marker line in the package's notes plus its filename, or package_id
MunkiThe item's name and version, and its architectures when set

Munki reconciles an existing item with the same identity. Intune and Jamf require the marker or an explicit app_id or package_id; display names are not unique. The marker occupies the final line of the notes and preserves the remaining text.

Changed bytes at the same version replace the existing content. After an interruption, the next run finds the remote object and uploads content if needed. Duplicate identities fail as ambiguous. retention.keep: N retains the current publication and the N−1 newest others the destination holds for the same software, including ones published before Stemma, plus anything still needed by native references. Protected publications do not consume those N slots.

DestinationFamily and orderCleanup
JamfPackages carrying the identity marker, by package IDUnreferenced package records and their content
MunkiItems sharing the name and declared architectures, by versionPkginfo, catalog entries, unreferenced installers

Cleanup follows successful publication and intended reference updates. Incomplete reference reads block cleanup. Munki also preserves referenced items whose catalogs or installation requirements differ from the current publication.

Keeping older content is not a rollback guarantee.